47fac22230
- .claude/CLAUDE.md - .claude/commands/subagentes.md - .claude/rules/INDEX.md - .mcp.json - bash/functions/cybersecurity/analyze_dns.md - bash/functions/cybersecurity/audit_http_headers.md - bash/functions/cybersecurity/audit_ssh_config.md - bash/functions/cybersecurity/check_firewall.md - bash/functions/cybersecurity/detect_suspicious_users.md - bash/functions/cybersecurity/encrypt_file.md - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
308 lines
9.7 KiB
Go
308 lines
9.7 KiB
Go
package infra
|
|
|
|
import (
|
|
"crypto/sha1"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
|
)
|
|
|
|
// ProposalDraft is a candidate proposal computed from call_monitor telemetry
|
|
// + registry static data. Persistence step writes it into registry.db.proposals
|
|
// with deterministic id (so re-runs are idempotent).
|
|
type ProposalDraft struct {
|
|
ID string `json:"id"`
|
|
Kind string `json:"kind"` // new_function | improve_function | deprecate_function | new_pipeline | ...
|
|
TargetID string `json:"target_id"` // existing function id (empty for new_function)
|
|
Title string `json:"title"`
|
|
Description string `json:"description"`
|
|
RuleID string `json:"rule_id"` // copy_detected | orphan | bug | wrapper_skipped | ...
|
|
Evidence map[string]any `json:"evidence"`
|
|
}
|
|
|
|
// GenerateProposalsFromTelemetry inspects call_monitor.operations.db and
|
|
// registry.db, applies the rules documented in issue 0085, and returns the
|
|
// proposed drafts. It does NOT write to any DB. Call PersistProposalDrafts
|
|
// to upsert into registry.db.proposals.
|
|
//
|
|
// Implemented rules (MVP):
|
|
// - copy_detected → improve_function (target = matched registry_id)
|
|
// - orphan → deprecate_function (calls_90d=0 AND writes_count=0
|
|
// AND no upstream consumer in registry.db.functions.uses_functions)
|
|
// - bug → improve_function (error_rate > 0.1 AND calls_total > 3)
|
|
// - wrapper_skip → improve_function (violations.function_id grouped, count > 3)
|
|
//
|
|
// Skipped rules (no data yet): perf_regression, test_flaky, e2e_blast_radius,
|
|
// pattern_inline (requires patterns table populated).
|
|
func GenerateProposalsFromTelemetry(registryRoot string) ([]ProposalDraft, error) {
|
|
regPath := filepath.Join(registryRoot, "registry.db")
|
|
monPath := filepath.Join(registryRoot, "projects", "fn_monitoring", "apps", "call_monitor", "operations.db")
|
|
|
|
regDB, err := sql.Open("sqlite3", regPath+"?_journal_mode=WAL")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open registry: %w", err)
|
|
}
|
|
defer regDB.Close()
|
|
|
|
monDB, err := sql.Open("sqlite3", monPath+"?_journal_mode=WAL")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open call_monitor: %w", err)
|
|
}
|
|
defer monDB.Close()
|
|
|
|
// Build set of "function_ids consumed by anything upstream" from registry
|
|
// (functions/apps/analysis.uses_functions JSON array). Used to filter orphan
|
|
// candidates.
|
|
consumers, err := loadConsumerSet(regDB)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var drafts []ProposalDraft
|
|
|
|
// --- Rule 1: copy_detected ---
|
|
rows, err := monDB.Query(`SELECT app_file, app_function, registry_id, kind, similarity
|
|
FROM copied_code ORDER BY detected_at DESC`)
|
|
if err == nil {
|
|
for rows.Next() {
|
|
var appFile, appFunc, regID, kind string
|
|
var sim float64
|
|
if err := rows.Scan(&appFile, &appFunc, ®ID, &kind, &sim); err != nil {
|
|
continue
|
|
}
|
|
drafts = append(drafts, ProposalDraft{
|
|
ID: deterministicID("copy_detected", regID, appFile+":"+appFunc),
|
|
Kind: "improve_function",
|
|
TargetID: regID,
|
|
Title: fmt.Sprintf("Replace inline copy with import: %s in %s::%s", regID, shortPath(appFile), appFunc),
|
|
Description: fmt.Sprintf("App `%s` declares `%s` with a body matching `%s` (%s, similarity %.2f). "+
|
|
"Replace the inline body with a registry import to centralize maintenance.",
|
|
appFile, appFunc, regID, kind, sim),
|
|
RuleID: "copy_detected",
|
|
Evidence: map[string]any{
|
|
"rule_id": "copy_detected",
|
|
"app_file": appFile,
|
|
"app_function": appFunc,
|
|
"registry_id": regID,
|
|
"kind": kind,
|
|
"similarity": sim,
|
|
},
|
|
})
|
|
}
|
|
rows.Close()
|
|
}
|
|
|
|
// --- Rule 2: orphan (deprecate_function) ---
|
|
// function_stats rows with no calls in 90d AND no writes.
|
|
// Cross-check against consumers set: if anybody imports it, NOT orphan.
|
|
rows, err = monDB.Query(`
|
|
SELECT function_id, calls_90d, writes_count
|
|
FROM function_stats
|
|
WHERE calls_90d = 0 AND writes_count = 0`)
|
|
if err == nil {
|
|
for rows.Next() {
|
|
var fnID string
|
|
var calls90d, writes int
|
|
if err := rows.Scan(&fnID, &calls90d, &writes); err != nil {
|
|
continue
|
|
}
|
|
if _, isConsumed := consumers[fnID]; isConsumed {
|
|
continue
|
|
}
|
|
drafts = append(drafts, ProposalDraft{
|
|
ID: deterministicID("orphan", fnID, ""),
|
|
Kind: "deprecate_function",
|
|
TargetID: fnID,
|
|
Title: "Deprecate orphan function: " + fnID,
|
|
Description: fmt.Sprintf("`%s` has no calls in 90d, no recent writes, and no upstream consumer "+
|
|
"declared in registry. Candidate for deprecation. Verify with `fn doctor unused` and manual review.", fnID),
|
|
RuleID: "orphan",
|
|
Evidence: map[string]any{
|
|
"rule_id": "orphan",
|
|
"calls_90d": calls90d,
|
|
"writes_count": writes,
|
|
},
|
|
})
|
|
}
|
|
rows.Close()
|
|
}
|
|
|
|
// --- Rule 3: bug (improve_function on error_rate) ---
|
|
rows, err = monDB.Query(`
|
|
SELECT function_id, calls_total, errors_total, error_rate
|
|
FROM function_stats
|
|
WHERE error_rate > 0.1 AND calls_total > 3`)
|
|
if err == nil {
|
|
for rows.Next() {
|
|
var fnID string
|
|
var calls, errs int
|
|
var rate float64
|
|
if err := rows.Scan(&fnID, &calls, &errs, &rate); err != nil {
|
|
continue
|
|
}
|
|
drafts = append(drafts, ProposalDraft{
|
|
ID: deterministicID("bug", fnID, ""),
|
|
Kind: "improve_function",
|
|
TargetID: fnID,
|
|
Title: fmt.Sprintf("Investigate high error rate: %s (%.0f%% over %d calls)", fnID, rate*100, calls),
|
|
Description: fmt.Sprintf("`%s` fails %.1f%% of the time across %d calls (%d errors). "+
|
|
"Check call_monitor.operations.db for recent error_class samples.",
|
|
fnID, rate*100, calls, errs),
|
|
RuleID: "bug",
|
|
Evidence: map[string]any{
|
|
"rule_id": "bug",
|
|
"calls_total": calls,
|
|
"errors_total": errs,
|
|
"error_rate": rate,
|
|
},
|
|
})
|
|
}
|
|
rows.Close()
|
|
}
|
|
|
|
// --- Rule 4: wrapper_skip (violations grouped) ---
|
|
rows, err = monDB.Query(`
|
|
SELECT function_id, COUNT(*) AS hits, MAX(severity) AS sev
|
|
FROM violations
|
|
WHERE function_id != ''
|
|
GROUP BY function_id
|
|
HAVING hits > 3`)
|
|
if err == nil {
|
|
for rows.Next() {
|
|
var fnID, sev string
|
|
var hits int
|
|
if err := rows.Scan(&fnID, &hits, &sev); err != nil {
|
|
continue
|
|
}
|
|
drafts = append(drafts, ProposalDraft{
|
|
ID: deterministicID("wrapper_skip", fnID, ""),
|
|
Kind: "improve_function",
|
|
TargetID: fnID,
|
|
Title: fmt.Sprintf("Wrapper skipped %d times: %s", hits, fnID),
|
|
Description: fmt.Sprintf("Agent bypassed the registry wrapper for `%s` %d times. "+
|
|
"API surface may be incomplete — check what callers needed and extend.", fnID, hits),
|
|
RuleID: "wrapper_skip",
|
|
Evidence: map[string]any{
|
|
"rule_id": "wrapper_skip",
|
|
"hits": hits,
|
|
"severity": sev,
|
|
},
|
|
})
|
|
}
|
|
rows.Close()
|
|
}
|
|
|
|
return drafts, nil
|
|
}
|
|
|
|
// PersistProposalDrafts upserts each draft into registry.db.proposals via
|
|
// INSERT OR IGNORE so previously-reviewed proposals (approved/rejected) are
|
|
// preserved untouched. Returns (newly_inserted, total_seen, error).
|
|
func PersistProposalDrafts(registryRoot string, drafts []ProposalDraft) (int, int, error) {
|
|
if len(drafts) == 0 {
|
|
return 0, 0, nil
|
|
}
|
|
regPath := filepath.Join(registryRoot, "registry.db")
|
|
conn, err := sql.Open("sqlite3", regPath+"?_journal_mode=WAL")
|
|
if err != nil {
|
|
return 0, 0, fmt.Errorf("open registry: %w", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
tx, err := conn.Begin()
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
stmt, err := tx.Prepare(`INSERT OR IGNORE INTO proposals
|
|
(id, kind, target_id, title, description, evidence, status, created_by, reviewed_by, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, 'pending', 'reactive_loop', '', ?, ?)`)
|
|
if err != nil {
|
|
_ = tx.Rollback()
|
|
return 0, 0, err
|
|
}
|
|
defer stmt.Close()
|
|
|
|
now := time.Now().UTC().Format(time.RFC3339)
|
|
inserted := 0
|
|
for _, d := range drafts {
|
|
evidence, _ := json.Marshal(d.Evidence)
|
|
res, err := stmt.Exec(d.ID, d.Kind, d.TargetID, d.Title, d.Description, string(evidence), now, 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(drafts), nil
|
|
}
|
|
|
|
// ---- Helpers ----
|
|
|
|
// loadConsumerSet returns every function id that appears in any uses_functions
|
|
// JSON array across functions, apps, and analysis tables. Used to filter orphan
|
|
// candidates.
|
|
func loadConsumerSet(db *sql.DB) (map[string]struct{}, error) {
|
|
consumers := make(map[string]struct{})
|
|
|
|
queries := []string{
|
|
"SELECT uses_functions FROM functions WHERE uses_functions != '[]'",
|
|
"SELECT uses_functions FROM apps WHERE uses_functions != '[]'",
|
|
"SELECT uses_functions FROM analysis WHERE uses_functions != '[]'",
|
|
}
|
|
for _, q := range queries {
|
|
rows, err := db.Query(q)
|
|
if err != nil {
|
|
// Table may not exist (e.g. analysis); ignore.
|
|
continue
|
|
}
|
|
for rows.Next() {
|
|
var raw string
|
|
if err := rows.Scan(&raw); err != nil {
|
|
continue
|
|
}
|
|
var ids []string
|
|
if err := json.Unmarshal([]byte(raw), &ids); err != nil {
|
|
continue
|
|
}
|
|
for _, id := range ids {
|
|
consumers[id] = struct{}{}
|
|
}
|
|
}
|
|
rows.Close()
|
|
}
|
|
return consumers, nil
|
|
}
|
|
|
|
// deterministicID returns a stable proposal id derived from (rule, target, extra).
|
|
// Format: "auto_<rule>_<sha1-10>" so re-running the generator never inserts
|
|
// duplicates of the same logical proposal.
|
|
func deterministicID(rule, target, extra string) string {
|
|
key := rule + "|" + target + "|" + extra
|
|
h := sha1.Sum([]byte(key))
|
|
short := fmt.Sprintf("%x", h)[:10]
|
|
return "auto_" + rule + "_" + short
|
|
}
|
|
|
|
func shortPath(p string) string {
|
|
idx := strings.LastIndex(p, "/")
|
|
if idx == -1 || idx == len(p)-1 {
|
|
return p
|
|
}
|
|
parent := p[:idx]
|
|
pidx := strings.LastIndex(parent, "/")
|
|
if pidx == -1 {
|
|
return p
|
|
}
|
|
return p[pidx+1:]
|
|
}
|