Files
fn_registry/functions/infra/audit_capability_groups.go
egutierrez 47fac22230 chore: auto-commit (799 archivos)
- .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>
2026-05-14 00:28:20 +02:00

210 lines
6.0 KiB
Go

package infra
import (
"bufio"
"database/sql"
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
_ "github.com/mattn/go-sqlite3"
)
// CapabilityGroupAudit holds the audit result for a single capability group.
type CapabilityGroupAudit struct {
Group string `json:"group"` // slug del grupo (tag canonico)
DeclaredInIndex bool `json:"declared_in_index"` // aparece en docs/capabilities/INDEX.md
DocExists bool `json:"doc_exists"` // existe docs/capabilities/<group>.md
FunctionCount int `json:"function_count"` // funciones con ese tag en registry.db
Issues []string `json:"issues"` // lista de problemas detectados
OK bool `json:"ok"` // true si Issues esta vacio
}
// indexLinkRe matches markdown links [slug](target.md). Go regexp has no
// backreferences, so we capture both sides and compare in code.
var indexLinkRe = regexp.MustCompile(`\[([a-z][a-z0-9_-]*)\]\(([a-z][a-z0-9_-]*)\.md\)`)
// AuditCapabilityGroups audits the drift between:
// 1. Groups declared in docs/capabilities/INDEX.md (parsed via link regex).
// 2. Tags present in functions.tags of registry.db (via json_each).
// 3. Doc pages existing as docs/capabilities/<group>.md on disk.
//
// Returns one CapabilityGroupAudit per group/candidate, ordered by Group ASC.
// Tags with count >= 3 that are not declared in INDEX are surfaced as
// "ungrouped_candidate". Doc files not listed in INDEX are surfaced as
// "doc_orphan".
func AuditCapabilityGroups(root string) ([]CapabilityGroupAudit, error) {
// --- 1. Open registry.db (read-only) ---
dbPath := filepath.Join(root, "registry.db")
dsn := fmt.Sprintf("file:%s?mode=ro&_foreign_keys=on", dbPath)
db, err := sql.Open("sqlite3", dsn)
if err != nil {
return nil, fmt.Errorf("audit_capability_groups: open db: %w", err)
}
defer db.Close()
if err := db.Ping(); err != nil {
return nil, fmt.Errorf("audit_capability_groups: ping db: %w", err)
}
// --- 2. Query all tags with their function counts ---
tagCounts, err := queryTagCounts(db)
if err != nil {
return nil, fmt.Errorf("audit_capability_groups: query tags: %w", err)
}
// --- 3. Parse INDEX.md for declared group slugs ---
indexPath := filepath.Join(root, "docs", "capabilities", "INDEX.md")
indexSlugs, err := parseIndexSlugs(indexPath)
if err != nil {
return nil, fmt.Errorf("audit_capability_groups: parse INDEX.md: %w", err)
}
// --- 4. Scan docs/capabilities/ for existing .md files (excluding INDEX.md) ---
capDir := filepath.Join(root, "docs", "capabilities")
docSlugs, err := scanDocSlugs(capDir)
if err != nil {
return nil, fmt.Errorf("audit_capability_groups: scan docs: %w", err)
}
// --- 5. Build audit entries ---
// We collect all relevant slugs: declared in INDEX, present as doc, or tags >= 3.
allSlugs := make(map[string]struct{})
for s := range indexSlugs {
allSlugs[s] = struct{}{}
}
for s := range docSlugs {
allSlugs[s] = struct{}{}
}
for tag, count := range tagCounts {
if count >= 3 {
allSlugs[tag] = struct{}{}
}
}
results := make([]CapabilityGroupAudit, 0, len(allSlugs))
for slug := range allSlugs {
_, declaredInIndex := indexSlugs[slug]
_, docExists := docSlugs[slug]
count := tagCounts[slug]
var issues []string
if declaredInIndex {
if !docExists {
issues = append(issues, "doc_missing")
}
if count == 0 {
issues = append(issues, "no_functions")
} else if count < 3 {
issues = append(issues, "below_minimum")
}
} else if docExists {
// Doc exists but not in INDEX
issues = append(issues, "doc_orphan")
} else if count >= 3 {
// Tag with enough functions but no declaration or doc
issues = append(issues, "ungrouped_candidate")
}
results = append(results, CapabilityGroupAudit{
Group: slug,
DeclaredInIndex: declaredInIndex,
DocExists: docExists,
FunctionCount: count,
Issues: issues,
OK: len(issues) == 0,
})
}
sort.Slice(results, func(i, j int) bool {
return results[i].Group < results[j].Group
})
return results, nil
}
// queryTagCounts returns a map of tag → function count using json_each on functions.tags.
func queryTagCounts(db *sql.DB) (map[string]int, error) {
rows, err := db.Query(`
SELECT j.value AS tag, COUNT(*) AS cnt
FROM functions f, json_each(f.tags) j
GROUP BY j.value
`)
if err != nil {
return nil, err
}
defer rows.Close()
counts := make(map[string]int)
for rows.Next() {
var tag string
var cnt int
if err := rows.Scan(&tag, &cnt); err != nil {
continue
}
counts[tag] = cnt
}
return counts, rows.Err()
}
// parseIndexSlugs reads INDEX.md and extracts group slugs from links of the
// form [slug](slug.md). Returns the set of declared slugs.
func parseIndexSlugs(indexPath string) (map[string]struct{}, error) {
f, err := os.Open(indexPath)
if err != nil {
if os.IsNotExist(err) {
// No INDEX.md — return empty set, not an error.
return make(map[string]struct{}), nil
}
return nil, err
}
defer f.Close()
slugs := make(map[string]struct{})
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
matches := indexLinkRe.FindAllStringSubmatch(line, -1)
for _, m := range matches {
// Self-referencing link only: [slug](slug.md), not external.
label := strings.TrimSpace(m[1])
target := strings.TrimSpace(m[2])
if label != "" && label == target {
slugs[label] = struct{}{}
}
}
}
return slugs, scanner.Err()
}
// scanDocSlugs returns the set of slugs that have a corresponding .md file in
// capDir, excluding INDEX.md itself.
func scanDocSlugs(capDir string) (map[string]struct{}, error) {
entries, err := os.ReadDir(capDir)
if err != nil {
if os.IsNotExist(err) {
return make(map[string]struct{}), nil
}
return nil, err
}
slugs := make(map[string]struct{})
for _, e := range entries {
if e.IsDir() {
continue
}
name := e.Name()
if !strings.HasSuffix(name, ".md") {
continue
}
slug := strings.TrimSuffix(name, ".md")
if slug == "INDEX" {
continue
}
slugs[slug] = struct{}{}
}
return slugs, nil
}