chore: sync from fn-registry agent

This commit is contained in:
fn-registry agent
2026-05-14 00:28:13 +02:00
commit 5d1dabb1eb
15 changed files with 935 additions and 0 deletions
+123
View File
@@ -0,0 +1,123 @@
---
name: call_monitor
lang: go
domain: infra
description: "Telemetria de invocaciones del agente al fn_registry. Persiste eventos (calls, code_writes, test_runs, e2e_runs_fn, violations, patterns, sessions) en su propia operations.db. Vista agregada function_stats por function_id alimenta el bucle reactivo (proposals automaticas). Issue 0085."
tags: [service, telemetry, monitoring, registry, sqlite]
uses_functions:
- sqlite_open_go_infra
- sqlite_apply_migrations_go_infra
- audit_copied_code_go_infra
- generate_proposals_from_telemetry_go_infra
uses_types: []
framework: "stdlib"
entry_point: "main.go"
dir_path: "projects/fn_monitoring/apps/call_monitor"
repo_url: ""
e2e_checks:
- id: build
cmd: "CGO_ENABLED=1 go build -tags fts5 -o call_monitor ."
timeout_s: 60
- id: init
cmd: "./call_monitor init --db /tmp/call_monitor_e2e.db"
expect_stdout_contains: "ready"
- id: status_empty
cmd: "./call_monitor status --db /tmp/call_monitor_e2e.db"
expect_stdout_contains: "no calls recorded yet"
- id: schema_view
cmd: "sqlite3 /tmp/call_monitor_e2e.db 'SELECT COUNT(*) FROM function_stats;'"
expect_stdout_contains: "0"
---
## Arquitectura
App Go bajo modulo `fn-registry` (sin `go.mod` propio). Estructura:
```
projects/fn_monitoring/apps/call_monitor/
app.md
main.go # CLI: init, status
db.go # openDB + apply migrations + queries
migrations/
001_init.sql # 7 tablas event-log
002_function_stats_view.sql # vista agregada por function_id
operations.db # creada al primer init (gitignored)
```
Reusa `infra.SQLiteOpen` + `infra.ApplyMigrations` del registry (mismo patron que `apps/kanban/backend/db.go`).
## Tablas event-log (append-only)
| Tabla | Captura |
|---|---|
| `sessions` | Sesion Claude Code: session_id, cwd, started_at, ended_at, health_score, mcp_ratio |
| `calls` | Cada invocacion al registry (heredoc/mcp/fn_run): function_id, tool_used, duration_ms, success, error_class, args_hash |
| `code_writes` | Edit/Write sobre archivo del registry: function_id, file_path, lines_added/removed |
| `test_runs` | Unit tests: function_id, test_id, passed, duration_ms, output_snippet |
| `e2e_runs_fn` | E2E checks de apps que dependen: function_id, app_id, check_id, passed |
| `violations` | Antipatrones (sqlite3 inline, import *, heredoc reinvento): rule_id, function_id, severity |
| `patterns` | Heredocs clusterizados por similitud: pattern_hash, occurrences, session_ids[] |
| `function_versions` | Historial de versiones por function_id. source = `index` (poblado por `call_monitor snapshot` tras `fn index`), `edit_hook` (poblado por hook PostToolUse), `copy_detected` (futura fase 0085k) |
Datos sensibles: solo `args_hash`, NUNCA argumentos concretos.
## Vista `function_stats`
Rollup por `function_id` con:
- **Uso**: calls_total, calls_24h/7d/30d/90d, last_used_at
- **Errores**: errors_total, error_rate, last_error_ts
- **Performance**: mean_duration_ms (p95 pendiente — requires window functions o sub-query)
- **Codigo**: writes_count, last_write_at
- **Tests**: tests_total, tests_failed, test_fail_rate, last_test_failed_at
- **E2E**: e2e_total, e2e_failed, e2e_fail_rate, consumer_apps_count
- **Salud**: violations_caused
Vista O(N) sobre tablas event-log. Si performance degrada en >100k filas, materializar como TABLE refrescada por cron.
## Uso
```bash
# Build
cd projects/fn_monitoring/apps/call_monitor
CGO_ENABLED=1 go build -tags fts5 -o call_monitor .
# Crear/abrir BD (aplica migraciones idempotentemente)
./call_monitor init
# Resumen actual
./call_monitor status --top 20
# Snapshot versions desde registry.db (idempotente, ejecutar tras cada fn index)
./call_monitor snapshot
./call_monitor snapshot --registry /home/lucas/fn_registry/registry.db
# BD personalizada
./call_monitor init --db /tmp/test.db
./call_monitor status --db /tmp/test.db
```
## Integracion con el resto
| Componente | Como interactua |
|---|---|
| Hook `PostToolUse` (0085b) | Parsea cada Bash + cada mcp__registry__* y hace `INSERT INTO calls/...` directo sobre operations.db |
| Wrapper Python `registry_telemetry` (0085c) | Patcha imports al activar `FN_TELEMETRY=1`, registra calls del heredoc |
| `registry_dashboard` (UI) | Lee via `sqlite_api`: nuevo datasource `ops:call_monitor`. Tab "Claude usage" con top funciones, huerfanas, patrones |
| `fn-mejorador` (fase 5 bucle reactivo) | Consulta `function_stats` + `patterns` + `violations` para generar proposals con evidencia trazable |
| `fn-orquestador` (issue 0069) | Usa `sessions.health_score` como criterio de exito adicional |
## Roadmap
- 0085a (este paso): schema + skeleton ✓
- 0085b: hook PostToolUse Bash que insert en `calls`/`code_writes`/`violations`
- 0085c: wrapper Python con `FN_TELEMETRY=1`
- 0085d: anadir datasource a `sqlite_api` + tabs en `registry_dashboard`
- 0085e..h: clusterizacion, proposals automaticas, gating
- p95 en mean_duration_ms via percentile calc o ext.
## Notas
- BD vive **junto al binario** (`<exe_dir>/operations.db`) por defecto, no en el cwd del agente. Hook puede pasar `--db` explicito si conviene.
- `operations.db` gitignored — telemetria es local por PC, no se sincroniza.
- Sin `repo_url`: aun no se ha hecho `gitea_create_repo`. Se inicializara con `/full-git-push` cuando este la fase 0085b lista para evitar repos vacios.
Executable
BIN
View File
Binary file not shown.
+45
View File
@@ -0,0 +1,45 @@
package main
import (
"fmt"
"time"
"fn-registry/functions/infra"
)
// persistCopiedCode runs infra.AuditCopiedCode and INSERT OR IGNOREs results
// into the copied_code table. Returns (newly_inserted, total_detected, error).
func persistCopiedCode(callDB *DB, registryRoot string) (int, int, error) {
entries, err := infra.AuditCopiedCode(registryRoot)
if err != nil {
return 0, 0, fmt.Errorf("audit: %w", err)
}
now := time.Now().UTC().Unix()
tx, err := callDB.conn.Begin()
if err != nil {
return 0, 0, err
}
stmt, err := tx.Prepare(`INSERT OR IGNORE INTO copied_code
(app_file, app_function, registry_id, body_hash, similarity, kind, detected_at)
VALUES (?, ?, ?, ?, ?, ?, ?)`)
if err != nil {
_ = tx.Rollback()
return 0, 0, err
}
defer stmt.Close()
inserted := 0
for _, e := range entries {
res, err := stmt.Exec(e.AppFile, e.AppFunction, e.RegistryID, e.BodyHash, e.Similarity, e.Kind, now)
if err != nil {
_ = tx.Rollback()
return 0, 0, err
}
if n, _ := res.RowsAffected(); n > 0 {
inserted++
}
}
if err := tx.Commit(); err != nil {
return 0, 0, err
}
return inserted, len(entries), nil
}
+77
View File
@@ -0,0 +1,77 @@
package main
import (
"database/sql"
"embed"
"fmt"
"fn-registry/functions/infra"
)
//go:embed migrations/*.sql
var migrationsFS embed.FS
type DB struct{ conn *sql.DB }
func openDB(path string) (*DB, error) {
conn, err := infra.SQLiteOpen(path, "")
if err != nil {
return nil, err
}
if err := infra.ApplyMigrations(conn, migrationsFS, "migrations/*.sql"); err != nil {
conn.Close()
return nil, fmt.Errorf("migrate: %w", err)
}
return &DB{conn: conn}, nil
}
func (d *DB) Close() error { return d.conn.Close() }
type tableCount struct {
Name string
Rows int64
}
func (d *DB) tableCounts() ([]tableCount, error) {
tables := []string{"sessions", "calls", "code_writes", "test_runs", "e2e_runs_fn", "violations", "patterns", "function_versions", "copied_code"}
out := make([]tableCount, 0, len(tables))
for _, t := range tables {
var n int64
if err := d.conn.QueryRow("SELECT COUNT(*) FROM " + t).Scan(&n); err != nil {
return nil, fmt.Errorf("count %s: %w", t, err)
}
out = append(out, tableCount{Name: t, Rows: n})
}
return out, nil
}
type funcStat struct {
FunctionID string
CallsTotal int64
Calls7d int64
ErrorsTotal int64
ErrorRate float64
MeanDurationMs float64
LastUsedAt sql.NullInt64
}
func (d *DB) topFunctions(limit int) ([]funcStat, error) {
rows, err := d.conn.Query(`
SELECT function_id, calls_total, calls_7d, errors_total, error_rate, mean_duration_ms, last_used_at
FROM function_stats
ORDER BY calls_total DESC
LIMIT ?`, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var out []funcStat
for rows.Next() {
var s funcStat
if err := rows.Scan(&s.FunctionID, &s.CallsTotal, &s.Calls7d, &s.ErrorsTotal, &s.ErrorRate, &s.MeanDurationMs, &s.LastUsedAt); err != nil {
return nil, err
}
out = append(out, s)
}
return out, rows.Err()
}
+235
View File
@@ -0,0 +1,235 @@
// call_monitor: telemetria de invocaciones del agente al fn_registry.
// Issue 0085. Persiste eventos en operations.db local. Hook PostToolUse (0085b)
// y wrapper Python (0085c) escriben aqui. registry_dashboard lee via sqlite_api.
package main
import (
"flag"
"fmt"
"os"
"path/filepath"
"text/tabwriter"
)
const defaultDBName = "operations.db"
func main() {
if len(os.Args) < 2 {
usage()
os.Exit(2)
}
sub := os.Args[1]
fs := flag.NewFlagSet(sub, flag.ExitOnError)
dbPath := fs.String("db", "", "Path to operations.db (default: ./operations.db relative to this binary's directory).")
switch sub {
case "init":
fs.Parse(os.Args[2:])
runInit(resolveDB(*dbPath))
case "status":
topN := fs.Int("top", 10, "Top N functions to list.")
fs.Parse(os.Args[2:])
runStatus(resolveDB(*dbPath), *topN)
case "snapshot":
registry := fs.String("registry", "", "Path to registry.db (default: walk up from cwd until found).")
fs.Parse(os.Args[2:])
runSnapshot(resolveDB(*dbPath), *registry)
case "copied-code":
root := fs.String("root", "", "Path to fn_registry root (default: walk up from cwd until registry.db found).")
fs.Parse(os.Args[2:])
runCopiedCode(resolveDB(*dbPath), *root)
case "propose":
root := fs.String("root", "", "Path to fn_registry root (default: walk up from cwd).")
dry := fs.Bool("dry-run", false, "Generate drafts without persisting to registry.db.proposals.")
fs.Parse(os.Args[2:])
runPropose(*root, *dry)
case "-h", "--help", "help":
usage()
default:
fmt.Fprintf(os.Stderr, "unknown subcommand: %s\n\n", sub)
usage()
os.Exit(2)
}
}
func usage() {
fmt.Fprintln(os.Stderr, `call_monitor — telemetria de invocaciones del agente al fn_registry
USO:
call_monitor <subcomando> [flags]
SUBCOMANDOS:
init Crea/abre operations.db y aplica migraciones (idempotente).
status Resumen: conteo de filas por tabla + top funciones por calls_total.
snapshot Lee registry.db.functions y snapshotea (function_id, content_hash) en
function_versions con source='index'. Idempotente: solo inserta nuevas tuplas.
FLAGS GLOBALES:
--db PATH Ruta a operations.db (default: ./operations.db junto al binario).
--registry PATH (subcomando snapshot) Ruta a registry.db. Default: walk up.
EJEMPLOS:
call_monitor init
call_monitor status --top 20
call_monitor snapshot
call_monitor snapshot --registry /home/lucas/fn_registry/registry.db`)
}
func resolveRegistryDB(override string) string {
if override != "" {
return override
}
exe, err := os.Executable()
if err != nil {
return "registry.db"
}
dir := filepath.Dir(exe)
for {
candidate := filepath.Join(dir, "registry.db")
if _, err := os.Stat(candidate); err == nil {
return candidate
}
parent := filepath.Dir(dir)
if parent == dir {
break
}
dir = parent
}
return "registry.db"
}
func resolveRegistryRoot(override string) string {
if override != "" {
return override
}
exe, err := os.Executable()
if err != nil {
return "."
}
dir := filepath.Dir(exe)
for {
if _, err := os.Stat(filepath.Join(dir, "registry.db")); err == nil {
return dir
}
parent := filepath.Dir(dir)
if parent == dir {
break
}
dir = parent
}
return "."
}
func runCopiedCode(callDBPath, rootOverride string) {
db, err := openDB(callDBPath)
if err != nil {
fmt.Fprintf(os.Stderr, "error: open call_monitor db: %v\n", err)
os.Exit(1)
}
defer db.Close()
root := resolveRegistryRoot(rootOverride)
if _, err := os.Stat(filepath.Join(root, "registry.db")); err != nil {
fmt.Fprintf(os.Stderr, "error: registry.db not found under %s\n", root)
os.Exit(1)
}
inserted, total, err := persistCopiedCode(db, root)
if err != nil {
fmt.Fprintf(os.Stderr, "error: copied-code: %v\n", err)
os.Exit(1)
}
fmt.Printf("copied-code: %d total match(es) detected, %d newly inserted into copied_code.\n", total, inserted)
}
func runSnapshot(callDBPath, registryOverride string) {
db, err := openDB(callDBPath)
if err != nil {
fmt.Fprintf(os.Stderr, "error: open call_monitor db: %v\n", err)
os.Exit(1)
}
defer db.Close()
registryPath := resolveRegistryDB(registryOverride)
if _, err := os.Stat(registryPath); err != nil {
fmt.Fprintf(os.Stderr, "error: registry.db not found at %s\n", registryPath)
os.Exit(1)
}
inserted, seen, err := snapshotFromRegistry(db, registryPath)
if err != nil {
fmt.Fprintf(os.Stderr, "error: snapshot: %v\n", err)
os.Exit(1)
}
fmt.Printf("snapshot: %d new versions inserted (out of %d functions seen with content_hash)\n", inserted, seen)
}
func resolveDB(override string) string {
if override != "" {
return override
}
exe, err := os.Executable()
if err == nil {
return filepath.Join(filepath.Dir(exe), defaultDBName)
}
return defaultDBName
}
func runInit(path string) {
db, err := openDB(path)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
defer db.Close()
abs, _ := filepath.Abs(path)
fmt.Printf("call_monitor.operations.db ready: %s\n", abs)
}
func runStatus(path string, topN int) {
db, err := openDB(path)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
defer db.Close()
counts, err := db.tableCounts()
if err != nil {
fmt.Fprintf(os.Stderr, "table counts: %v\n", err)
os.Exit(1)
}
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(tw, "=== Tables ===")
fmt.Fprintln(tw, "TABLE\tROWS")
for _, c := range counts {
fmt.Fprintf(tw, "%s\t%d\n", c.Name, c.Rows)
}
tw.Flush()
top, err := db.topFunctions(topN)
if err != nil {
fmt.Fprintf(os.Stderr, "top functions: %v\n", err)
os.Exit(1)
}
fmt.Println()
tw = tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(tw, "=== Top %d functions (by calls_total) ===\n", topN)
if len(top) == 0 {
fmt.Fprintln(tw, "(no calls recorded yet)")
tw.Flush()
return
}
fmt.Fprintln(tw, "FUNCTION_ID\tCALLS\tCALLS_7D\tERRORS\tERROR_RATE\tMEAN_MS\tLAST_USED_TS")
for _, s := range top {
last := ""
if s.LastUsedAt.Valid {
last = fmt.Sprintf("%d", s.LastUsedAt.Int64)
}
fmt.Fprintf(tw, "%s\t%d\t%d\t%d\t%.2f\t%.0f\t%s\n",
s.FunctionID, s.CallsTotal, s.Calls7d, s.ErrorsTotal, s.ErrorRate, s.MeanDurationMs, last)
}
tw.Flush()
}
+113
View File
@@ -0,0 +1,113 @@
-- call_monitor schema v1.0.0
-- Event-log de invocaciones del agente al registry + telemetria asociada.
-- Issue 0085. Aditivo. Aplicado via embed.FS al abrir operations.db.
PRAGMA journal_mode=WAL;
PRAGMA foreign_keys=ON;
-- Sesiones Claude Code. Una por arranque de claude-code.
CREATE TABLE IF NOT EXISTS sessions (
session_id TEXT PRIMARY KEY,
cwd TEXT NOT NULL DEFAULT '',
started_at INTEGER NOT NULL,
ended_at INTEGER,
health_score REAL,
mcp_ratio REAL,
notes TEXT NOT NULL DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at);
-- Cada invocacion del agente sobre funcion del registry (heredoc/mcp/fn_run).
CREATE TABLE IF NOT EXISTS calls (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL DEFAULT '',
function_id TEXT NOT NULL DEFAULT '',
tool_used TEXT NOT NULL,
args_hash TEXT NOT NULL DEFAULT '',
duration_ms INTEGER NOT NULL DEFAULT 0,
success INTEGER NOT NULL DEFAULT 1,
error_class TEXT NOT NULL DEFAULT '',
error_snippet TEXT NOT NULL DEFAULT '',
ts INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_calls_function ON calls(function_id);
CREATE INDEX IF NOT EXISTS idx_calls_session ON calls(session_id);
CREATE INDEX IF NOT EXISTS idx_calls_ts ON calls(ts);
CREATE INDEX IF NOT EXISTS idx_calls_tool ON calls(tool_used);
CREATE INDEX IF NOT EXISTS idx_calls_success ON calls(success);
-- Edit/Write del agente sobre archivos del registry.
CREATE TABLE IF NOT EXISTS code_writes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL DEFAULT '',
function_id TEXT NOT NULL DEFAULT '',
file_path TEXT NOT NULL,
lines_added INTEGER NOT NULL DEFAULT 0,
lines_removed INTEGER NOT NULL DEFAULT 0,
ts INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_code_writes_function ON code_writes(function_id);
CREATE INDEX IF NOT EXISTS idx_code_writes_ts ON code_writes(ts);
-- Test run de unit tests del registry (go test / pytest / etc.).
CREATE TABLE IF NOT EXISTS test_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL DEFAULT '',
function_id TEXT NOT NULL DEFAULT '',
test_id TEXT NOT NULL DEFAULT '',
passed INTEGER NOT NULL DEFAULT 1,
duration_ms INTEGER NOT NULL DEFAULT 0,
output_snippet TEXT NOT NULL DEFAULT '',
ts INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_test_runs_function ON test_runs(function_id);
CREATE INDEX IF NOT EXISTS idx_test_runs_passed ON test_runs(passed);
CREATE INDEX IF NOT EXISTS idx_test_runs_ts ON test_runs(ts);
-- E2E checks de apps que dependen de una funcion del registry.
CREATE TABLE IF NOT EXISTS e2e_runs_fn (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL DEFAULT '',
function_id TEXT NOT NULL,
app_id TEXT NOT NULL,
check_id TEXT NOT NULL,
passed INTEGER NOT NULL DEFAULT 1,
ts INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_e2e_function ON e2e_runs_fn(function_id);
CREATE INDEX IF NOT EXISTS idx_e2e_app ON e2e_runs_fn(app_id);
CREATE INDEX IF NOT EXISTS idx_e2e_passed ON e2e_runs_fn(passed);
-- Antipatrones detectados (sqlite3 inline, import *, heredoc reescribiendo, etc.).
CREATE TABLE IF NOT EXISTS violations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL DEFAULT '',
rule_id TEXT NOT NULL,
function_id TEXT NOT NULL DEFAULT '',
command_snippet TEXT NOT NULL DEFAULT '',
severity TEXT NOT NULL DEFAULT 'warning',
ts INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_violations_rule ON violations(rule_id);
CREATE INDEX IF NOT EXISTS idx_violations_function ON violations(function_id);
CREATE INDEX IF NOT EXISTS idx_violations_severity ON violations(severity);
-- Heredocs/snippets clusterizados por similitud (alimenta proposals new_function).
CREATE TABLE IF NOT EXISTS patterns (
pattern_hash TEXT PRIMARY KEY,
representative_snippet TEXT NOT NULL,
occurrences INTEGER NOT NULL DEFAULT 1,
session_ids_json TEXT NOT NULL DEFAULT '[]',
first_seen INTEGER NOT NULL,
last_seen INTEGER NOT NULL,
proposal_id TEXT NOT NULL DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_patterns_occurrences ON patterns(occurrences);
CREATE INDEX IF NOT EXISTS idx_patterns_last_seen ON patterns(last_seen);
+100
View File
@@ -0,0 +1,100 @@
-- Vista agregada por funcion del registry. Lectura O(N) sobre tablas event-log.
-- Si performance degrada, materializar como TABLE refrescada por cron.
DROP VIEW IF EXISTS function_stats;
CREATE VIEW function_stats AS
WITH
call_agg AS (
SELECT
function_id,
COUNT(*) AS calls_total,
SUM(CASE WHEN ts >= CAST(strftime('%s','now','-1 day') AS INTEGER) THEN 1 ELSE 0 END) AS calls_24h,
SUM(CASE WHEN ts >= CAST(strftime('%s','now','-7 days') AS INTEGER) THEN 1 ELSE 0 END) AS calls_7d,
SUM(CASE WHEN ts >= CAST(strftime('%s','now','-30 days') AS INTEGER) THEN 1 ELSE 0 END) AS calls_30d,
SUM(CASE WHEN ts >= CAST(strftime('%s','now','-90 days') AS INTEGER) THEN 1 ELSE 0 END) AS calls_90d,
SUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) AS errors_total,
AVG(duration_ms) AS mean_duration_ms,
MAX(ts) AS last_used_at,
MAX(CASE WHEN success = 0 THEN ts ELSE 0 END) AS last_error_ts
FROM calls
WHERE function_id != ''
GROUP BY function_id
),
write_agg AS (
SELECT function_id, COUNT(*) AS writes_count, MAX(ts) AS last_write_at
FROM code_writes
WHERE function_id != ''
GROUP BY function_id
),
test_agg AS (
SELECT
function_id,
COUNT(*) AS tests_total,
SUM(CASE WHEN passed = 0 THEN 1 ELSE 0 END) AS tests_failed,
MAX(CASE WHEN passed = 0 THEN ts ELSE 0 END) AS last_test_failed_at
FROM test_runs
WHERE function_id != ''
GROUP BY function_id
),
e2e_agg AS (
SELECT
function_id,
COUNT(*) AS e2e_total,
SUM(CASE WHEN passed = 0 THEN 1 ELSE 0 END) AS e2e_failed,
COUNT(DISTINCT app_id) AS consumer_apps_count
FROM e2e_runs_fn
GROUP BY function_id
),
viol_agg AS (
SELECT function_id, COUNT(*) AS violations_caused
FROM violations
WHERE function_id != ''
GROUP BY function_id
),
all_fns AS (
SELECT function_id FROM call_agg
UNION
SELECT function_id FROM write_agg
UNION
SELECT function_id FROM test_agg
UNION
SELECT function_id FROM e2e_agg
UNION
SELECT function_id FROM viol_agg
)
SELECT
f.function_id,
COALESCE(c.calls_total, 0) AS calls_total,
COALESCE(c.calls_24h, 0) AS calls_24h,
COALESCE(c.calls_7d, 0) AS calls_7d,
COALESCE(c.calls_30d, 0) AS calls_30d,
COALESCE(c.calls_90d, 0) AS calls_90d,
COALESCE(c.errors_total, 0) AS errors_total,
CASE WHEN COALESCE(c.calls_total, 0) > 0
THEN CAST(c.errors_total AS REAL) / c.calls_total
ELSE 0 END AS error_rate,
COALESCE(c.mean_duration_ms, 0) AS mean_duration_ms,
c.last_used_at,
CASE WHEN c.last_error_ts > 0 THEN c.last_error_ts END AS last_error_ts,
COALESCE(w.writes_count, 0) AS writes_count,
w.last_write_at,
COALESCE(t.tests_total, 0) AS tests_total,
COALESCE(t.tests_failed, 0) AS tests_failed,
CASE WHEN COALESCE(t.tests_total, 0) > 0
THEN CAST(t.tests_failed AS REAL) / t.tests_total
ELSE 0 END AS test_fail_rate,
CASE WHEN t.last_test_failed_at > 0 THEN t.last_test_failed_at END AS last_test_failed_at,
COALESCE(e.e2e_total, 0) AS e2e_total,
COALESCE(e.e2e_failed, 0) AS e2e_failed,
CASE WHEN COALESCE(e.e2e_total, 0) > 0
THEN CAST(e.e2e_failed AS REAL) / e.e2e_total
ELSE 0 END AS e2e_fail_rate,
COALESCE(e.consumer_apps_count, 0) AS consumer_apps_count,
COALESCE(v.violations_caused, 0) AS violations_caused
FROM all_fns f
LEFT JOIN call_agg c ON c.function_id = f.function_id
LEFT JOIN write_agg w ON w.function_id = f.function_id
LEFT JOIN test_agg t ON t.function_id = f.function_id
LEFT JOIN e2e_agg e ON e.function_id = f.function_id
LEFT JOIN viol_agg v ON v.function_id = f.function_id;
+23
View File
@@ -0,0 +1,23 @@
-- function_versions: historial de versiones por function_id.
-- Fuente principal = `call_monitor snapshot` que lee registry.db.functions.content_hash
-- tras cada `fn index`. Edit-hook tambien anota cambios con sha256 del archivo.
-- copy_detected = source de la fase 0085k (fn doctor copied-code).
--
-- PK incluye source para permitir que cada source mantenga su propio espacio de
-- hashes (el sha256 del archivo NO coincide con el content_hash canonical del
-- registry.db, que incluye metadata adicional).
CREATE TABLE IF NOT EXISTS function_versions (
function_id TEXT NOT NULL,
content_hash TEXT NOT NULL,
version TEXT NOT NULL DEFAULT '',
snapped_at INTEGER NOT NULL,
source TEXT NOT NULL,
lines_added INTEGER NOT NULL DEFAULT 0,
lines_removed INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (function_id, content_hash, source)
);
CREATE INDEX IF NOT EXISTS idx_fn_versions_function ON function_versions(function_id);
CREATE INDEX IF NOT EXISTS idx_fn_versions_snapped_at ON function_versions(snapped_at);
CREATE INDEX IF NOT EXISTS idx_fn_versions_source ON function_versions(source);
+20
View File
@@ -0,0 +1,20 @@
-- copied_code: matches de cuerpos de funcion del registry encontrados en apps.
-- Poblado por `call_monitor copied-code` que invoca infra.AuditCopiedCode.
-- Cada fila representa un (app_file, app_function, registry_id) sospechoso.
-- UNIQUE incluye body_hash para que solo se inserte una vez por (path,fn,id,hash).
CREATE TABLE IF NOT EXISTS copied_code (
id INTEGER PRIMARY KEY AUTOINCREMENT,
app_file TEXT NOT NULL,
app_function TEXT NOT NULL,
registry_id TEXT NOT NULL,
body_hash TEXT NOT NULL,
similarity REAL NOT NULL DEFAULT 1.0,
kind TEXT NOT NULL,
detected_at INTEGER NOT NULL,
UNIQUE(app_file, app_function, registry_id, body_hash)
);
CREATE INDEX IF NOT EXISTS idx_copied_registry ON copied_code(registry_id);
CREATE INDEX IF NOT EXISTS idx_copied_kind ON copied_code(kind);
CREATE INDEX IF NOT EXISTS idx_copied_detected ON copied_code(detected_at);
@@ -0,0 +1,37 @@
-- session_capability_growth: vista que mide el ciclo "crear+usar" por sesion.
-- Una funcion se considera "creada en sesion X" si el primer code_writes
-- registrado para ese function_id (a nivel global) ocurrio dentro de la sesion X.
-- Una funcion "usada en sesion X" = >=1 fila en calls con misma sesion y ts >= created_at.
--
-- Lectura: created_this_session = COUNT(*) WHERE session_id = ?
-- used = COUNT(*) WHERE session_id = ? AND calls_in_session > 0
-- orphan = COUNT(*) WHERE session_id = ? AND calls_in_session = 0
--
-- Issue 0086. Aditivo. No reescribe schema previo.
CREATE VIEW IF NOT EXISTS session_capability_growth AS
WITH first_write AS (
SELECT
cw.function_id,
cw.session_id,
cw.ts AS created_at
FROM code_writes cw
WHERE cw.function_id != ''
AND cw.ts = (
SELECT MIN(ts) FROM code_writes
WHERE function_id = cw.function_id
)
)
SELECT
fw.session_id,
fw.function_id,
fw.created_at,
(SELECT MIN(c.ts) FROM calls c
WHERE c.function_id = fw.function_id
AND c.session_id = fw.session_id
AND c.ts >= fw.created_at) AS first_call_at,
(SELECT COUNT(*) FROM calls c
WHERE c.function_id = fw.function_id
AND c.session_id = fw.session_id
AND c.ts >= fw.created_at) AS calls_in_session
FROM first_write fw;
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+62
View File
@@ -0,0 +1,62 @@
package main
import (
"fmt"
"os"
"fn-registry/functions/infra"
)
// runPropose invokes the proposal generator pipeline and persists drafts
// into registry.db.proposals (INSERT OR IGNORE — never overwrites reviewed).
func runPropose(rootOverride string, dryRun bool) {
root := resolveRegistryRoot(rootOverride)
if _, err := os.Stat(rootOverride + "/registry.db"); err != nil && rootOverride != "" {
// fall back to walk-up
root = resolveRegistryRoot("")
}
if _, err := os.Stat(root + "/registry.db"); err != nil {
fmt.Fprintf(os.Stderr, "error: registry.db not found under %s\n", root)
os.Exit(1)
}
drafts, err := infra.GenerateProposalsFromTelemetry(root)
if err != nil {
fmt.Fprintf(os.Stderr, "error: generate: %v\n", err)
os.Exit(1)
}
if len(drafts) == 0 {
fmt.Println("propose: no proposal drafts generated (insufficient telemetry or all rules pass).")
return
}
// Group by rule for the summary
byRule := map[string]int{}
for _, d := range drafts {
byRule[d.RuleID]++
}
fmt.Printf("propose: %d draft(s) generated\n", len(drafts))
for rule, n := range byRule {
fmt.Printf(" %s: %d\n", rule, n)
}
if dryRun {
fmt.Println("\nDRY RUN — drafts NOT persisted. Sample (first 5):")
for i, d := range drafts {
if i >= 5 {
break
}
fmt.Printf(" [%s] %s → %s\n", d.RuleID, d.ID, d.Title)
}
return
}
inserted, total, err := infra.PersistProposalDrafts(root, drafts)
if err != nil {
fmt.Fprintf(os.Stderr, "error: persist: %v\n", err)
os.Exit(1)
}
fmt.Printf("\npersisted: %d new, %d total (idempotent — existing IDs skipped)\n", inserted, total)
}
+100
View File
@@ -0,0 +1,100 @@
package main
import (
"fmt"
"time"
"fn-registry/functions/infra"
)
// snapshotFromRegistry reads registry.db.functions and inserts one row in
// function_versions per (function_id, content_hash) tuple with source='index'.
// Duplicate rows (same hash for same function from the same source) are
// silently ignored — so this can be re-run after every `fn index` to capture
// only NEW versions.
//
// Returns (inserted_rows, total_seen, error).
func snapshotFromRegistry(callDB *DB, registryPath string) (int, int, error) {
rconn, err := infra.SQLiteOpen(registryPath, "")
if err != nil {
return 0, 0, fmt.Errorf("open registry: %w", err)
}
defer rconn.Close()
rows, err := rconn.Query("SELECT id, content_hash, version FROM functions WHERE content_hash != ''")
if err != nil {
return 0, 0, fmt.Errorf("query functions: %w", err)
}
defer rows.Close()
now := time.Now().UTC().Unix()
tx, err := callDB.conn.Begin()
if err != nil {
return 0, 0, err
}
stmt, err := tx.Prepare(`INSERT OR IGNORE INTO function_versions
(function_id, content_hash, version, snapped_at, source, lines_added, lines_removed)
VALUES (?, ?, ?, ?, 'index', 0, 0)`)
if err != nil {
_ = tx.Rollback()
return 0, 0, err
}
defer stmt.Close()
inserted, seen := 0, 0
for rows.Next() {
var id, hash, version string
if err := rows.Scan(&id, &hash, &version); err != nil {
_ = tx.Rollback()
return 0, 0, err
}
seen++
res, err := stmt.Exec(id, hash, version, now)
if err != nil {
_ = tx.Rollback()
return 0, 0, err
}
if n, _ := res.RowsAffected(); n > 0 {
inserted++
}
}
if err := rows.Err(); err != nil {
_ = tx.Rollback()
return 0, 0, err
}
if err := tx.Commit(); err != nil {
return 0, 0, err
}
return inserted, seen, nil
}
// versionsSummary returns aggregate stats per function_id from function_versions.
func (d *DB) versionsSummary(limit int) ([]funcVersionSummary, error) {
rows, err := d.conn.Query(`
SELECT function_id,
COUNT(DISTINCT content_hash) AS versions,
MAX(snapped_at) AS last_snapped_at
FROM function_versions
GROUP BY function_id
ORDER BY versions DESC, last_snapped_at DESC
LIMIT ?`, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var out []funcVersionSummary
for rows.Next() {
var s funcVersionSummary
if err := rows.Scan(&s.FunctionID, &s.Versions, &s.LastSnappedAt); err != nil {
return nil, err
}
out = append(out, s)
}
return out, rows.Err()
}
type funcVersionSummary struct {
FunctionID string
Versions int64
LastSnappedAt int64
}