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() if f.CreatedAt.IsZero() { f.CreatedAt = now } if f.UpdatedAt.IsZero() { f.UpdatedAt = now } 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, content_hash, created_at, updated_at, props, emits, has_state, framework, variant, notes, documentation, code, source_repo, source_license, source_file, params_schema ) 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.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, f.ParamsSchema, ) return err } // InsertType inserts or replaces a type entry. func (db *DB) InsertType(t *Type) error { now := time.Now().UTC() if t.CreatedAt.IsZero() { t.CreatedAt = now } if t.UpdatedAt.IsZero() { t.UpdatedAt = now } 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, 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.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 } // SearchFunctions performs FTS search on functions with optional filters. // tags filter: each entry must be present in functions.tags JSON array (AND across tags). func (db *DB) SearchFunctions(query string, kind Kind, purity Purity, lang, domain string, tags ...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) } for _, t := range tags { if t == "" { continue } where = append(where, "EXISTS (SELECT 1 FROM json_each(f.tags) WHERE value = ?)") args = append(args, t) } 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. // tags filter: each entry must be present in types.tags JSON array (AND across tags). func (db *DB) SearchTypes(query string, lang, domain string, tags ...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) } for _, t := range tags { if t == "" { continue } where = append(where, "EXISTS (SELECT 1 FROM json_each(t.tags) WHERE value = ?)") args = append(args, t) } 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() if a.CreatedAt.IsZero() { a.CreatedAt = now } if a.UpdatedAt.IsZero() { a.UpdatedAt = now } if a.ID == "" { a.ID = GenerateID(a.Name, a.Lang, a.Domain) } var ( svcPort int svcHealth string svcHealthTO int svcUnit string svcScope string svcRestart string svcRuntime string svcLocalOnly int ) if a.Service != nil { svcPort = a.Service.Port svcHealth = a.Service.HealthEndpoint svcHealthTO = a.Service.HealthTimeoutS svcUnit = a.Service.SystemdUnit svcScope = a.Service.SystemdScope svcRestart = a.Service.RestartPolicy svcRuntime = a.Service.Runtime if a.Service.IsLocalOnly { svcLocalOnly = 1 } } if a.Version == "" { a.Version = "0.1.0" } _, 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, content_hash, created_at, updated_at, repo_url, project_id, uses_modules, service_port, service_health_endpoint, service_health_timeout_s, service_systemd_unit, service_systemd_scope, service_restart_policy, service_runtime, service_is_local_only, version ) 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.ContentHash, a.CreatedAt.Format(time.RFC3339), a.UpdatedAt.Format(time.RFC3339), a.RepoURL, a.ProjectID, marshalStrings(a.UsesModules), svcPort, svcHealth, svcHealthTO, svcUnit, svcScope, svcRestart, svcRuntime, svcLocalOnly, a.Version, ) if err != nil { return err } // Replace service_targets for this app (idempotent). if _, err := db.conn.Exec("DELETE FROM service_targets WHERE app_id = ?", a.ID); err != nil { return fmt.Errorf("clearing service_targets for %s: %w", a.ID, err) } if a.Service != nil { for _, pc := range a.Service.PCTargets { if pc == "" { continue } if _, err := db.conn.Exec( "INSERT OR REPLACE INTO service_targets (app_id, pc_id, role) VALUES (?, ?, 'primary')", a.ID, pc, ); err != nil { return fmt.Errorf("inserting service_target %s/%s: %w", a.ID, pc, err) } } } return nil } // 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, usesModJSON string var createdAt, updatedAt string var svcPort, svcHealthTO, svcLocalOnly int var svcHealth, svcUnit, svcScope, svcRestart, svcRuntime 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, &a.ContentHash, &a.RepoURL, &a.ProjectID, &usesModJSON, &svcPort, &svcHealth, &svcHealthTO, &svcUnit, &svcScope, &svcRestart, &svcRuntime, &svcLocalOnly, &a.Version, ) if err != nil { return nil, fmt.Errorf("scanning app: %w", err) } a.Tags = unmarshalStrings(tagsJSON) a.UsesFunctions = unmarshalStrings(usesFnJSON) a.UsesTypes = unmarshalStrings(usesTypJSON) a.UsesModules = unmarshalStrings(usesModJSON) a.CreatedAt, _ = time.Parse(time.RFC3339, createdAt) a.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt) if svcPort != 0 || svcHealth != "" || svcUnit != "" || svcScope != "" || svcRestart != "" || svcRuntime != "" || svcLocalOnly != 0 { a.Service = &ServiceSpec{ Port: svcPort, HealthEndpoint: svcHealth, HealthTimeoutS: svcHealthTO, SystemdUnit: svcUnit, SystemdScope: svcScope, RestartPolicy: svcRestart, Runtime: svcRuntime, IsLocalOnly: svcLocalOnly != 0, } } result = append(result, a) } return result, nil } // GetServicePCTargets returns the pc_ids declared in service_targets for an app. // Empty slice when the app has no declared targets. Issue 0105. func (db *DB) GetServicePCTargets(appID string) ([]string, error) { rows, err := db.conn.Query( "SELECT pc_id FROM service_targets WHERE app_id = ? ORDER BY pc_id", appID, ) if err != nil { return nil, err } defer rows.Close() var out []string for rows.Next() { var pc string if err := rows.Scan(&pc); err != nil { return nil, err } out = append(out, pc) } return out, nil } // Purge deletes all data from functions, types, apps, analysis, projects, vaults and modules. 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 } if _, err := db.conn.Exec("DELETE FROM apps"); err != nil { return err } if _, err := db.conn.Exec("DELETE FROM service_targets"); err != nil { return err } if _, err := db.conn.Exec("DELETE FROM analysis"); err != nil { return err } if _, err := db.conn.Exec("DELETE FROM projects"); err != nil { return err } if _, err := db.conn.Exec("DELETE FROM vaults"); err != nil { return err } _, err := db.conn.Exec("DELETE FROM modules") return err } // PurgeLocalOnly deletes functions, types, and only locally-present apps/analysis/projects/vaults. // Remote-only records (repo_url set, not in local ID maps) are preserved. func (db *DB) PurgeLocalOnly(localAppIDs, localAnalysisIDs, localProjectIDs map[string]bool) 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 } // Delete local apps (those scanned from disk) for id := range localAppIDs { if _, err := db.conn.Exec("DELETE FROM apps WHERE id = ?", id); err != nil { return err } if _, err := db.conn.Exec("DELETE FROM service_targets WHERE app_id = ?", id); err != nil { return err } } // Delete apps without repo_url (legacy local-only apps not yet pushed) if _, err := db.conn.Exec("DELETE FROM apps WHERE repo_url = '' OR repo_url IS NULL"); err != nil { return err } // Orphan service_targets cleanup if _, err := db.conn.Exec("DELETE FROM service_targets WHERE app_id NOT IN (SELECT id FROM apps)"); err != nil { return err } // Same for analysis for id := range localAnalysisIDs { if _, err := db.conn.Exec("DELETE FROM analysis WHERE id = ?", id); err != nil { return err } } if _, err := db.conn.Exec("DELETE FROM analysis WHERE repo_url = '' OR repo_url IS NULL"); err != nil { return err } // Projects: delete locally-scanned, preserve remote-only for id := range localProjectIDs { if _, err := db.conn.Exec("DELETE FROM projects WHERE id = ?", id); err != nil { return err } } if _, err := db.conn.Exec("DELETE FROM projects WHERE repo_url = '' OR repo_url IS NULL"); err != nil { return err } // Vaults: always purge and re-insert from vault.yaml if _, err := db.conn.Exec("DELETE FROM vaults"); err != nil { return err } // Modules: always purge and re-insert from modules/*/module.md if _, err := db.conn.Exec("DELETE FROM modules"); err != nil { return err } return nil } // --- Analysis CRUD --- // InsertAnalysis inserts or replaces an analysis entry. func (db *DB) InsertAnalysis(a *Analysis) error { now := time.Now().UTC() if a.CreatedAt.IsZero() { a.CreatedAt = now } if a.UpdatedAt.IsZero() { a.UpdatedAt = now } if a.ID == "" { a.ID = GenerateID(a.Name, a.Lang, a.Domain) } _, err := db.conn.Exec(` INSERT OR REPLACE INTO analysis ( id, name, lang, domain, description, tags, uses_functions, uses_types, framework, entry_point, documentation, notes, repo_url, dir_path, content_hash, created_at, updated_at, project_id, uses_modules ) 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.RepoURL, a.DirPath, a.ContentHash, a.CreatedAt.Format(time.RFC3339), a.UpdatedAt.Format(time.RFC3339), a.ProjectID, marshalStrings(a.UsesModules), ) return err } // GetAnalysis returns a single analysis by ID. func (db *DB) GetAnalysis(id string) (*Analysis, error) { rows, err := db.conn.Query("SELECT * FROM analysis WHERE id = ?", id) if err != nil { return nil, err } defer rows.Close() items, err := scanAnalysis(rows) if err != nil { return nil, err } if len(items) == 0 { return nil, fmt.Errorf("analysis %q not found", id) } return &items[0], nil } // SearchAnalysis performs FTS search on analysis with optional filters. func (db *DB) SearchAnalysis(query string, lang, domain string) ([]Analysis, error) { where := []string{} args := []any{} if query != "" { where = append(where, "a.id IN (SELECT id FROM analysis_fts WHERE analysis_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 analysis 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 analysis: %w", err) } defer rows.Close() return scanAnalysis(rows) } // ListAllAnalysis returns all analysis entries. func (db *DB) ListAllAnalysis() ([]Analysis, error) { return db.SearchAnalysis("", "", "") } // ListAllApps returns all app entries. func (db *DB) ListAllApps() ([]App, error) { return db.SearchApps("", "", "") } func scanAnalysis(rows interface{ Next() bool; Scan(...any) error }) ([]Analysis, error) { var result []Analysis for rows.Next() { var a Analysis var tagsJSON, usesFnJSON, usesTypJSON, usesModJSON 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.RepoURL, &a.DirPath, &a.ContentHash, &createdAt, &updatedAt, &a.ProjectID, &usesModJSON, ) if err != nil { return nil, fmt.Errorf("scanning analysis: %w", err) } a.Tags = unmarshalStrings(tagsJSON) a.UsesFunctions = unmarshalStrings(usesFnJSON) a.UsesTypes = unmarshalStrings(usesTypJSON) a.UsesModules = unmarshalStrings(usesModJSON) a.CreatedAt, _ = time.Parse(time.RFC3339, createdAt) a.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt) result = append(result, a) } return result, nil } // --- Project CRUD --- // InsertProject inserts or replaces a project entry. func (db *DB) InsertProject(p *Project) error { now := time.Now().UTC() if p.CreatedAt.IsZero() { p.CreatedAt = now } if p.UpdatedAt.IsZero() { p.UpdatedAt = now } _, err := db.conn.Exec(` INSERT OR REPLACE INTO projects ( id, name, description, tags, repo_url, dir_path, documentation, notes, content_hash, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, p.ID, p.Name, p.Description, marshalStrings(p.Tags), p.RepoURL, p.DirPath, p.Documentation, p.Notes, p.ContentHash, p.CreatedAt.Format(time.RFC3339), p.UpdatedAt.Format(time.RFC3339), ) return err } // GetProject returns a single project by ID. func (db *DB) GetProject(id string) (*Project, error) { rows, err := db.conn.Query("SELECT * FROM projects WHERE id = ?", id) if err != nil { return nil, err } defer rows.Close() ps, err := scanProjects(rows) if err != nil { return nil, err } if len(ps) == 0 { return nil, fmt.Errorf("project %q not found", id) } return &ps[0], nil } // SearchProjects performs FTS search on projects. func (db *DB) SearchProjects(query string) ([]Project, error) { where := []string{} args := []any{} if query != "" { where = append(where, "p.id IN (SELECT id FROM projects_fts WHERE projects_fts MATCH ?)") args = append(args, query) } sql := "SELECT * FROM projects p" if len(where) > 0 { sql += " WHERE " + strings.Join(where, " AND ") } sql += " ORDER BY p.name" rows, err := db.conn.Query(sql, args...) if err != nil { return nil, fmt.Errorf("search projects: %w", err) } defer rows.Close() return scanProjects(rows) } // ListAllProjects returns all project entries. func (db *DB) ListAllProjects() ([]Project, error) { return db.SearchProjects("") } // GetProjectApps returns all apps belonging to a project. func (db *DB) GetProjectApps(projectID string) ([]App, error) { rows, err := db.conn.Query("SELECT * FROM apps WHERE project_id = ? ORDER BY name", projectID) if err != nil { return nil, err } defer rows.Close() return scanApps(rows) } // GetProjectAnalysis returns all analysis entries belonging to a project. func (db *DB) GetProjectAnalysis(projectID string) ([]Analysis, error) { rows, err := db.conn.Query("SELECT * FROM analysis WHERE project_id = ? ORDER BY name", projectID) if err != nil { return nil, err } defer rows.Close() return scanAnalysis(rows) } func scanProjects(rows interface{ Next() bool; Scan(...any) error }) ([]Project, error) { var result []Project for rows.Next() { var p Project var tagsJSON string var createdAt, updatedAt string err := rows.Scan( &p.ID, &p.Name, &p.Description, &tagsJSON, &p.RepoURL, &p.DirPath, &p.Documentation, &p.Notes, &p.ContentHash, &createdAt, &updatedAt, ) if err != nil { return nil, fmt.Errorf("scanning project: %w", err) } p.Tags = unmarshalStrings(tagsJSON) p.CreatedAt, _ = time.Parse(time.RFC3339, createdAt) p.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt) result = append(result, p) } return result, nil } // --- Vault CRUD --- // InsertVault inserts or replaces a vault entry. func (db *DB) InsertVault(v *Vault) error { now := time.Now().UTC() if v.CreatedAt.IsZero() { v.CreatedAt = now } if v.UpdatedAt.IsZero() { v.UpdatedAt = now } sym := 0 if v.Symlink { sym = 1 } _, err := db.conn.Exec(` INSERT OR REPLACE INTO vaults ( id, name, project_id, description, path, symlink, tags, content_hash, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, v.ID, v.Name, v.ProjectID, v.Description, v.Path, sym, marshalStrings(v.Tags), v.ContentHash, v.CreatedAt.Format(time.RFC3339), v.UpdatedAt.Format(time.RFC3339), ) return err } // GetVault returns a single vault by ID. func (db *DB) GetVault(id string) (*Vault, error) { rows, err := db.conn.Query("SELECT * FROM vaults WHERE id = ?", id) if err != nil { return nil, err } defer rows.Close() vs, err := scanVaults(rows) if err != nil { return nil, err } if len(vs) == 0 { return nil, fmt.Errorf("vault %q not found", id) } return &vs[0], nil } // SearchVaults performs search on vaults with optional project filter. func (db *DB) SearchVaults(query, projectID string) ([]Vault, error) { where := []string{} args := []any{} if query != "" { where = append(where, "name LIKE ? OR description LIKE ?") q := "%" + query + "%" args = append(args, q, q) } if projectID != "" { where = append(where, "project_id = ?") args = append(args, projectID) } sql := "SELECT * FROM vaults" if len(where) > 0 { sql += " WHERE " + strings.Join(where, " AND ") } sql += " ORDER BY name" rows, err := db.conn.Query(sql, args...) if err != nil { return nil, fmt.Errorf("search vaults: %w", err) } defer rows.Close() return scanVaults(rows) } // GetProjectVaults returns all vaults belonging to a project. func (db *DB) GetProjectVaults(projectID string) ([]Vault, error) { return db.SearchVaults("", projectID) } func scanVaults(rows interface{ Next() bool; Scan(...any) error }) ([]Vault, error) { var result []Vault for rows.Next() { var v Vault var tagsJSON string var createdAt, updatedAt string var sym int err := rows.Scan( &v.ID, &v.Name, &v.ProjectID, &v.Description, &v.Path, &sym, &tagsJSON, &v.ContentHash, &createdAt, &updatedAt, ) if err != nil { return nil, fmt.Errorf("scanning vault: %w", err) } v.Symlink = sym == 1 v.Tags = unmarshalStrings(tagsJSON) v.CreatedAt, _ = time.Parse(time.RFC3339, createdAt) v.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt) result = append(result, v) } return result, nil } 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, &f.ContentHash, &f.SourceRepo, &f.SourceLicense, &f.SourceFile, &f.ParamsSchema, ) 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, &t.ContentHash, &t.SourceRepo, &t.SourceLicense, &t.SourceFile, ) 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 } // --- Unit Tests CRUD --- // InsertUnitTest inserts or replaces a unit test entry. func (db *DB) InsertUnitTest(ut *UnitTest) error { now := time.Now().UTC() if ut.CreatedAt.IsZero() { ut.CreatedAt = now } if ut.UpdatedAt.IsZero() { ut.UpdatedAt = now } _, err := db.conn.Exec(` INSERT OR REPLACE INTO unit_tests ( id, function_id, name, code, file_path, lang, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, ut.ID, ut.FunctionID, ut.Name, ut.Code, ut.FilePath, ut.Lang, ut.CreatedAt.Format(time.RFC3339), ut.UpdatedAt.Format(time.RFC3339), ) return err } // GetUnitTestsByFunction returns all unit tests for a given function ID. func (db *DB) GetUnitTestsByFunction(functionID string) ([]UnitTest, error) { rows, err := db.conn.Query( "SELECT id, function_id, name, code, file_path, lang, created_at, updated_at FROM unit_tests WHERE function_id = ? ORDER BY name", functionID, ) if err != nil { return nil, err } defer rows.Close() return scanUnitTests(rows) } // SearchUnitTests performs FTS search on unit tests. func (db *DB) SearchUnitTests(query string, lang string) ([]UnitTest, error) { where := []string{} args := []any{} if query != "" { where = append(where, "ut.id IN (SELECT id FROM unit_tests_fts WHERE unit_tests_fts MATCH ?)") args = append(args, query) } if lang != "" { where = append(where, "ut.lang = ?") args = append(args, lang) } sql := "SELECT id, function_id, name, code, file_path, lang, created_at, updated_at FROM unit_tests ut" if len(where) > 0 { sql += " WHERE " + strings.Join(where, " AND ") } sql += " ORDER BY ut.function_id, ut.name" rows, err := db.conn.Query(sql, args...) if err != nil { return nil, fmt.Errorf("search unit tests: %w", err) } defer rows.Close() return scanUnitTests(rows) } func scanUnitTests(rows interface{ Next() bool; Scan(...any) error }) ([]UnitTest, error) { var result []UnitTest for rows.Next() { var ut UnitTest var createdAt, updatedAt string err := rows.Scan(&ut.ID, &ut.FunctionID, &ut.Name, &ut.Code, &ut.FilePath, &ut.Lang, &createdAt, &updatedAt) if err != nil { return nil, fmt.Errorf("scanning unit test: %w", err) } ut.CreatedAt, _ = time.Parse(time.RFC3339, createdAt) ut.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt) result = append(result, ut) } return result, nil } // PurgeUnitTests deletes all unit test entries. Used before re-indexing. func (db *DB) PurgeUnitTests() error { _, err := db.conn.Exec("DELETE FROM unit_tests") return err } // --- 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 } // --- PcLocation CRUD --- // InsertPcLocation inserts or replaces a pc_location entry. func (db *DB) InsertPcLocation(loc *PcLocation) error { now := time.Now().UTC() if loc.CreatedAt.IsZero() { loc.CreatedAt = now } if loc.UpdatedAt.IsZero() { loc.UpdatedAt = now } if loc.ID == "" { loc.ID = loc.EntityType + "_" + loc.EntityID + "_" + loc.PcID } if loc.Status == "" { loc.Status = "active" } _, err := db.conn.Exec(` INSERT OR REPLACE INTO pc_locations ( id, entity_type, entity_id, pc_id, dir_path, status, notes, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, loc.ID, loc.EntityType, loc.EntityID, loc.PcID, loc.DirPath, loc.Status, loc.Notes, loc.CreatedAt.Format(time.RFC3339), loc.UpdatedAt.Format(time.RFC3339), ) return err } // GetPcLocationsByPC returns all locations for a given PC. func (db *DB) GetPcLocationsByPC(pcID string) ([]PcLocation, error) { rows, err := db.conn.Query( "SELECT id, entity_type, entity_id, pc_id, dir_path, status, notes, created_at, updated_at FROM pc_locations WHERE pc_id = ? ORDER BY entity_type, entity_id", pcID, ) if err != nil { return nil, err } defer rows.Close() return scanPcLocations(rows) } // GetPcLocationsByEntity returns all PC locations for a given entity. func (db *DB) GetPcLocationsByEntity(entityType, entityID string) ([]PcLocation, error) { rows, err := db.conn.Query( "SELECT id, entity_type, entity_id, pc_id, dir_path, status, notes, created_at, updated_at FROM pc_locations WHERE entity_type = ? AND entity_id = ? ORDER BY pc_id", entityType, entityID, ) if err != nil { return nil, err } defer rows.Close() return scanPcLocations(rows) } // ListAllPcLocations returns all pc_location entries. func (db *DB) ListAllPcLocations() ([]PcLocation, error) { rows, err := db.conn.Query( "SELECT id, entity_type, entity_id, pc_id, dir_path, status, notes, created_at, updated_at FROM pc_locations ORDER BY pc_id, entity_type, entity_id", ) if err != nil { return nil, err } defer rows.Close() return scanPcLocations(rows) } // DeletePcLocationsByPC removes all locations for a given PC. func (db *DB) DeletePcLocationsByPC(pcID string) error { _, err := db.conn.Exec("DELETE FROM pc_locations WHERE pc_id = ?", pcID) return err } func scanPcLocations(rows interface{ Next() bool; Scan(...any) error }) ([]PcLocation, error) { var result []PcLocation for rows.Next() { var loc PcLocation var createdAt, updatedAt string err := rows.Scan( &loc.ID, &loc.EntityType, &loc.EntityID, &loc.PcID, &loc.DirPath, &loc.Status, &loc.Notes, &createdAt, &updatedAt, ) if err != nil { return nil, fmt.Errorf("scanning pc_location: %w", err) } loc.CreatedAt, _ = time.Parse(time.RFC3339, createdAt) loc.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt) result = append(result, loc) } return result, nil } // --- Sync helpers --- // AllApps returns all apps (for sync export). func (db *DB) AllApps() ([]App, error) { return db.SearchApps("", "", "") } // AllAnalysis returns all analysis entries (for sync export). func (db *DB) AllAnalysis() ([]Analysis, error) { return db.SearchAnalysis("", "", "") } // AllProposals returns all proposals (for sync export). func (db *DB) AllProposals() ([]Proposal, error) { return db.ListProposals("", "") } // AllVaults returns all vaults (for sync export). func (db *DB) AllVaults() ([]Vault, error) { return db.SearchVaults("", "") } // --- Module CRUD --- // InsertModule inserts or replaces a module entry. func (db *DB) InsertModule(m *Module) error { now := time.Now().UTC() if m.CreatedAt.IsZero() { m.CreatedAt = now } if m.UpdatedAt.IsZero() { m.UpdatedAt = now } if m.ID == "" { m.ID = GenerateModuleID(m.Name, m.Lang) } _, err := db.conn.Exec(` INSERT OR REPLACE INTO modules ( id, name, version, lang, description, members, tags, dir_path, repo_url, documentation, notes, content_hash, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, m.ID, m.Name, m.Version, m.Lang, m.Description, marshalStrings(m.Members), marshalStrings(m.Tags), m.DirPath, m.RepoURL, m.Documentation, m.Notes, m.ContentHash, m.CreatedAt.Format(time.RFC3339), m.UpdatedAt.Format(time.RFC3339), ) return err } // GetModule returns a single module by ID. func (db *DB) GetModule(id string) (*Module, error) { rows, err := db.conn.Query("SELECT * FROM modules WHERE id = ?", id) if err != nil { return nil, err } defer rows.Close() items, err := scanModules(rows) if err != nil { return nil, err } if len(items) == 0 { return nil, fmt.Errorf("module %q not found", id) } return &items[0], nil } // SearchModules performs FTS search on modules with optional filters. func (db *DB) SearchModules(query, lang string) ([]Module, error) { where := []string{} args := []any{} if query != "" { where = append(where, "m.id IN (SELECT id FROM modules_fts WHERE modules_fts MATCH ?)") args = append(args, query) } if lang != "" { where = append(where, "m.lang = ?") args = append(args, lang) } sql := "SELECT * FROM modules m" if len(where) > 0 { sql += " WHERE " + strings.Join(where, " AND ") } sql += " ORDER BY m.name" rows, err := db.conn.Query(sql, args...) if err != nil { return nil, fmt.Errorf("search modules: %w", err) } defer rows.Close() return scanModules(rows) } // ListAllModules returns all module entries. func (db *DB) ListAllModules() ([]Module, error) { return db.SearchModules("", "") } // AllModules returns all modules (for sync export). func (db *DB) AllModules() ([]Module, error) { return db.SearchModules("", "") } func scanModules(rows interface{ Next() bool; Scan(...any) error }) ([]Module, error) { var result []Module for rows.Next() { var m Module var membersJSON, tagsJSON string var createdAt, updatedAt string err := rows.Scan( &m.ID, &m.Name, &m.Version, &m.Lang, &m.Description, &membersJSON, &tagsJSON, &m.DirPath, &m.RepoURL, &m.Documentation, &m.Notes, &m.ContentHash, &createdAt, &updatedAt, ) if err != nil { return nil, fmt.Errorf("scanning module: %w", err) } m.Members = unmarshalStrings(membersJSON) m.Tags = unmarshalStrings(tagsJSON) m.CreatedAt, _ = time.Parse(time.RFC3339, createdAt) m.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt) result = append(result, m) } return result, nil }