package infra import ( "database/sql" "encoding/json" "fmt" "path/filepath" "strings" "time" _ "github.com/mattn/go-sqlite3" ) // CapabilityEntry represents one function/pipeline line in the emitted markdown. type CapabilityEntry struct { ID string Signature string Description string UpdatedAt string // RFC3339 or date only, used for Fresh section } // CapabilitiesMdResult holds the three sections for the emitted markdown block. type CapabilitiesMdResult struct { Top20 []CapabilityEntry // top 20 by calls_total (or fallback updated_at) Fresh7d []CapabilityEntry // updated in last 7 days TopPipelines []CapabilityEntry // top 5 pipelines by calls_total (or updated_at) TelemetryAvail bool // false = call_monitor.db not found or empty GeneratedAt string // RFC3339 timestamp } const capMdMaxDescLen = 80 func truncateDesc(s string) string { // strip newlines so description stays on one line s = strings.ReplaceAll(s, "\n", " ") s = strings.ReplaceAll(s, "\r", " ") if len(s) > capMdMaxDescLen { return s[:capMdMaxDescLen-3] + "..." } return s } // EmitCapabilitiesMd generates the markdown block for embedding in CLAUDE.md. // It queries: // 1. registry.db (root) for function metadata (signature, description, updated_at). // 2. call_monitor/operations.db (projects/fn_monitoring/apps/call_monitor/) for // function_stats.calls_total. Falls back gracefully when unavailable. // // Exclusions: // - kind = "component" (frontend components, handled separately) // - lang = "cpp" AND file_path LIKE 'cpp/apps/%' (auto-touched C++ app files) // - tags containing "pendiente-usar" (orphaned/deprecated) func EmitCapabilitiesMd(registryRoot string) (CapabilitiesMdResult, error) { result := CapabilitiesMdResult{ GeneratedAt: time.Now().UTC().Format("2006-01-02T15:04"), } // --- 1. Open registry.db --- regDB, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?mode=ro&_foreign_keys=on", filepath.Join(registryRoot, "registry.db"))) if err != nil { return result, fmt.Errorf("emit_capabilities_md: open registry.db: %w", err) } defer regDB.Close() if err := regDB.Ping(); err != nil { return result, fmt.Errorf("emit_capabilities_md: ping registry.db: %w", err) } // --- 2. Try opening call_monitor/operations.db --- monitorDBPath := filepath.Join(registryRoot, "projects", "fn_monitoring", "apps", "call_monitor", "operations.db") monitorDB, monErr := sql.Open("sqlite3", fmt.Sprintf("file:%s?mode=ro", monitorDBPath)) if monErr == nil { if pingErr := monitorDB.Ping(); pingErr != nil { monitorDB.Close() monitorDB = nil } else { result.TelemetryAvail = true } } if monitorDB != nil { defer monitorDB.Close() } // --- 3. Load call counts from function_stats if available --- // map[function_id]calls_total callCounts := map[string]int64{} if result.TelemetryAvail { rows, err := monitorDB.Query( `SELECT function_id, calls_total FROM function_stats WHERE calls_total > 0`) if err == nil { defer rows.Close() for rows.Next() { var fid string var ct int64 if rows.Scan(&fid, &ct) == nil { callCounts[fid] = ct } } } } // --- 4. Load eligible functions from registry.db --- // Eligible: kind IN ('function','pipeline'), not component, not cpp/apps/, not pendiente-usar type fnRow struct { id string kind string signature string description string updatedAt string tags string // JSON array } rows, err := regDB.Query(` SELECT id, kind, signature, description, updated_at, tags FROM functions WHERE kind IN ('function', 'pipeline') AND NOT (lang = 'cpp' AND file_path LIKE 'cpp/apps/%') ORDER BY updated_at DESC `) if err != nil { return result, fmt.Errorf("emit_capabilities_md: query functions: %w", err) } defer rows.Close() var allFns []fnRow for rows.Next() { var r fnRow if err := rows.Scan(&r.id, &r.kind, &r.signature, &r.description, &r.updatedAt, &r.tags); err != nil { continue } // Exclude pendiente-usar var tagList []string _ = json.Unmarshal([]byte(r.tags), &tagList) pending := false for _, t := range tagList { if strings.TrimSpace(t) == "pendiente-usar" { pending = true break } } if pending { continue } allFns = append(allFns, r) } if err := rows.Err(); err != nil { return result, fmt.Errorf("emit_capabilities_md: rows: %w", err) } // --- 5. Build Top20 (functions only, kind=function) by calls_total desc --- // When telemetry unavailable, order by updated_at desc (already sorted that way). type scored struct { fnRow score int64 // calls_total if telemetry, else 0 (order preserved) } var fnOnly []scored var pipelines []scored for _, r := range allFns { s := scored{fnRow: r, score: callCounts[r.id]} if r.kind == "pipeline" { pipelines = append(pipelines, s) } else { fnOnly = append(fnOnly, s) } } // Sort functions by score desc (stable: updated_at desc is the tiebreaker from query order) if result.TelemetryAvail { stableSort(fnOnly, func(a, b scored) bool { return a.score > b.score }) stableSort(pipelines, func(a, b scored) bool { return a.score > b.score }) } // else: already in updated_at DESC from SQL // Top 20 functions top := min20(fnOnly) for _, s := range top { result.Top20 = append(result.Top20, CapabilityEntry{ ID: s.id, Signature: s.signature, Description: truncateDesc(s.description), UpdatedAt: dateOnly(s.updatedAt), }) } // --- 6. Fresh 7d (functions + pipelines updated in last 7 days) --- cutoff := time.Now().UTC().Add(-7 * 24 * time.Hour) seen := map[string]bool{} // Mark top20 IDs so we can still include them in fresh if they were touched for _, e := range result.Top20 { seen[e.ID] = false // false = not yet in fresh } for _, r := range allFns { t, err := parseTime(r.updatedAt) if err != nil || t.Before(cutoff) { continue } // include any kind (function or pipeline) touched in last 7d result.Fresh7d = append(result.Fresh7d, CapabilityEntry{ ID: r.id, Signature: r.signature, Description: truncateDesc(r.description), UpdatedAt: dateOnly(r.updatedAt), }) } // --- 7. Top 5 pipelines --- top5 := min5(pipelines) for _, s := range top5 { result.TopPipelines = append(result.TopPipelines, CapabilityEntry{ ID: s.id, Signature: s.signature, Description: truncateDesc(s.description), UpdatedAt: dateOnly(s.updatedAt), }) } return result, nil } // RenderCapabilitiesMd formats CapabilitiesMdResult as a markdown string. func RenderCapabilitiesMd(r CapabilitiesMdResult) string { var sb strings.Builder sb.WriteString("\n") sb.WriteString(fmt.Sprintf("\n\n", r.GeneratedAt)) // Section 1: Top 20 if !r.TelemetryAvail { sb.WriteString("## Capabilities — Top 20 (by updated_at, telemetry unavailable)\n\n") } else { sb.WriteString("## Capabilities — Top 20 (by calls_total)\n\n") } if len(r.Top20) == 0 { sb.WriteString("_(no functions found)_\n") } for _, e := range r.Top20 { sig := e.Signature if sig == "" { sig = e.ID } sb.WriteString(fmt.Sprintf("- `%s` — `%s` — %s\n", e.ID, sig, e.Description)) } // Section 2: Fresh 7d sb.WriteString("\n## Capabilities — Fresh (last 7d)\n\n") if len(r.Fresh7d) == 0 { sb.WriteString("_(no functions updated in the last 7 days)_\n") } for _, e := range r.Fresh7d { sb.WriteString(fmt.Sprintf("- `%s` (%s) — %s\n", e.ID, e.UpdatedAt, e.Description)) } // Section 3: Top 5 pipelines sb.WriteString("\n## Pipelines — Top 5 (one-shot composites)\n\n") if len(r.TopPipelines) == 0 { sb.WriteString("_(no pipelines found)_\n") } for _, e := range r.TopPipelines { sig := e.Signature if sig == "" { sig = e.ID } sb.WriteString(fmt.Sprintf("- `%s` — `%s` — %s\n", e.ID, sig, e.Description)) } return sb.String() } // --- helpers --- func stableSort[T any](slice []T, less func(a, b T) bool) { // insertion sort — small slices (<500), stable for i := 1; i < len(slice); i++ { for j := i; j > 0 && less(slice[j], slice[j-1]); j-- { slice[j], slice[j-1] = slice[j-1], slice[j] } } } func min20[T any](s []T) []T { if len(s) <= 20 { return s } return s[:20] } func min5[T any](s []T) []T { if len(s) <= 5 { return s } return s[:5] } func dateOnly(ts string) string { // "2026-05-14T01:30:00Z" -> "2026-05-14" if len(ts) >= 10 { return ts[:10] } return ts } func parseTime(ts string) (time.Time, error) { formats := []string{ time.RFC3339, "2006-01-02T15:04:05Z", "2006-01-02T15:04:05", "2006-01-02", } for _, f := range formats { if t, err := time.Parse(f, ts); err == nil { return t, nil } } return time.Time{}, fmt.Errorf("cannot parse time: %q", ts) }