package registry import ( "encoding/json" "fmt" "strings" "time" ) func marshalStrings(ss []string) string { if ss == nil { ss = []string{} } b, _ := json.Marshal(ss) return string(b) } func unmarshalStrings(s string) []string { var out []string json.Unmarshal([]byte(s), &out) if out == nil { out = []string{} } return out } func marshalJSON(v map[string]any) string { if v == nil { v = map[string]any{} } b, _ := json.Marshal(v) return string(b) } func unmarshalJSON(s string) map[string]any { var out map[string]any json.Unmarshal([]byte(s), &out) if out == nil { out = map[string]any{} } return out } func marshalProps(ps []PropDef) string { if ps == nil { ps = []PropDef{} } b, _ := json.Marshal(ps) return string(b) } func unmarshalProps(s string) []PropDef { var out []PropDef json.Unmarshal([]byte(s), &out) return out } // InsertFunction inserts or replaces a function entry. func (db *DB) InsertFunction(f *Function) error { now := time.Now().UTC().Format(time.RFC3339) if f.CreatedAt.IsZero() { f.CreatedAt = time.Now().UTC() } f.UpdatedAt = time.Now().UTC() if f.ID == "" { f.ID = GenerateID(f.Name, f.Lang, f.Domain) } var hasState *int if f.HasState != nil { v := 0 if *f.HasState { v = 1 } hasState = &v } _, err := db.conn.Exec(` INSERT OR REPLACE INTO functions ( 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, props, emits, has_state, framework, variant, notes, documentation, code ) 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, marshalProps(f.Props), marshalStrings(f.Emits), hasState, f.Framework, marshalStrings(f.Variant), f.Notes, f.Documentation, f.Code, ) return err } // InsertType inserts or replaces a type entry. func (db *DB) InsertType(t *Type) error { now := time.Now().UTC().Format(time.RFC3339) if t.CreatedAt.IsZero() { t.CreatedAt = time.Now().UTC() } t.UpdatedAt = time.Now().UTC() if t.ID == "" { t.ID = GenerateID(t.Name, t.Lang, t.Domain) } _, err := db.conn.Exec(` 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, 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.Examples, t.Notes, t.Documentation, t.Code, ) return err } // SearchFunctions performs FTS search on functions with optional filters. func (db *DB) SearchFunctions(query string, kind Kind, purity Purity, lang, domain string) ([]Function, error) { where := []string{} args := []any{} if query != "" { where = append(where, "f.id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH ?)") args = append(args, query) } if kind != "" { where = append(where, "f.kind = ?") args = append(args, string(kind)) } if purity != "" { where = append(where, "f.purity = ?") args = append(args, string(purity)) } if lang != "" { where = append(where, "f.lang = ?") args = append(args, lang) } if domain != "" { where = append(where, "f.domain = ?") args = append(args, domain) } sql := "SELECT * FROM functions f" if len(where) > 0 { sql += " WHERE " + strings.Join(where, " AND ") } sql += " ORDER BY f.name" rows, err := db.conn.Query(sql, args...) if err != nil { return nil, fmt.Errorf("search functions: %w", err) } defer rows.Close() return scanFunctions(rows) } // SearchTypes performs FTS search on types with optional filters. func (db *DB) SearchTypes(query string, lang, domain string) ([]Type, error) { where := []string{} args := []any{} if query != "" { where = append(where, "t.id IN (SELECT id FROM types_fts WHERE types_fts MATCH ?)") args = append(args, query) } if lang != "" { where = append(where, "t.lang = ?") args = append(args, lang) } if domain != "" { where = append(where, "t.domain = ?") args = append(args, domain) } sql := "SELECT * FROM types t" if len(where) > 0 { sql += " WHERE " + strings.Join(where, " AND ") } sql += " ORDER BY t.name" rows, err := db.conn.Query(sql, args...) if err != nil { return nil, fmt.Errorf("search types: %w", err) } defer rows.Close() return scanTypes(rows) } // GetFunction returns a single function by ID. func (db *DB) GetFunction(id string) (*Function, error) { rows, err := db.conn.Query("SELECT * FROM functions WHERE id = ?", id) if err != nil { return nil, err } defer rows.Close() fns, err := scanFunctions(rows) if err != nil { return nil, err } if len(fns) == 0 { return nil, fmt.Errorf("function %q not found", id) } return &fns[0], nil } // GetType returns a single type by ID. func (db *DB) GetType(id string) (*Type, error) { rows, err := db.conn.Query("SELECT * FROM types WHERE id = ?", id) if err != nil { return nil, err } defer rows.Close() ts, err := scanTypes(rows) if err != nil { return nil, err } if len(ts) == 0 { return nil, fmt.Errorf("type %q not found", id) } return &ts[0], nil } // GetFunctionsByName returns all functions matching a given name (across langs/domains). func (db *DB) GetFunctionsByName(name string) ([]Function, error) { rows, err := db.conn.Query("SELECT * FROM functions WHERE name = ? ORDER BY lang, domain", name) if err != nil { return nil, err } defer rows.Close() return scanFunctions(rows) } // DeleteFunction removes a function by ID. func (db *DB) DeleteFunction(id string) error { _, err := db.conn.Exec("DELETE FROM functions WHERE id = ?", id) return err } // DeleteType removes a type by ID. func (db *DB) DeleteType(id string) error { _, err := db.conn.Exec("DELETE FROM types WHERE id = ?", id) return err } // InsertApp inserts or replaces an app entry. func (db *DB) InsertApp(a *App) error { now := time.Now().UTC().Format(time.RFC3339) if a.CreatedAt.IsZero() { a.CreatedAt = time.Now().UTC() } a.UpdatedAt = time.Now().UTC() if a.ID == "" { a.ID = GenerateID(a.Name, a.Lang, a.Domain) } _, err := db.conn.Exec(` 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, 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, ) return err } // GetApp returns a single app by ID. func (db *DB) GetApp(id string) (*App, error) { rows, err := db.conn.Query("SELECT * FROM apps WHERE id = ?", id) if err != nil { return nil, err } defer rows.Close() apps, err := scanApps(rows) if err != nil { return nil, err } if len(apps) == 0 { return nil, fmt.Errorf("app %q not found", id) } return &apps[0], nil } // SearchApps performs FTS search on apps with optional filters. func (db *DB) SearchApps(query string, lang, domain string) ([]App, error) { where := []string{} args := []any{} if query != "" { where = append(where, "a.id IN (SELECT id FROM apps_fts WHERE apps_fts MATCH ?)") args = append(args, query) } if lang != "" { where = append(where, "a.lang = ?") args = append(args, lang) } if domain != "" { where = append(where, "a.domain = ?") args = append(args, domain) } sql := "SELECT * FROM apps a" if len(where) > 0 { sql += " WHERE " + strings.Join(where, " AND ") } sql += " ORDER BY a.name" rows, err := db.conn.Query(sql, args...) if err != nil { return nil, fmt.Errorf("search apps: %w", err) } defer rows.Close() return scanApps(rows) } func scanApps(rows interface{ Next() bool; Scan(...any) error }) ([]App, error) { var result []App for rows.Next() { var a App var tagsJSON, usesFnJSON, usesTypJSON string var createdAt, updatedAt string 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, ) if err != nil { return nil, fmt.Errorf("scanning app: %w", err) } a.Tags = unmarshalStrings(tagsJSON) a.UsesFunctions = unmarshalStrings(usesFnJSON) a.UsesTypes = unmarshalStrings(usesTypJSON) a.CreatedAt, _ = time.Parse(time.RFC3339, createdAt) a.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt) result = append(result, a) } return result, nil } // Purge deletes all data from functions, types and apps. Used before re-indexing. func (db *DB) Purge() error { if _, err := db.conn.Exec("DELETE FROM functions"); err != nil { return err } if _, err := db.conn.Exec("DELETE FROM types"); err != nil { return err } _, err := db.conn.Exec("DELETE FROM apps") return err } func scanFunctions(rows interface{ Next() bool; Scan(...any) error }) ([]Function, error) { var result []Function for rows.Next() { var f Function var tagsJSON, usesFnJSON, usesTypJSON, returnsJSON, importsJSON, testsJSON string var propsJSON, emitsJSON, variantJSON string var hasState *int var createdAt, updatedAt string err := rows.Scan( &f.ID, &f.Name, &f.Kind, &f.Lang, &f.Domain, &f.Version, &f.Purity, &f.Signature, &f.Description, &tagsJSON, &usesFnJSON, &usesTypJSON, &returnsJSON, &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, ) if err != nil { return nil, fmt.Errorf("scanning function: %w", err) } f.Tags = unmarshalStrings(tagsJSON) f.UsesFunctions = unmarshalStrings(usesFnJSON) f.UsesTypes = unmarshalStrings(usesTypJSON) f.Returns = unmarshalStrings(returnsJSON) f.Imports = unmarshalStrings(importsJSON) f.Tests = unmarshalStrings(testsJSON) f.Props = unmarshalProps(propsJSON) f.Emits = unmarshalStrings(emitsJSON) f.Variant = unmarshalStrings(variantJSON) f.CreatedAt, _ = time.Parse(time.RFC3339, createdAt) f.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt) if hasState != nil { v := *hasState == 1 f.HasState = &v } result = append(result, f) } return result, nil } func scanTypes(rows interface{ Next() bool; Scan(...any) error }) ([]Type, error) { var result []Type for rows.Next() { var t Type var tagsJSON, usesTypJSON string var createdAt, updatedAt string err := rows.Scan( &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, ) if err != nil { return nil, fmt.Errorf("scanning type: %w", err) } t.Tags = unmarshalStrings(tagsJSON) t.UsesTypes = unmarshalStrings(usesTypJSON) t.CreatedAt, _ = time.Parse(time.RFC3339, createdAt) t.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt) result = append(result, t) } return result, nil } // --- Proposal CRUD --- // InsertProposal inserts or replaces a proposal. func (db *DB) InsertProposal(p *Proposal) error { now := time.Now().UTC() if p.CreatedAt.IsZero() { p.CreatedAt = now } p.UpdatedAt = now if p.Status == "" { p.Status = ProposalPending } _, err := db.conn.Exec(` INSERT OR REPLACE INTO proposals ( id, kind, target_id, title, description, evidence, status, created_by, reviewed_by, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, p.ID, string(p.Kind), p.TargetID, p.Title, p.Description, marshalJSON(p.Evidence), string(p.Status), p.CreatedBy, p.ReviewedBy, p.CreatedAt.Format(time.RFC3339), p.UpdatedAt.Format(time.RFC3339), ) return err } // GetProposal returns a proposal by ID. func (db *DB) GetProposal(id string) (*Proposal, error) { rows, err := db.conn.Query(` SELECT id, kind, target_id, title, description, evidence, status, created_by, reviewed_by, created_at, updated_at FROM proposals WHERE id = ?`, id) if err != nil { return nil, err } defer rows.Close() ps, err := scanProposals(rows) if err != nil { return nil, err } if len(ps) == 0 { return nil, fmt.Errorf("proposal %q not found", id) } return &ps[0], nil } // UpdateProposal updates an existing proposal. func (db *DB) UpdateProposal(p *Proposal) error { p.UpdatedAt = time.Now().UTC() _, err := db.conn.Exec(` UPDATE proposals SET kind=?, target_id=?, title=?, description=?, evidence=?, status=?, created_by=?, reviewed_by=?, updated_at=? WHERE id=?`, string(p.Kind), p.TargetID, p.Title, p.Description, marshalJSON(p.Evidence), string(p.Status), p.CreatedBy, p.ReviewedBy, p.UpdatedAt.Format(time.RFC3339), p.ID, ) return err } // DeleteProposal removes a proposal by ID. func (db *DB) DeleteProposal(id string) error { _, err := db.conn.Exec("DELETE FROM proposals WHERE id = ?", id) return err } // ListProposals returns proposals filtered by kind and/or status. func (db *DB) ListProposals(kind ProposalKind, status ProposalStatus) ([]Proposal, error) { where := []string{} args := []any{} if kind != "" { where = append(where, "kind = ?") args = append(args, string(kind)) } if status != "" { where = append(where, "status = ?") args = append(args, string(status)) } q := `SELECT id, kind, target_id, title, description, evidence, status, created_by, reviewed_by, created_at, updated_at FROM proposals` if len(where) > 0 { q += " WHERE " + strings.Join(where, " AND ") } q += " ORDER BY created_at DESC" rows, err := db.conn.Query(q, args...) if err != nil { return nil, err } defer rows.Close() return scanProposals(rows) } // SearchProposals performs FTS search on proposals with optional filters. func (db *DB) SearchProposals(query string, kind ProposalKind, status ProposalStatus) ([]Proposal, error) { where := []string{} args := []any{} if query != "" { where = append(where, "p.id IN (SELECT id FROM proposals_fts WHERE proposals_fts MATCH ?)") args = append(args, query) } if kind != "" { where = append(where, "p.kind = ?") args = append(args, string(kind)) } if status != "" { where = append(where, "p.status = ?") args = append(args, string(status)) } q := `SELECT p.id, p.kind, p.target_id, p.title, p.description, p.evidence, p.status, p.created_by, p.reviewed_by, p.created_at, p.updated_at FROM proposals p` if len(where) > 0 { q += " WHERE " + strings.Join(where, " AND ") } q += " ORDER BY p.created_at DESC" rows, err := db.conn.Query(q, args...) if err != nil { return nil, err } defer rows.Close() return scanProposals(rows) } func scanProposals(rows interface{ Next() bool; Scan(...any) error }) ([]Proposal, error) { var result []Proposal for rows.Next() { var p Proposal var evidenceJSON, createdAt, updatedAt string err := rows.Scan( &p.ID, &p.Kind, &p.TargetID, &p.Title, &p.Description, &evidenceJSON, &p.Status, &p.CreatedBy, &p.ReviewedBy, &createdAt, &updatedAt, ) if err != nil { return nil, fmt.Errorf("scanning proposal: %w", err) } p.Evidence = unmarshalJSON(evidenceJSON) p.CreatedAt, _ = time.Parse(time.RFC3339, createdAt) p.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt) result = append(result, p) } return result, nil }