feat(infra): auto-commit con 29 cambios

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-14 02:06:44 +02:00
parent 47fac22230
commit ca1bf5a59b
29 changed files with 2148 additions and 11 deletions
+13
View File
@@ -2,6 +2,19 @@
Registry personal de codigo reutilizable con busqueda FTS. Diseñado para composicion funcional y agentes.
## Objetivos del registry (Norte) — Issues 0086 + 0087
**4 metricas optimizadas por el bucle reactivo** (visibles en Monitor tab del `registry_dashboard`):
1. **MAXIMIZAR `Reg %`** — porcentaje de calls del agente que golpean una funcion del registry (`function_id != ''`). Cada bash inline o heredoc que reescribe logica baja el ratio. Target: subir cada semana.
2. **MEJORAR uso del registry por Claude** — el agente debe encontrar y usar funciones existentes antes de escribir codigo. Indicadores: `MCP` (mcp/heredoc/fn run) sube; violations baja. Si Claude no encuentra una funcion por busqueda mediocre, mejorar `description`/`tags`/`params_schema` de esa funcion.
3. **ACELERAR tareas comunes via funciones nuevas** — patrones inline repetidos >2 veces -> `fn-constructor` crea la funcion, Claude la usa el siguiente turno. Velocidad medida en pasos (turnos) por tarea. Pattern detection: tab Monitor + `mcp__registry__fn_proposal action="list"`.
4. **PROMOVER COMPOSICIONES A PIPELINES** (issue 0087) — el registry no crece inflando funciones, crece **promoviendo secuencias A→B(→C) que se repiten con exito** a pipelines one-shot. Hoy `bank_login + bank_make_transfer` (2 calls). Manana `bank_transfer_oneshot` (1 call). Misma capacidad, mitad de pasos. Detectado por telemetria de secuencias en `call_monitor`. Una funcion que hace bien UNA cosa NO necesita crecer — lo que crece es el catalogo de composiciones probadas.
**Auto-discovery zero-second-lookup:** cada `.md` debe ser autosuficiente — `## Ejemplo` lanzable + `## Cuando usarla` + `## Gotchas` (impuras). Descubrir = lanzar, sin segunda lectura. Ver `.claude/rules/function_growth_and_self_docs.md`.
Cualquier decision tecnica que choque con estos objetivos esta mal priorizada. Ejemplo: un bash heredoc rapido hoy que reinventa logica = penaliza objetivos 1 y 3 manana.
**Dos bases de datos SQLite:**
- **registry.db** (raiz) — funciones, tipos, proposals, apps, projects, analysis, vaults, pc_locations. Regenerable con `fn index` (excepto proposals y pc_locations).
- **operations.db** (por app en `apps/*/`) — entities, relations, executions, assertions. Datos vivos.
+35
View File
@@ -6,6 +6,41 @@ description: "Auto-auditoria: verifica que la sesion registra uso de funciones,
Comando meta: Claude se audita a si mismo. Verifica que su comportamiento en esta sesion (y las recientes) deja rastro en `call_monitor.operations.db`, detecta gaps reales del registry para el trabajo actual, lanza sub-agentes `fn-constructor` en paralelo para cerrar esos gaps, y verifica que la proxima vez usara las funciones nuevas.
## Objetivos del registry (Norte) — Issues 0086 + 0087
Cada corrida de `/fn_claude` optimiza 4 metricas visibles en Monitor tab del `registry_dashboard`:
1. **MAXIMIZAR `Reg %`** — % de calls con `function_id != ''`. Cada heredoc/bash que reescribe logica baja el ratio. Target: subir cada semana.
2. **MEJORAR uso del registry por Claude** — Claude busca y reusa antes de escribir. `MCP` (mcp/heredoc/fn run) sube, `violations` baja. Si una funcion existe pero Claude no la encuentra, mejorar su `description`/`tags`/`params_schema` (FTS indexa todo).
3. **ACELERAR tareas comunes** — patrones inline repetidos >2x -> `fn-constructor` los convierte en funcion, Claude las usa el siguiente turno. Menos pasos por tarea = mas valor.
4. **PROMOVER COMPOSICIONES A PIPELINES** (issue 0087) — el registry crece **promoviendo secuencias A->B(->C) que se repiten con exito** a pipelines one-shot. Una funcion que hace bien una cosa NO necesita crecer. Pattern detection: `call_monitor sequences --detect --propose` (cron 6h activo) + tab `Promotion candidates` del dashboard.
Si `/fn_claude` no mueve estas 4 metricas, no esta haciendo su trabajo.
## Infraestructura de discovery activa (issue 0087)
Cada turno tienes capacidades ya cargadas SIN buscar. Si no las usas estas pagando el coste de FTS innecesariamente:
| Senal | Donde | Que hacer |
|---|---|---|
| Linea `CAPABILITIES (cache 1h): TOP: ... FRESH (7d): ... PIPELINES: ...` en cada UserPromptSubmit | hook `hook_capabilities_inject.sh` | Antes de buscar con `mcp__registry__fn_search`, mira si la funcion que necesitas esta en TOP/FRESH/PIPELINES. Si si, ve directo a `fn show <id>` (1 read) o `./fn run <id>` (0 reads). |
| `<system-reminder>FUZZY-MATCH (issue 0087): your Bash command may already be a function. USE: ./fn run <id> -> <signature>` aparecido mid-flight | hook `hook_fn_match.sh` (PreToolUse, Bash matcher) | El hook detecto que tu Bash inline coincide con una funcion del registry. **NO ignores el reminder** — abandona el inline, llama a `./fn run <id>` o `mcp__registry__fn_run id="<id>"`. Si crees que la sugerencia es falso positivo, justifica brevemente antes de seguir inline (queda en violations). |
| Hint AUSENTE para una query corta (`rsi sma` < 3 tokens) | threshold `raw_score >= 4.0` no alcanzado | NO interpretar la ausencia de hint como "no existe funcion". Usa `mcp__registry__fn_search` con query mas rica (3+ tokens del dominio). |
| Falso positivo conocido: `agent` token | `robots.txt user-agent` matchea `agent_scaffold` | Ignora el reminder y sigue. Cost = 1 reminder ignorable. |
## Como combinar la 3 senales para minimizar pasos
1. **User prompt llega** -> lees `CAPABILITIES` line. Si la tarea encaja claramente con TOP/FRESH -> usa directo.
2. **Vas a escribir Bash inline** -> el hook PreToolUse lo intercepta. Si dispara FUZZY-MATCH -> usa `./fn run <id>`.
3. **No hay match y necesitas codigo** -> `mcp__registry__fn_search` con 3+ tokens. Si sigue sin hit -> delega a `fn-constructor` (no escribas inline). Patron repetido detectado por `call_monitor sequences` se promovera a pipeline en proximas iteraciones.
## Las 4 metricas norte (donde vigilarlas)
- `Reg %` (Monitor KPI) — % calls con function_id no vacio. Sube cuando el registry se usa.
- `MCP` (Monitor KPI) — count calls con tools registry-aware (mcp*/heredoc*/fn_cli_run). Adopcion de patrones canonicos.
- `Errors` / `Violations` (Monitor KPI) — bajan cuando el bucle cierra.
- `Failed Functions` (Monitor sub-tab) — registry-functions que fallaron: diagnostico de bugs prioritarios.
Issue 0085 fase autocompleta. Reemplaza el flujo manual de "veo un patron, decido si extraer, escribo proposal, espero humano, fn-mejorador genera, fn-orquestador opera". Con `/fn_claude` Claude hace todo eso solo, **autonomamente para si mismo**.
---
+1
View File
@@ -33,3 +33,4 @@ Reglas operativas del proyecto. Cada archivo es una regla independiente.
| 27 | [registry_calls.md](registry_calls.md) | Patrones canonicos para invocar funciones del registry (MCP inspect / MCP run / heredoc compose), antipatrones, excepciones, telemetria. Issue 0085 |
| 28 | [delegation.md](delegation.md) | Si vas a escribir logica reutilizable inline -> spawn fn-constructor inmediato + tag de grupo + usar en mismo turno. Issue 0086 |
| 29 | [capability_groups.md](capability_groups.md) | Tags planos + paginas madre `docs/capabilities/<grupo>.md` para desbloquear clusters de funciones en un read. Issue 0086 |
| 30 | [function_growth_and_self_docs.md](function_growth_and_self_docs.md) | Contrato self-doc de cada `.md` (Ejemplo + Cuando usarla + Gotchas + Growth log) + crecimiento del registry por **promocion de composiciones** a pipelines, NO por inflado de funciones. Issue 0087 |
@@ -0,0 +1,115 @@
## Function growth + self-documenting capability
Dos doctrinas hermanas. Una define **como deben ser** las funciones (auto-descubribles y lanzables sin segunda lectura). La otra define **como crece** el registry (no inflando funciones — promoviendo composiciones a pipelines).
Issue 0087.
---
### Parte A — `.md` autosuficiente (contrato OBLIGATORIO)
Cuando Claude (o un humano) encuentra una funcion via FTS / fuzzy match / capability page / TOP block, el `.md` debe bastar para **lanzarla sin abrir el codigo**. Esto es lo que hace que descubrir = lanzar y elimina el coste del second lookup.
**Secciones obligatorias** en cada `.md` del registry (functions + pipelines + types con uso practico):
| Seccion | Contenido | Tamaño |
|---|---|---|
| Frontmatter | `name`, `signature`, `params` (con `desc` por param), `output`, `tags`, `uses_functions`, etc. Lo de hoy. | — |
| `## Ejemplo` | Bloque de codigo lanzable con args **concretos**. Copiar+pegar produce ejecucion real. NO placeholders abstractos. | 3-10 lineas |
| `## Cuando usarla` | 1-2 frases con triggers: "cuando hagas X / antes de Y / si necesitas Z". Verbos imperativos. Ayuda al fuzzy match y a Claude a saber sin leer el codigo. | 1-3 lineas |
| `## Gotchas` | Problemas conocidos / no-go cases. Obligatoria para funciones impuras o con efectos (Windows-side, red, FS write, GPU). Omisible para funciones puras triviales. | 0-5 puntos |
| `## Capability growth log` | Solo SI la funcion ha crecido. Una linea por version: `v1.1.0 (YYYY-MM-DD) — anade --build flag para skip build`. No se rellena en v1.0.0. | crece con el tiempo |
**Anti-patrones del .md:**
- Ejemplo con `<arg1>`, `<arg2>` placeholders abstractos — NO. Ejemplos con valores reales (`registry_dashboard`, `/home/lucas/...`).
- "Cuando usarla" vacio o "ver descripcion arriba" — NO. Frase nueva con trigger explicito.
- `notes` lleno + `## Gotchas` vacio cuando la funcion tiene efectos — mover de `notes` a `## Gotchas`.
- Capability growth log inventado (sin que la funcion haya cambiado) — NO. Solo se rellena cuando hay version bump real.
**Verificacion** (TBD: convertir a check de `fn doctor`): cada .md de `functions/`/`pipelines/` debe tener `## Ejemplo` y `## Cuando usarla`. `## Gotchas` obligatoria solo si `purity: impure`. `## Capability growth log` libre.
---
### Parte B — Crecimiento por composicion (no por inflado)
**Principio:** una funcion que hace bien UNA cosa NO necesita crecer. Anadir params "por si acaso" la hace peor (Inner Platform Effect). Lo que crece es el **registry**: pipelines nuevos que componen funciones existentes.
#### Ejemplo del principio
- **Hoy:** Claude para hacer una transferencia bancaria llama `bank_login` -> `bank_list_accounts` -> `bank_make_transfer`. 3 calls, 3 decisiones, 3 puntos de fallo.
- **Manana:** pipeline `bank_transfer_oneshot(account, amount, target)` que compone las 3 internamente. 1 call, 1 decision.
Misma capacidad, 3x menos pasos. **Esto es lo que multiplica la velocidad de Claude**, no anadir flags a `bank_login`.
#### Como se promueve una composicion
Senal detectable en `call_monitor.operations.db`: secuencia A→B(→C) con
- **Mismo session_id**.
- **Intervalo entre calls < N segundos** (default 30s).
- **Occurrences > K** (default 5) en ventana de **D dias** (default 30).
- **Success rate > S** (default 0.9 — falla < 10%).
- **No existe ya un pipeline** que la cubra (validar con FTS sobre `uses_functions`).
Cuando se cumple → **proposal `new_pipeline`** con evidencia (sequence_ids, session_ids, occurrence count). Humano (o `fn-orquestador` autonomo) decide promover.
#### Implementacion (issue 0087 tanda A)
- `call_monitor sequences --detect` subcomando: escanea `calls` table, agrupa por session+window, computa secuencias, upserta en tabla `function_sequences`.
- Cron diario que ejecuta el detector + genera proposals automaticas.
- Visible en Monitor tab del `registry_dashboard`: sub-tab "Promotion candidates".
#### Cuando SI inflar una funcion
Casos legitimos para anadir feature a una funcion existente:
1. **Generalizar firma** sin romper consumidores (anadir param opcional con default sensato).
2. **Mejor manejo de error** (mensajes mas claros, retry sensible).
3. **Default mas inteligente** (autodetectar lo que antes era arg obligatorio).
4. **Eliminar gotcha conocido** (fix de bug que estaba en `## Gotchas`).
NO infles para casos hipoteticos. NO anadas params "por flexibilidad". Si dudas, separa la responsabilidad en una funcion nueva o un pipeline.
#### Capability growth log — cuando se rellena
- Se rellena **solo cuando la funcion crece** (alguno de los 4 casos arriba).
- Cada bump de `version` -> 1 linea en `## Capability growth log` con fecha y resumen 1-frase.
- Una funcion estable de hace 6 meses puede seguir en v1.0.0 sin log: indica madurez, no abandono.
- Telemetria (call_monitor) decide si una funcion estable es huerfana (`calls_90d=0`) o usada-y-buena (`calls_30d>10, error_rate<0.05`). Las primeras se deprecan; las segundas se respetan.
---
### Parte C — Output de discovery
Cuando un mecanismo de discovery (fuzzy match / FRESH hook / TOP block / capability page) surfacea una funcion, el payload **minimo** es:
```
<id> → <signature> → <ejemplo de 1 linea>
```
Ejemplo concreto:
```
redeploy_cpp_app_windows_bash_pipelines
./fn run redeploy_cpp_app_windows registry_dashboard /path/to/app [--build]
use: tras compilar cpp/build/windows, antes de smoke test manual
```
Si Claude necesita mas (gotchas, params completos, codigo), un `mcp__registry__fn_show <id>` adicional. Pero el primer hit ya basta para el 80% de casos.
---
### Parte D — Relacion con otras reglas
- [[registry_first]] dice CUANDO buscar/usar/delegar. Esta regla dice **COMO** debe ser la funcion para que esa busqueda valga.
- [[ids_naming]] hace ID predictible. Esta regla hace metadata predictible.
- [[delegation]] dice cuando spawnar fn-constructor. Esta regla es lo que fn-constructor debe producir.
- [[capability_groups]] agrupa funciones afines. Las paginas madre de cada grupo deben respetar el mismo contrato self-doc (mejor con su propio ejemplo end-to-end por grupo).
### Resumen TL;DR
1. Cada `.md` autosuficiente: Ejemplo + Cuando usarla + Gotchas (si impura) + Growth log (si crecio).
2. Las funciones que hacen bien una cosa NO necesitan crecer.
3. El registry crece **promoviendo composiciones repetidas a pipelines**, no inflando funciones.
4. Telemetria de `call_monitor` detecta secuencias candidatas y abre proposals automaticas.
5. Discovery devuelve siempre: `id + signature + 1-line example`. Resto on-demand.
+15
View File
@@ -33,6 +33,21 @@ Casos donde el MCP no aplica y `sqlite3 registry.db` es legitimo:
El hook `PreToolUse` (`.claude/scripts/hook_registry_mcp.sh`) ya deja pasar estas excepciones y solo avisa cuando ve `sqlite3 registry.db "SELECT ..."` plano.
### Excepcion: hooks e infraestructura de telemetria (issue 0087)
Los **hooks** (`PreToolUse`, `PostToolUse`, `UserPromptSubmit`, etc.) y los **binarios de infraestructura** que sirven al agente (`fn_match`, `fn doctor`, `call_monitor`) **pueden leer `registry.db` directo** via `sqlite3` o `database/sql` con conexion read-only. NO estan sujetos a la regla MCP-first porque:
- No son acciones del agente — son inspeccion automatizada del entorno.
- El MCP requiere tool invocation por Claude; un hook no puede invocar tools.
- Latencia objetivo (50-200ms) incompatible con round-trip MCP.
**Restricciones:**
- SOLO lectura. Conexion debe abrirse con `?mode=ro` o `?_query_only=1`.
- NUNCA escritura a `registry.db` desde hooks.
- Si un hook necesita escribir (cache, telemetria propia), usa su propia DB (`operations.db` del app de hooks, o `~/.fn_hooks/cache.db`).
Esta excepcion es **explicita y acotada** — no aplica al agente, que sigue regido por la regla MCP-first.
### Verificacion previa — `fn doctor`
Antes de empezar trabajo no trivial sobre el registry, ejecutar `fn doctor` para confirmar que el ecosistema esta sano:
+13 -4
View File
@@ -59,15 +59,24 @@ ARGS_HASH=$(printf '%s' "$INPUT" | jq -c '.tool_input // {}' | sha256sum | cut -
sql_escape() { printf '%s' "$1" | sed "s/'/''/g"; }
insert_call() {
local fn_id="$1" tool_used="$2" duration_ms="${3:-0}"
local fn_esc tu_esc ec_esc es_esc sid_esc ah_esc
local fn_id="$1" tool_used="$2" duration_ms="${3:-0}" snippet="${4:-}"
local fn_esc tu_esc ec_esc es_esc sid_esc ah_esc snip_esc
# Politica issue 0087: command_snippet solo se rellena cuando function_id
# esta vacio. Si la call golpea una funcion del registry, su ID y
# tool_used bastan; no duplicamos el comando.
if [ -n "$fn_id" ]; then snippet=""; fi
# Redact common secrets antes de persistir
snippet=$(printf '%s' "$snippet" \
| sed -E 's/(password|token|secret|api[_-]?key|bearer)([[:space:]]*[=:][[:space:]]*)[^[:space:]]+/\1\2<REDACTED>/Ig' \
| head -c 200)
fn_esc=$(sql_escape "$fn_id")
tu_esc=$(sql_escape "$tool_used")
ec_esc=$(sql_escape "$ERROR_CLASS")
es_esc=$(sql_escape "$ERROR_SNIPPET")
sid_esc=$(sql_escape "$SESSION_ID")
ah_esc=$(sql_escape "$ARGS_HASH")
sqlite3 "$DB" "INSERT INTO calls (session_id, function_id, tool_used, args_hash, duration_ms, success, error_class, error_snippet, ts) VALUES ('$sid_esc','$fn_esc','$tu_esc','$ah_esc',$duration_ms,$SUCCESS,'$ec_esc','$es_esc',$TS);" 2>/dev/null || true
snip_esc=$(sql_escape "$snippet")
sqlite3 "$DB" "INSERT INTO calls (session_id, function_id, tool_used, args_hash, duration_ms, success, error_class, error_snippet, command_snippet, ts) VALUES ('$sid_esc','$fn_esc','$tu_esc','$ah_esc',$duration_ms,$SUCCESS,'$ec_esc','$es_esc','$snip_esc',$TS);" 2>/dev/null || true
}
insert_code_write() {
@@ -204,7 +213,7 @@ case "$TOOL_NAME" in
TOOL_USED="sqlite_direct"
fi
insert_call "$FN_ID" "$TOOL_USED"
insert_call "$FN_ID" "$TOOL_USED" 0 "$CMD_HEAD"
# ---- Violation rules ----
# 1. sqlite3 directo SELECT sobre registry.db (excepto schema/pragma/count/join)
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env bash
# UserPromptSubmit hook: inyecta capacidades calientes (TOP/FRESH/PIPELINES)
# del registry como additionalContext en cada turno del usuario.
#
# Cache: ~/.cache/fn_registry/capabilities.txt (TTL 1h).
# Fuente: `./fn doctor capabilities --emit-claude-md` desde la raiz del repo.
#
# NUNCA bloquea: si algo falla, emite contexto vacio y sale 0.
set -uo pipefail
CACHE_DIR="${HOME}/.cache/fn_registry"
CACHE_FILE="${CACHE_DIR}/capabilities.txt"
TTL_SECONDS=3600
# Resolve registry root (walks up from cwd, fallback CLAUDE_PROJECT_DIR)
resolve_root() {
local d="${PWD}"
while [ "$d" != "/" ]; do
if [ -f "$d/registry.db" ] && [ -x "$d/fn" ]; then
printf '%s' "$d"
return 0
fi
d=$(dirname "$d")
done
if [ -n "${CLAUDE_PROJECT_DIR:-}" ] && [ -f "${CLAUDE_PROJECT_DIR}/registry.db" ]; then
printf '%s' "${CLAUDE_PROJECT_DIR}"
return 0
fi
return 1
}
# Consume stdin (UserPromptSubmit payload) — we don't need it but keep stdin clean
cat >/dev/null 2>&1 || true
ROOT=$(resolve_root) || exit 0
mkdir -p "$CACHE_DIR" 2>/dev/null || exit 0
# Cache freshness check
need_refresh=1
if [ -f "$CACHE_FILE" ]; then
now=$(date +%s)
mtime=$(stat -c %Y "$CACHE_FILE" 2>/dev/null || stat -f %m "$CACHE_FILE" 2>/dev/null || echo 0)
age=$((now - mtime))
if [ "$age" -lt "$TTL_SECONDS" ]; then
need_refresh=0
fi
fi
if [ "$need_refresh" -eq 1 ]; then
# Regenerate: call fn doctor capabilities --emit-claude-md and process
raw=$("$ROOT/fn" doctor capabilities --emit-claude-md 2>/dev/null || true)
if [ -z "$raw" ]; then
exit 0
fi
# Extract top 5 from each section using awk.
# Sections detected by "## ... Top" / "## ... Fresh" / "## ... Pipelines".
line=$(printf '%s\n' "$raw" | awk '
BEGIN { sec=""; n_top=0; n_fresh=0; n_pipe=0; }
/^## .*Top 20/ { sec="TOP"; next }
/^## .*Fresh/ { sec="FRESH"; next }
/^## .*Pipelines/ { sec="PIPE"; next }
/^## / { sec=""; next }
/^- `/ {
# extract first backticked token
s = $0
sub(/^- `/, "", s)
i = index(s, "`")
if (i == 0) next
id = substr(s, 1, i-1)
if (sec == "TOP" && n_top < 5) { tops[n_top++] = id }
if (sec == "FRESH" && n_fresh < 5) { fresh[n_fresh++] = id }
if (sec == "PIPE" && n_pipe < 5) { pipes[n_pipe++] = id }
}
END {
out = "CAPABILITIES (cache 1h):"
if (n_top > 0) {
line = " TOP: " tops[0]
for (i=1; i<n_top; i++) line = line ", " tops[i]
out = out "\n" line
}
if (n_fresh > 0) {
line = " FRESH (7d): " fresh[0]
for (i=1; i<n_fresh; i++) line = line ", " fresh[i]
out = out "\n" line
}
if (n_pipe > 0) {
line = " PIPELINES: " pipes[0]
for (i=1; i<n_pipe; i++) line = line ", " pipes[i]
out = out "\n" line
}
print out
}
')
if [ -z "$line" ]; then
exit 0
fi
printf '%s\n' "$line" >"$CACHE_FILE" 2>/dev/null || exit 0
fi
# Emit cached content as additionalContext
if [ ! -s "$CACHE_FILE" ]; then
exit 0
fi
ctx=$(cat "$CACHE_FILE")
if command -v jq >/dev/null 2>&1; then
jq -n --arg ctx "$ctx" '{
hookSpecificOutput: {
hookEventName: "UserPromptSubmit",
additionalContext: $ctx
}
}'
else
# Fallback: print raw text (Claude Code prints stdout as context too)
printf '%s\n' "$ctx"
fi
exit 0
+133
View File
@@ -0,0 +1,133 @@
#!/usr/bin/env bash
# PreToolUse hook: sugiere funciones del registry cuando un comando Bash
# inline probablemente reinventa una funcion existente (issue 0087).
#
# Llama a `./fn match "<cmd>"` con timeout 200ms. Si encaja con alta
# confianza, imprime un <system-reminder> a stderr para que Claude Code
# lo lea como recordatorio. NUNCA bloquea la tool — exit 0 siempre.
set -euo pipefail
# ---- Always exit 0, no matter what ----
trap 'exit 0' ERR
# ---- Resolve registry root (walks up from cwd) ----
resolve_root() {
local d="${PWD}"
while [ "$d" != "/" ]; do
if [ -f "$d/registry.db" ]; then
printf '%s' "$d"
return 0
fi
d=$(dirname "$d")
done
return 1
}
ROOT=$(resolve_root) || exit 0
FN_BIN="$ROOT/fn"
[ -x "$FN_BIN" ] || exit 0
# ---- Read stdin JSON ----
command -v jq >/dev/null 2>&1 || exit 0
INPUT=$(cat)
[ -z "$INPUT" ] && exit 0
TOOL_NAME=$(printf '%s' "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null || echo "")
[ "$TOOL_NAME" = "Bash" ] || exit 0
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null || echo "")
[ -z "$CMD" ] && exit 0
# Single-line for matching against denylist patterns
CMD_FLAT=$(printf '%s' "$CMD" | tr '\n' ' ')
# ---- Denylist (skip antes de llamar fn match para ahorrar el invoke) ----
# Comandos demasiado cortos -> trivial
CMD_LEN=${#CMD_FLAT}
[ "$CMD_LEN" -lt 20 ] && exit 0
# Trivial single-utility commands
case "$CMD_FLAT" in
"ls"|"ls "*|"cd"|"cd "*|"pwd"|"pwd "*|"cat"|"cat "*|"echo"|"echo "*)
exit 0 ;;
"grep"|"grep "*|"head"|"head "*|"tail"|"tail "*|"wc"|"wc "*)
exit 0 ;;
"mkdir"|"mkdir "*|"rm"|"rm "*|"mv"|"mv "*|"cp"|"cp "*)
exit 0 ;;
"git"|"git "*)
exit 0 ;;
"go"|"go "*)
# go build / go test corrientes — el agente ya los maneja
exit 0 ;;
esac
# Comandos que ya usan el registry: ./fn ..., fn run ..., mcp__registry__*
if printf '%s' "$CMD_FLAT" | grep -qE '(^|[[:space:]])\./fn([[:space:]]|$)'; then
exit 0
fi
if printf '%s' "$CMD_FLAT" | grep -qE '(^|[[:space:]])fn[[:space:]]+(run|search|show|code|uses|doctor|index|match|list|add|proposal|sync|ops|check)'; then
exit 0
fi
# Pure-cd (movement only, no logic)
if printf '%s' "$CMD_FLAT" | grep -qE '^[[:space:]]*cd[[:space:]]+[^&|;]+$'; then
exit 0
fi
# ---- Llamar fn match con timeout 200ms ----
command -v timeout >/dev/null 2>&1 || exit 0
# Truncar el comando a algo razonable para fn match (evitar args huge)
CMD_TRUNC=$(printf '%s' "$CMD_FLAT" | head -c 500)
MATCH_JSON=$(timeout 0.2 "$FN_BIN" match "$CMD_TRUNC" --format json --top 3 2>/dev/null) || exit 0
[ -z "$MATCH_JSON" ] && exit 0
# ---- Parsear JSON ----
HIGH_CONF=$(printf '%s' "$MATCH_JSON" | jq -r '.high_confidence // false' 2>/dev/null || echo "false")
TOP_ID=$(printf '%s' "$MATCH_JSON" | jq -r '.top[0].id // ""' 2>/dev/null || echo "")
TOP_SCORE=$(printf '%s' "$MATCH_JSON" | jq -r '.top[0].score // 0' 2>/dev/null || echo "0")
TOP_SIG=$(printf '%s' "$MATCH_JSON" | jq -r '.top[0].signature // ""' 2>/dev/null || echo "")
TOP_SNIP=$(printf '%s' "$MATCH_JSON" | jq -r '.top[0].snippet // ""' 2>/dev/null || echo "")
[ -z "$TOP_ID" ] && exit 0
# Trigger condition: (high_confidence==true OR score>=0.85) AND score>=0.6
# - high_confidence requires top1/top2 gap > 1.5 (set por fn match)
# - score>=0.85 cubre matches muy fuertes donde el gap es modesto
SCORE_HI=$(awk -v s="$TOP_SCORE" 'BEGIN{ print (s+0 >= 0.85) ? "1" : "0" }')
SCORE_MIN=$(awk -v s="$TOP_SCORE" 'BEGIN{ print (s+0 >= 0.6) ? "1" : "0" }')
[ "$SCORE_MIN" = "1" ] || exit 0
if [ "$HIGH_CONF" != "true" ] && [ "$SCORE_HI" != "1" ]; then
exit 0
fi
# Truncar snippet a 100 chars y limpiar saltos de linea
SNIP_SHORT=$(printf '%s' "$TOP_SNIP" | tr '\n' ' ' | head -c 100)
# Formatear score con 2 decimales
SCORE_FMT=$(awk -v s="$TOP_SCORE" 'BEGIN{ printf "%.2f", s+0 }')
# ---- Emitir <system-reminder> a stderr ----
cat >&2 <<EOF
<system-reminder>FUZZY-MATCH (issue 0087): your Bash command may already be a function.
USE: ./fn run $TOP_ID -> $TOP_SIG
SNIPPET: $SNIP_SHORT
Confidence: $SCORE_FMT. If you proceed inline, the violation will be logged.
</system-reminder>
EOF
exit 0
# Test manual:
# echo '{"tool_name":"Bash","tool_input":{"command":"taskkill.exe /IM registry_dashboard.exe /F"},"session_id":"test"}' \
# | bash .claude/scripts/hook_fn_match.sh
#
# Casos silenciosos:
# echo '{"tool_name":"Bash","tool_input":{"command":"ls -la"},"session_id":"test"}' \
# | bash .claude/scripts/hook_fn_match.sh
# echo '{"tool_name":"Bash","tool_input":{"command":"./fn run filter_slice_go_core 1 2 3"},"session_id":"test"}' \
# | bash .claude/scripts/hook_fn_match.sh
+38
View File
@@ -8,6 +8,44 @@ Para contexto detallado del trabajo diario ver `docs/diary/`. Para decisiones ar
## [Unreleased]
## 2026-05-14
### Added
- **Issue 0086 — Monitor tab del `registry_dashboard`** (sub-repo `dataforge/registry_dashboard`). Pestaña `Monitor` primera y por defecto del TabBar, landing del bucle reactivo construir->ejecutar->recopilar->analizar->mejorar.
- 7 KPIs (Calls / MCP / Reg % / Errors / Violations / Copies / Versions) filtradas por ventana temporal (1h/24h/7d/30d/All).
- Sub-tab `Recent Executions` con columnas When/Function/Tool/ms/OK/Error. Columna Function muestra `$ <snippet>` en gris cuando `function_id` vacio, hover tooltip con comando completo. Checkbox `Only registry functions` filtra por `function_id != ''`.
- Sub-tab `Failed Functions` (5a) — subset filtrado a registry-functions fallidas, columnas When/Function/Tool/Error class/Error snippet, function_id en rojo.
- Live scatter `duracion (ms)` vs `time`: eje X auto-scroll a `now`, ventana configurable (1m/5m/15m/1h/6h) independiente del filtro de KPIs, eje Y dinamico `0..max(visible)+500ms`. Hora local (`UseLocalTime`). Series ok/error en verde/rojo. Hover sobre punto = tooltip Function/Tool/Duration/Error.
- Indicador `live`/`offline` con timestamp del ultimo evento WS.
- **WebSocket live stream sqlite_api -> registry_dashboard** (sub-repo `dataforge/sqlite_api`). Endpoint `GET /api/events/call_monitor`. Hub global con subscribers; ticker arranca solo con >=1 subscriber (cero overhead si nadie mira). Cliente recibe snapshot inicial (KPIs + 100 ultimas filas + watermark) y luego deltas `id > watermark`. Cliente puede mandar `{watermark: N}` para resumir tras reconexion.
- **WS client C++** hand-rolled RFC6455 en `ws_client.{h,cpp}` (~330 LOC) en el dashboard. Localhost-only (no TLS). Thread propio, reconnect exponencial 0.5s->8s, FIN/text/ping/pong/close handling, queue thread-safe drenada cada frame.
- **Migration 007 `command_snippet` en `calls`** (`projects/fn_monitoring/apps/call_monitor/migrations/007_calls_command_snippet.sql`). Aditiva, idempotente. Llena por hook `hook_call_monitor.sh` solo cuando `function_id == ''`. Redactado de `password=`/`token=`/`secret=`/`api_key=`/`bearer=`. Truncado 200 chars.
- **Issue 0087 — Capability Discovery Acceleration**. Modelo 5 capas + 7 piezas (ver `dev/issues/0087-*.md`).
- **`fn match`** (`cmd/fn/match.go`) — subcommand fuzzy-FTS5 que dado un comando devuelve top-N funciones del registry candidates. Latencia 6-7ms. Output JSON con `score` (normalizado top=1.0) + `raw_score` (absoluto pre-normalizacion) + `high_confidence` gate (`raw_score >= 4.0 AND top1.raw/top2.raw > 1.5`).
- **`fn doctor capabilities --emit-claude-md`** (`cmd/fn/doctor.go` + `functions/infra/emit_capabilities_md.go`) — emite bloque markdown con secciones TOP 20 (por `calls_total`), Fresh 7d, Pipelines top 5. Fallback si `call_monitor.operations.db` ausente.
- **`call_monitor sequences --detect [--propose]`** (`projects/fn_monitoring/apps/call_monitor/sequences.go` + `migrations/006_function_sequences.sql`). Detecta secuencias A->B(->C) en `calls` (same session, gap < 30s, occ >= 5, sess >= 2, success_rate >= 0.9) y abre proposals `new_pipeline` automaticamente.
- **Hook `PreToolUse` `hook_fn_match.sh`** — denylist + `fn match` con timeout 0.2s. Inyecta `<system-reminder>FUZZY-MATCH: USE ./fn run <id>` cuando confidence alta. Latencia 113ms trigger / 32ms denylist. Registrado en `.claude/settings.local.json` (Bash matcher).
- **Hook `UserPromptSubmit` `hook_capabilities_inject.sh`** — cache 1h en `~/.cache/fn_registry/capabilities.txt`. Emite JSON `hookSpecificOutput.additionalContext` con linea compacta `CAPABILITIES: TOP / FRESH / PIPELINES`. Latencia cold 33ms / warm 18ms.
- **Timer systemd user** `call_monitor_sequences.timer` (OnCalendar 0/6h) + `.service` oneshot ejecutando `call_monitor sequences --detect --propose --report`. Versionado en `projects/fn_monitoring/apps/call_monitor/systemd/`.
- **3 funciones nuevas grupo `cpp-windows`** + pagina madre `docs/capabilities/cpp-windows.md`:
- `launch_cpp_app_windows_bash_infra``cmd.exe`/`PowerShell Start-Process` para lanzar exe en Windows desde WSL2.
- `is_cpp_app_running_windows_bash_infra``tasklist.exe /FI` con exit code 0/1 + stdout `RUNNING: PID=N MEM=K` o `NOT_RUNNING`.
- `redeploy_cpp_app_windows_bash_pipelines` — pipeline build? + deploy + launch + verify en 1 invocacion. Reemplaza ~6 commands manuales.
- **ADR 0004 `docs/adr/0004-telemetry-driven-capability-growth.md`** — formaliza el bucle telemetria -> proposal -> capability group -> discovery acceleration como motor de crecimiento del registry.
- **Regla `.claude/rules/function_growth_and_self_docs.md`** (entry #30 en `INDEX.md`) — contrato `.md` autosuficiente (Ejemplo + Cuando usarla + Gotchas + Growth log) + crecimiento del registry por promocion de composiciones, NO por inflado de funciones individuales.
### Changed
- **`.claude/CLAUDE.md` Norte ampliado** — 4o objetivo `PROMOVER COMPOSICIONES A PIPELINES` (el registry crece por composicion, no por inflado). Linea sobre auto-discovery zero-second-lookup.
- **`.claude/rules/registry_calls.md`** — clausula nueva: hooks e infraestructura de telemetria (`fn_match`, `fn doctor`, `call_monitor`) pueden leer `registry.db` directo con conexion read-only. NO sujeto a regla MCP-first (no son acciones del agente).
- **`/fn_claude` command** mejorado con objetivos del Monitor + interpretacion de `FUZZY-MATCH` hint + `CAPABILITIES` line + threshold semantica.
### Fixed
- **`launch_cpp_app_windows` quoting bug** — `cmd.exe /c "cd /d \"$dir\" && start ..."` rompia con paths Windows (el `\"` final se interpretaba como escape de comilla -> string sin cerrar -> "Windows cannot find \\"). Fix: reescribir a `powershell.exe -Command "Start-Process -FilePath ... -WorkingDirectory ..."` (single-quote PowerShell es literal, sin procesar `\` ni `$`).
- **`fn match high_confidence` siempre true** — debido a normalizacion `top=1.0`. Fix: añadir `raw_score` preservado pre-normalizacion + gate dual `raw_score >= 4.0 AND top1.raw/top2.raw > 1.5`. Threshold 4.0 tuneado contra 14 patrones del analysis `domain_coverage_gaps` (~93% precision).
## 2026-05-07
### Added
+1 -1
View File
@@ -7,7 +7,7 @@ version: "1.0.0"
purity: impure
signature: "build_cpp_windows(target?: string) -> void"
description: "Cross-compila las funciones y apps C++ del registry para Windows usando mingw-w64"
tags: [cpp, build, cmake, windows, cross-compile, mingw, imgui]
tags: [cpp, build, cmake, windows, cross-compile, mingw, imgui, cpp-windows]
uses_functions: []
uses_types: []
returns: []
@@ -7,7 +7,7 @@ version: "1.0.0"
purity: impure
signature: "deploy_cpp_exe_to_windows(app_name: string, app_dir: string) -> void"
description: "Copia el .exe de Windows (compilado por build_cpp_windows) y sus assets al escritorio de Windows /mnt/c/Users/lucas/Desktop/apps/<APP>/. Mata el proceso si esta corriendo (taskkill.exe pre-autorizado), copia DLLs, sincroniza assets/ y enrichers/ con rsync, maneja runtime Python embebido si python_runtime: true en app.md, y copia extras gx-cli. Preserva siempre local_files/ (estado del usuario)."
tags: [cpp, deploy, windows, exe, dll, assets, rsync]
tags: [cpp, deploy, windows, exe, dll, assets, rsync, cpp-windows]
uses_functions: []
uses_types: []
returns: []
+1 -1
View File
@@ -3,7 +3,7 @@ name: e2e_run_cpp_windows
lang: bash
domain: infra
description: "Cross-compila una app C++ del registry para Windows con mingw-w64, deploy al Desktop\\apps de Windows (matando instancia previa con taskkill.exe), lanza el .exe nativamente desde WSL y devuelve stdout + exit code. Pensado para tests headless tipo altsnap_jitter_test."
tags: [windows, e2e, cross-compile, test, mingw, pendiente-usar]
tags: [windows, e2e, cross-compile, test, mingw, pendiente-usar, cpp-windows]
purity: impure
kind: function
signature: "e2e_run_cpp_windows(target string, --no-build, --no-deploy) int"
@@ -0,0 +1,70 @@
---
name: is_cpp_app_running_windows
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "is_cpp_app_running_windows(app_name: string) -> void"
description: "Comprueba si un ejecutable Windows esta corriendo via tasklist.exe desde WSL2. Exit 0 si el proceso existe, exit 1 si no."
tags: [cpp, windows, monitoring, wsl, tasklist, process, cpp-windows]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/is_cpp_app_running_windows.sh"
params:
- name: app_name
desc: "Nombre del binario sin extension .exe (ej. registry_dashboard). Se le añade .exe internamente para el filtro de tasklist."
output: "Exit 0 con stdout 'RUNNING: PID=<pid> MEM=<mem>' si el proceso existe. Exit 1 con stdout 'NOT_RUNNING: <app_name>' si no. Exit 1 con stderr si tasklist.exe no esta disponible."
---
## Ejemplo
```bash
if is_cpp_app_running_windows "registry_dashboard"; then
echo "Ya esta corriendo"
else
launch_cpp_app_windows "registry_dashboard"
fi
```
Composicion tipica en pipelines de redeploy:
```bash
# Matar antes de copiar si esta vivo
if is_cpp_app_running_windows "$app"; then
taskkill.exe /IM "${app}.exe" /F
fi
deploy_cpp_exe_to_windows "$app" "$app_dir"
launch_cpp_app_windows "$app"
```
## Comportamiento
El exit code es la API principal; stdout es informacion adjunta parseable por el caller:
| Situacion | Exit | Stdout |
|---|---|---|
| Proceso encontrado | 0 | `RUNNING: PID=<n> MEM=<n>K` |
| Proceso no encontrado | 1 | `NOT_RUNNING: <app_name>` |
| `tasklist.exe` no disponible | 1 | (stderr) `ERROR: tasklist.exe no encontrado` |
| `app_name` vacio | 1 | (stderr) `ERROR: uso: ...` |
El formato `RUNNING: PID=N MEM=...K` esta pensado para ser parseado por `cut -d= -f2` si el caller necesita el PID.
## Prerequisitos
- WSL2 con acceso a herramientas Windows (`/mnt/c/Windows/System32/tasklist.exe`).
- El proceso debe haber sido lanzado via `launch_cpp_app_windows` o cualquier otro mecanismo que cree un proceso Windows con el nombre del `.exe`.
## Notas
Util para componer con `launch_cpp_app_windows` y `deploy_cpp_exe_to_windows` en pipelines de redeploy desde WSL2. `deploy_cpp_exe_to_windows` ya incluye un `taskkill.exe` interno antes de copiar, pero esta funcion permite tomar la decision de matar/no matar de forma explicita en el caller.
Tasklist con `/NH /FO CSV` no emite header, por lo que la primera linea de salida con el nombre de imagen es directamente el primer proceso coincidente. Si hay multiples instancias del mismo `.exe`, se informa solo del primero (PID mas bajo en el listado del sistema).
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# is_cpp_app_running_windows — Comprueba si un .exe de Windows esta corriendo
# via tasklist.exe desde WSL2. Exit 0 si vivo, exit 1 si no.
is_cpp_app_running_windows() {
local app_name="${1:-}"
if [ -z "$app_name" ]; then
echo "ERROR: uso: is_cpp_app_running_windows <app_name>" >&2
return 1
fi
# Verificar que tasklist.exe esta disponible
if ! command -v tasklist.exe >/dev/null 2>&1; then
echo "ERROR: tasklist.exe no encontrado (requiere WSL2 con acceso a Windows tools)" >&2
return 1
fi
# /NH: sin header, /FO CSV: salida CSV, /FI: filtro por imagen
local output
output=$(tasklist.exe /FI "IMAGENAME eq ${app_name}.exe" /NH /FO CSV 2>/dev/null)
# tasklist.exe devuelve "INFO: No tasks..." cuando no hay coincidencia.
# Con CSV+NH, cada proceso encontrado produce: "imagen.exe","pid","tipo","sesion","mem"
if echo "$output" | grep -qi "\"${app_name}.exe\""; then
# Extraer PID y memoria de la primera linea que coincida
local line
line=$(echo "$output" | grep -i "\"${app_name}.exe\"" | head -n1)
# Campos CSV: "imagen","PID","tipo","sesion","MEM K"
local pid mem
pid=$(echo "$line" | cut -d',' -f2 | tr -d '"')
mem=$(echo "$line" | cut -d',' -f5 | tr -d '"' | tr -d ' ')
echo "RUNNING: PID=${pid} MEM=${mem}"
return 0
else
echo "NOT_RUNNING: ${app_name}"
return 1
fi
}
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
is_cpp_app_running_windows "$@"
fi
@@ -0,0 +1,70 @@
---
name: launch_cpp_app_windows
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "launch_cpp_app_windows(app_name: string, [desktop_dir: string]) -> void"
description: "Lanza un binario .exe en Windows desde WSL2. Asume que deploy_cpp_exe_to_windows ya copió el exe a Desktop/apps/<app_name>/. Usa cmd.exe /c start para desacoplar el proceso y retornar inmediatamente."
tags: [cpp, windows, launch, wsl, exe, cpp-windows]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/launch_cpp_app_windows.sh"
params:
- name: app_name
desc: "Nombre de la app (ej: registry_dashboard). Localiza Desktop/apps/<app_name>/<app_name>.exe."
- name: desktop_dir
desc: "Override opcional del directorio escritorio Windows. Default: /mnt/c/Users/lucas/Desktop."
output: "Imprime 'OK: <app_name> launched at <ts>' en stdout si el exe existe y el comando se lanza. Errores fatales a stderr con exit 1."
---
## Ejemplo
```bash
source bash/functions/infra/launch_cpp_app_windows.sh
# Lanzar con default desktop dir
launch_cpp_app_windows "registry_dashboard"
# OK: registry_dashboard launched at 2026-05-14T10:32:01
# Override de desktop_dir (ej. otro usuario)
launch_cpp_app_windows "chart_demo" "/mnt/c/Users/otrouser/Desktop"
# OK: chart_demo launched at 2026-05-14T10:32:05
```
## Comportamiento
`cmd.exe /c start` es la clave: lanza el proceso en Windows y **retorna inmediatamente** sin esperar a que el exe termine. El proceso queda desacoplado del shell WSL2. Esta funcion **no verifica** que el exe arranco correctamente ni que sigue corriendo — esa responsabilidad recae en `is_cpp_app_running_windows` (funcion complementaria).
El `cd /d` previo al `start` es esencial: los apps C++ del registry buscan sus assets, `local_files/` y DLLs relativos al directorio de trabajo. Sin el `cd`, Windows buscaria desde `C:\Windows\System32` y el exe no encontraria nada.
## Prerequisitos
- **WSL2**: la funcion usa `wslpath -w` y `cmd.exe`, ambos solo disponibles en WSL2.
- **`/mnt/c/` montado**: el exe debe ser accesible via la ruta `/mnt/c/...`.
- **Exe ya copiado**: `deploy_cpp_exe_to_windows` debe haberse ejecutado antes. Esta funcion no compila ni copia nada.
## Notes
Mitad complementaria de `deploy_cpp_exe_to_windows_bash_infra`. El flujo completo para actualizar y relanzar una app es:
```bash
# 1. Compilar para Windows
build_cpp_windows "registry_dashboard"
# 2. Copiar al escritorio (mata proceso si corre, copia DLLs+assets)
deploy_cpp_exe_to_windows "registry_dashboard" "/home/lucas/fn_registry/apps/registry_dashboard"
# 3. Lanzar
launch_cpp_app_windows "registry_dashboard"
```
No se incluyen tests automatizados porque requieren entorno WSL2 con Windows activo y no son automatizables en CI.
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# launch_cpp_app_windows — Lanza un .exe en Windows desde WSL2 via cmd.exe /c start.
# Asume que el exe ya fue copiado por deploy_cpp_exe_to_windows al escritorio.
launch_cpp_app_windows() {
local app="${1:-}"
local desktop_dir="${2:-/mnt/c/Users/lucas/Desktop}"
if [ -z "$app" ]; then
echo "ERROR: uso: launch_cpp_app_windows <app_name> [desktop_dir]" >&2
return 1
fi
local exe_path="$desktop_dir/apps/$app/$app.exe"
if [ ! -f "$exe_path" ]; then
echo "ERROR: exe no encontrado: $exe_path" >&2
echo "Copia primero con: deploy_cpp_exe_to_windows $app <app_dir>" >&2
return 1
fi
# Usamos PowerShell en vez de cmd.exe — los backslashes de paths Windows
# no rompen el escaping aqui (cmd.exe `\"` interpreta como escape de
# comilla y deja el string sin cerrar; PowerShell single-quote es literal).
local win_app_dir win_exe
win_app_dir=$(wslpath -w "$desktop_dir/apps/$app")
win_exe="$win_app_dir\\$app.exe"
# Start-Process detacha (equivale a `start` de cmd) y respeta -WorkingDirectory.
# Las comillas simples en PowerShell son literales — no procesa \ ni $.
powershell.exe -NoProfile -Command \
"Start-Process -FilePath '$win_exe' -WorkingDirectory '$win_app_dir'" \
>/dev/null 2>&1
local ts
ts=$(date '+%Y-%m-%dT%H:%M:%S')
echo "OK: $app launched at $ts"
}
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
launch_cpp_app_windows "$@"
fi
+1 -1
View File
@@ -7,7 +7,7 @@ version: "1.0.0"
purity: impure
signature: "compile_cpp_app(app_name?: string) -> void"
description: "Pipeline que resuelve la app C++ desde el nombre o CWD, la cross-compila para Windows con mingw-w64, y despliega el .exe al escritorio de Windows. Composicion de resolve_cpp_app_dir + build_cpp_windows + deploy_cpp_exe_to_windows."
tags: [cpp, compile, windows, mingw, cross-compile, deploy, pipeline, pendiente-usar]
tags: [cpp, compile, windows, mingw, cross-compile, deploy, pipeline, pendiente-usar, cpp-windows]
uses_functions:
- resolve_cpp_app_dir_bash_infra
- build_cpp_windows_bash_infra
@@ -0,0 +1,75 @@
---
name: redeploy_cpp_app_windows
kind: pipeline
lang: bash
domain: pipelines
version: "1.0.0"
purity: impure
signature: "redeploy_cpp_app_windows(app_name: string, app_dir: string, [--build]) -> void"
description: "Pipeline orquestador para redeployar una app C++ en Windows desde WSL2 en un solo comando. Reemplaza la secuencia manual taskkill+copy+launch+verify."
tags: [cpp, windows, redeploy, pipeline, wsl, launcher, cpp-windows]
uses_functions:
- build_cpp_windows_bash_infra
- deploy_cpp_exe_to_windows_bash_infra
- launch_cpp_app_windows_bash_infra
- is_cpp_app_running_windows_bash_infra
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/pipelines/redeploy_cpp_app_windows.sh"
params:
- name: app_name
desc: "Nombre de la app C++ (ej: chart_demo, registry_dashboard). Se usa para localizar el .exe en cpp/build/windows/apps/<app>/ y el destino Desktop/apps/<app>/."
- name: app_dir
desc: "Ruta absoluta al directorio fuente de la app (ej: /home/lucas/fn_registry/cpp/apps/chart_demo). Requerido para localizar enrichers/, runtime/ y app.md."
- name: "--build"
desc: "Flag opcional. Si presente, compila la app para Windows antes del deploy. Por defecto off (asume .exe ya compilado)."
output: "Imprime 'OK: <app_name> redeployed (build=yes/no, PID=N)' en stdout. Exit 1 en cualquier paso fallido con mensaje de error indicando el paso."
---
## Ejemplo
```bash
# Solo redeploy (asume build ya hecho)
redeploy_cpp_app_windows "registry_dashboard" "/home/lucas/fn_registry/projects/fn_monitoring/apps/registry_dashboard"
# Con build previo
redeploy_cpp_app_windows "chart_demo" "/home/lucas/fn_registry/cpp/apps/chart_demo" --build
```
## Comportamiento
1. **Parsear flag `--build`** (default off, opt-in).
2. **Si `--build`**: invocar `build_cpp_windows <app_name>` para compilar `cpp/build/windows/apps/<app_name>/<app_name>.exe`. Si falla, exit 1 sin tocar el Desktop.
3. **Deploy**: invocar `deploy_cpp_exe_to_windows "<app_name>" "<app_dir>"`. Esta función mata el proceso si está vivo (taskkill.exe pre-autorizado), copia exe + DLLs + assets + runtime + enrichers, y preserva `local_files/`.
4. **Launch**: invocar `launch_cpp_app_windows "<app_name>"` para arrancar la app en Windows.
5. **Wait**: `sleep 1` — espera arranque corto.
6. **Verify**: invocar `is_cpp_app_running_windows "<app_name>"`. Si NO está vivo → exit 1 con mensaje claro.
7. **Stdout final**: `OK: <app_name> redeployed (build=yes/no, PID=N)`.
## Argumentos
- `app_name` — obligatorio. Nombre corto de la app.
- `app_dir` — obligatorio. Ruta absoluta al directorio fuente (donde vive `app.md`, `enrichers/`, `runtime/`).
- `--build` — opcional. Activa la compilación antes del deploy.
## Errores
Cada paso es punto de fallo independiente. En caso de error el pipeline imprime a stderr indicando qué paso falló y hace exit 1:
- `ERROR [build]: build_cpp_windows falló` — fallo de compilación.
- `ERROR [deploy]: deploy_cpp_exe_to_windows falló` — fallo al copiar archivos.
- `ERROR [launch]: launch_cpp_app_windows falló` — la app no arrancó.
- `ERROR [verify]: <app> no está corriendo` — la app arrancó pero murió antes de 1s.
## Notas
- Ahorra ~6 comandos manuales por redeploy (taskkill + cp + cd + start + sleep + tasklist). Motivación: issue 0086 — maximizar `Reg %` registrando esta secuencia como pipeline trazable.
- Cada `fn run redeploy_cpp_app_windows_bash_pipelines` queda registrado en `call_monitor/operations.db`, alimentando las métricas del bucle reactivo.
- `launch_cpp_app_windows_bash_infra` e `is_cpp_app_running_windows_bash_infra` son creadas por un fn-constructor paralelo. Si aún no existen en el registry al indexar, el indexer lo registra como advertencia pero el pipeline es válido structuralmente.
- Tag `launcher` incluido: este pipeline tiene sentido en el Pipeline Launcher TUI (`apps/pipeline_launcher`) — es un workflow interactivo lanzable con argumentos.
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
# redeploy_cpp_app_windows — Pipeline orquestador: build (opcional) + deploy + launch + verify
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../infra/build_cpp_windows.sh"
source "$SCRIPT_DIR/../infra/deploy_cpp_exe_to_windows.sh"
source "$SCRIPT_DIR/../infra/launch_cpp_app_windows.sh"
source "$SCRIPT_DIR/../infra/is_cpp_app_running_windows.sh"
redeploy_cpp_app_windows() {
local app_name=""
local app_dir=""
local do_build=0
# Parsear argumentos posicionales y flags
while [[ $# -gt 0 ]]; do
case "$1" in
--build)
do_build=1
shift
;;
-*)
echo "redeploy_cpp_app_windows: flag desconocido: $1" >&2
return 1
;;
*)
if [[ -z "$app_name" ]]; then
app_name="$1"
elif [[ -z "$app_dir" ]]; then
app_dir="$1"
else
echo "redeploy_cpp_app_windows: argumento inesperado: $1" >&2
return 1
fi
shift
;;
esac
done
if [[ -z "$app_name" || -z "$app_dir" ]]; then
echo "redeploy_cpp_app_windows: uso: redeploy_cpp_app_windows <app_name> <app_dir> [--build]" >&2
return 1
fi
# Paso 1: build opcional
if [[ $do_build -eq 1 ]]; then
echo "[1/4] Building $app_name for Windows..."
if ! build_cpp_windows "$app_name"; then
echo "ERROR [build]: build_cpp_windows falló para '$app_name'" >&2
return 1
fi
echo "[1/4] Build OK"
else
echo "[1/4] Build skipped (--build no especificado)"
fi
# Paso 2: deploy (taskkill + copy exe/DLLs/assets/runtime + preserva local_files)
echo "[2/4] Deploying $app_name to Windows Desktop..."
if ! deploy_cpp_exe_to_windows "$app_name" "$app_dir"; then
echo "ERROR [deploy]: deploy_cpp_exe_to_windows falló para '$app_name'" >&2
return 1
fi
echo "[2/4] Deploy OK"
# Paso 3: lanzar la app
echo "[3/4] Launching $app_name..."
if ! launch_cpp_app_windows "$app_name"; then
echo "ERROR [launch]: launch_cpp_app_windows falló para '$app_name'" >&2
return 1
fi
echo "[3/4] Launch OK"
# Paso 4: esperar arranque y verificar
sleep 1
echo "[4/4] Verifying $app_name is running..."
local pid=""
if ! pid=$(is_cpp_app_running_windows "$app_name"); then
echo "ERROR [verify]: $app_name no está corriendo tras el lanzamiento. Revisa logs en Desktop/apps/$app_name/." >&2
return 1
fi
echo "[4/4] Running OK"
local build_label="no"
[[ $do_build -eq 1 ]] && build_label="yes"
echo "OK: $app_name redeployed (build=$build_label, PID=$pid)"
}
# Ejecutar si se llama directamente (fn run lo invoca como script)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
redeploy_cpp_app_windows "$@"
fi
+19 -2
View File
@@ -12,11 +12,14 @@ import (
func cmdDoctor(args []string) {
jsonOut := false
emitClaudeMd := false
sub := ""
for _, a := range args {
switch a {
case "--json":
jsonOut = true
case "--emit-claude-md":
emitClaudeMd = true
case "-h", "--help":
doctorUsage()
return
@@ -51,7 +54,11 @@ func cmdDoctor(args []string) {
case "copied-code":
doctorCopiedCode(r, jsonOut)
case "capabilities":
doctorCapabilities(r, jsonOut)
if emitClaudeMd {
doctorCapabilitiesEmitMd(r)
} else {
doctorCapabilities(r, jsonOut)
}
default:
fmt.Fprintf(os.Stderr, "unknown doctor subcommand: %s\n", sub)
doctorUsage()
@@ -79,7 +86,8 @@ Subcommands:
capabilities Drift entre docs/capabilities/INDEX.md, tags de funciones, y paginas <grupo>.md (issue 0086)
Flags:
--json Salida JSON (para scripting/agentes)`)
--json Salida JSON (para scripting/agentes)
--emit-claude-md (solo capabilities) Genera bloque markdown para CLAUDE.md`)
}
func doctorAll(root string, jsonOut bool) {
@@ -432,6 +440,15 @@ func doctorCapabilities(root string, jsonOut bool) {
fmt.Printf("\n%d/%d capability groups healthy.\n", len(audits)-bad, len(audits))
}
func doctorCapabilitiesEmitMd(root string) {
result, err := infra.EmitCapabilitiesMd(root)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
fmt.Print(infra.RenderCapabilitiesMd(result))
}
func doctorCopiedCode(root string, jsonOut bool) {
entries, err := infra.AuditCopiedCode(root)
if err != nil {
+5 -1
View File
@@ -49,6 +49,8 @@ func main() {
cmdVault(os.Args[2:])
case "doctor":
cmdDoctor(os.Args[2:])
case "match":
cmdMatch(os.Args[2:])
case "help", "-h", "--help":
printUsage()
default:
@@ -77,7 +79,9 @@ Usage:
fn sync [status|locations] Sincroniza con servidor central
fn vault <list|search|index|info> Gestiona y busca en data vaults
fn doctor [artefacts|services|sync|uses-functions|unused] [--json]
Diagnostico read-only del registry`)
Diagnostico read-only del registry
fn match [--top N] [--format json|text] [--min-score F] "<cmd>"
Fuzzy match entre comando shell y funciones del registry`)
}
func root() string {
+553
View File
@@ -0,0 +1,553 @@
package main
import (
"database/sql"
"encoding/json"
"fmt"
"math"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"unicode"
_ "github.com/mattn/go-sqlite3"
)
// matchResult holds one candidate function match.
type matchResult struct {
ID string `json:"id"`
Score float64 `json:"score"` // normalized (top=1.0)
RawScore float64 `json:"raw_score"` // absolute, pre-normalization. Use for confidence gates.
Signature string `json:"signature"`
Snippet string `json:"snippet"`
Lang string `json:"-"`
Name string `json:"-"`
Tags string `json:"-"`
HighConfidence bool `json:"-"` // filled after ranking
}
// matchOutput is the JSON envelope returned by fn match.
type matchOutput struct {
Query string `json:"query"`
Top []matchResult `json:"top"`
HighConfidence bool `json:"high_confidence"`
}
// fts5Row is a raw row from the FTS query.
type fts5Row struct {
id string
name string
lang string
signature string
description string
tags string
rank float64
}
// --- tokenizer ---------------------------------------------------------
var (
reNonAlnum = regexp.MustCompile(`[^a-zA-Z0-9]+`)
reFlag = regexp.MustCompile(`^-{1,2}[a-zA-Z]`)
reAbsPath = regexp.MustCompile(`^(/|[A-Za-z]:\\|\\\\)`)
rePureNumber = regexp.MustCompile(`^\d+$`)
)
// domainStopwords are tokens so generic in this codebase that they add noise
// rather than signal to the matcher (they match hundreds of functions equally).
var domainStopwords = map[string]bool{
"registry": true, "function": true, "functions": true,
"app": true, "apps": true, "file": true, "files": true,
"get": true, "set": true, "run": true, "list": true, "add": true,
"new": true, "all": true, "the": true, "and": true, "for": true,
"use": true, "fmt": true, "log": true, "err": true, "nil": true,
"true": true, "false": true, "var": true, "val": true, "str": true,
"tmp": true, "out": true, "src": true, "dst": true, "opt": true,
"usr": true, "etc": true, "bin": true, "lib": true, "mnt": true,
"home": true, "root": true, "host": true, "user": true, "name": true,
"path": true, "type": true, "data": true, "info": true, "init": true,
"main": true, "test": true, "util": true, "base": true, "core": true,
"api": true, "url": true, "uri": true, "http": true, "html": true,
"json": true, "yaml": true, "toml": true, "conf": true, "config": true,
"dir": true, "map": true, "key": true, "obj": true,
"ctx": true, "pkg": true, "mod": true, "cmd": true, "cli": true,
"help": true, "read": true, "open": true, "close": true, "stop": true,
"start": true, "end": true, "begin": true, "done": true, "make": true,
"build": true, "check": true, "scan": true, "load": true, "save": true,
"send": true, "recv": true, "show": true, "print": true, "write": true,
"create": true, "update": true, "delete": true, "remove": true,
"desktop": true, "lucas": true, "windows": true, "linux": true,
}
// tokenize splits a shell command into significant lowercase tokens.
// It discards flags, absolute paths (keeping basenames), pure numbers,
// and short tokens (< 3 chars).
func tokenize(cmd string) []string {
// Replace common shell operators with spaces so they act as separators
cmd = strings.NewReplacer("|", " ", ";", " ", "&&", " ", "||", " ",
"(", " ", ")", " ", "{", " ", "}", " ").Replace(cmd)
parts := strings.Fields(cmd)
seen := map[string]bool{}
var tokens []string
for _, p := range parts {
// Skip flags like -v, --port, /F, /IM
if reFlag.MatchString(p) || (len(p) > 1 && p[0] == '/') {
continue
}
// Handle paths: keep only basename without extension
if reAbsPath.MatchString(p) || strings.ContainsAny(p, "/\\") {
p = filepath.Base(p)
ext := filepath.Ext(p)
if ext != "" {
p = strings.TrimSuffix(p, ext)
// also add ext without dot
extTok := strings.ToLower(strings.TrimPrefix(ext, "."))
if len(extTok) >= 3 && !seen[extTok] {
seen[extTok] = true
tokens = append(tokens, extTok)
}
}
}
// Split remaining by non-alphanumeric chars
subparts := reNonAlnum.Split(p, -1)
for _, sp := range subparts {
tok := strings.ToLower(sp)
if len(tok) < 3 {
continue
}
if rePureNumber.MatchString(tok) {
continue
}
if seen[tok] {
continue
}
if domainStopwords[tok] {
continue
}
seen[tok] = true
tokens = append(tokens, tok)
}
}
return tokens
}
// buildFTSQuery constructs a safe FTS5 OR query from tokens.
// Tokens with special FTS5 characters are wrapped in double quotes.
func buildFTSQuery(tokens []string) string {
if len(tokens) == 0 {
return ""
}
var parts []string
specialChars := `"'()^*:-.`
for _, tok := range tokens {
needsQuoting := false
for _, c := range tok {
if strings.ContainsRune(specialChars, c) {
needsQuoting = true
break
}
}
if needsQuoting {
// escape inner double quotes
escaped := strings.ReplaceAll(tok, `"`, `""`)
parts = append(parts, `"`+escaped+`"`)
} else {
parts = append(parts, tok)
}
}
return strings.Join(parts, " OR ")
}
// --- language penalty heuristics ---------------------------------------
// pythonMarkers are tokens that strongly suggest Python code.
var pythonMarkers = map[string]bool{
"def": true, "import": true, "class": true, "elif": true,
"self": true, "lambda": true, "yield": true, "async": true,
"await": true, "with": true,
}
// bashMarkers are tokens that strongly suggest Bash code.
var bashMarkers = map[string]bool{
"chmod": true, "chown": true, "grep": true, "awk": true,
"sed": true, "curl": true, "wget": true, "ssh": true,
"rsync": true, "systemctl": true, "apt": true, "yum": true,
"taskkill": true, "cmd": true, "powershell": true,
"exe": true, "bat": true,
}
func hasPythonMarkers(tokens []string) bool {
for _, t := range tokens {
if pythonMarkers[t] {
return true
}
}
return false
}
func hasBashMarkers(tokens []string) bool {
for _, t := range tokens {
if bashMarkers[t] {
return true
}
}
return false
}
// --- scoring -----------------------------------------------------------
// scoreHit computes a composite score for a single FTS5 hit.
// bm25 from SQLite is negative (more negative = better match).
// Scoring uses an additive boost model: each token that matches a field
// contributes a flat bonus (name=3.0, tags=2.0, signature=1.5). The total
// bonus is added to the base BM25 score, not multiplied per-token. This
// prevents runaway clamping when many tokens all match different functions
// equally (dashboard + registry + exe → all score 1.0 with the old model).
func scoreHit(row fts5Row, tokens []string, hasPython, hasBash bool) float64 {
// Base score from BM25 rank (negative -> positive, bounded [0,1])
base := 1.0 / (1.0 + math.Abs(row.rank))
nameLower := strings.ToLower(row.name)
tagsLower := strings.ToLower(row.tags)
sigLower := strings.ToLower(row.signature)
descLower := strings.ToLower(row.description)
var boost float64
for _, tok := range tokens {
// Use best-field bonus per token (additive across tokens, not multiplicative)
tokBoost := 0.0
if strings.Contains(nameLower, tok) && tokBoost < 3.0 {
tokBoost = 3.0
}
if strings.Contains(tagsLower, tok) && tokBoost < 2.0 {
tokBoost = 2.0
}
if strings.Contains(sigLower, tok) && tokBoost < 1.5 {
tokBoost = 1.5
}
if strings.Contains(descLower, tok) && tokBoost < 1.0 {
tokBoost = 1.0
}
boost += tokBoost
}
// Language penalties (applied to total, not per-token)
penalty := 1.0
langLower := strings.ToLower(row.lang)
if hasPython && langLower == "bash" {
penalty = 0.5
}
if hasBash && langLower == "py" {
penalty = 0.5
}
// No clamping — scores differentiate via normalisation in the caller
return (base + boost) * penalty
}
// snippet returns the first ~120 chars of description, trimmed cleanly.
func snippet(description string, maxLen int) string {
description = strings.Map(func(r rune) rune {
if unicode.IsControl(r) && r != '\t' {
return ' '
}
return r
}, description)
description = strings.TrimSpace(description)
if len(description) <= maxLen {
return description
}
// Cut at last space before maxLen
cut := description[:maxLen]
if idx := strings.LastIndex(cut, " "); idx > maxLen/2 {
cut = cut[:idx]
}
return cut + "..."
}
// --- FTS5 query --------------------------------------------------------
// ftsOnlyQuery returns id + rank from the FTS virtual table only.
// bm25() must be used without JOIN — it only works in direct FTS queries.
const ftsOnlyQuery = `
SELECT id, bm25(functions_fts) AS rank
FROM functions_fts
WHERE functions_fts MATCH ?
ORDER BY rank
LIMIT 50
`
// fnDetailQuery fetches metadata for a list of IDs.
const fnDetailQuery = `
SELECT id, name, lang, signature, description, COALESCE(tags, '[]')
FROM functions
WHERE id IN (%s)
`
func runMatch(dbPath string, query string, topN int, minScore float64) ([]matchResult, error) {
tokens := tokenize(query)
if len(tokens) == 0 {
return nil, fmt.Errorf("no significant tokens extracted from: %q", query)
}
ftsQ := buildFTSQuery(tokens)
if ftsQ == "" {
return nil, fmt.Errorf("could not build FTS query")
}
// Open normally (not strict read-only) so WAL frames are visible.
// bm25() with mode=ro fails with "missing row from content table" when
// the WAL has not been checkpointed — the FTS index references rows that
// aren't in the main db file yet. We never write anything here.
conn, err := sql.Open("sqlite3", dbPath)
if err != nil {
return nil, fmt.Errorf("opening db: %w", err)
}
defer conn.Close()
// Step 1: FTS-only query to get ids + bm25 ranks (no JOIN)
ftsRows, err := conn.Query(ftsOnlyQuery, ftsQ)
if err != nil {
return nil, fmt.Errorf("fts query: %w", err)
}
type idRank struct {
id string
rank float64
}
var ranked []idRank
for ftsRows.Next() {
var r idRank
if err := ftsRows.Scan(&r.id, &r.rank); err != nil {
continue
}
ranked = append(ranked, r)
}
ftsRows.Close()
if len(ranked) == 0 {
return nil, nil
}
// Step 2: fetch metadata for those IDs with a regular SELECT
rankMap := make(map[string]float64, len(ranked))
ids := make([]string, 0, len(ranked))
placeholders := make([]string, 0, len(ranked))
args := make([]any, 0, len(ranked))
for _, r := range ranked {
rankMap[r.id] = r.rank
ids = append(ids, r.id)
placeholders = append(placeholders, "?")
args = append(args, r.id)
}
detailSQL := fmt.Sprintf(fnDetailQuery, strings.Join(placeholders, ","))
detailRows, err := conn.Query(detailSQL, args...)
if err != nil {
return nil, fmt.Errorf("detail query: %w", err)
}
defer detailRows.Close()
hasPython := hasPythonMarkers(tokens)
hasBash := hasBashMarkers(tokens)
var results []matchResult
for detailRows.Next() {
var r fts5Row
if err := detailRows.Scan(&r.id, &r.name, &r.lang, &r.signature, &r.description, &r.tags); err != nil {
continue
}
r.rank = rankMap[r.id]
score := scoreHit(r, tokens, hasPython, hasBash)
results = append(results, matchResult{
ID: r.id,
Score: score, // rounded after normalisation below
Signature: r.signature,
Snippet: snippet(r.description, 120),
Lang: r.lang,
Name: r.name,
Tags: r.tags,
})
}
// Sort by score descending
sort.Slice(results, func(i, j int) bool {
return results[i].Score > results[j].Score
})
// Preserva raw_score (absoluto) ANTES de normalizar — sirve para gates
// de confidence absoluto. La normalizacion estetica enmascara queries
// debiles donde el top hit es solo el "menos malo" pero realmente no
// matchea — sin raw, high_confidence sobre normalized siempre dispara.
for i := range results {
results[i].RawScore = results[i].Score
}
// Normalise scores so the top result is 1.0 and the rest are relative.
// This makes the output stable and meaningful regardless of token count.
if len(results) > 0 && results[0].Score > 0 {
maxScore := results[0].Score
for i := range results {
results[i].Score = math.Round((results[i].Score/maxScore)*1000) / 1000
}
}
// Filter by min score
var filtered []matchResult
for _, r := range results {
if r.Score >= minScore {
filtered = append(filtered, r)
}
}
// Limit to topN
if len(filtered) > topN {
filtered = filtered[:topN]
}
return filtered, nil
}
// --- command -----------------------------------------------------------
func cmdMatch(args []string) {
topN := 3
format := "json"
minScore := 0.3
var queryArg string
for i := 0; i < len(args); i++ {
switch args[i] {
case "--top", "-n":
i++
if i < len(args) {
if n, err := strconv.Atoi(args[i]); err == nil && n > 0 {
topN = n
}
}
case "--format", "-f":
i++
if i < len(args) {
format = args[i]
}
case "--min-score":
i++
if i < len(args) {
if f, err := strconv.ParseFloat(args[i], 64); err == nil {
minScore = f
}
}
case "--help", "-h":
fmt.Println(`fn match — fuzzy matcher between a shell command and registry functions
Usage:
fn match [--top N] [--format json|text] [--min-score F] "<command>"
echo "<command>" | fn match [--top N] [--format json|text] [--min-score F]
Flags:
--top N Return top N results (default: 3)
--format Output format: json (default) or text
--min-score F Minimum score threshold 0..1 (default: 0.3)
Example:
fn match "taskkill.exe /IM registry_dashboard.exe /F"
fn match --top 5 --format text "curl -sf https://api.example.com/health"
echo "rsync -avz --exclude .git src/ user@host:/opt/app" | fn match`)
return
default:
if !strings.HasPrefix(args[i], "-") {
queryArg = args[i]
}
}
}
// Try stdin if no positional arg
if queryArg == "" {
stat, err := os.Stdin.Stat()
if err == nil && (stat.Mode()&os.ModeCharDevice) == 0 {
var sb strings.Builder
buf := make([]byte, 4096)
for {
n, err := os.Stdin.Read(buf)
if n > 0 {
sb.Write(buf[:n])
}
if err != nil {
break
}
}
queryArg = strings.TrimSpace(sb.String())
}
}
if queryArg == "" {
fmt.Fprintln(os.Stderr, "fn match: no command provided. Use --help for usage.")
os.Exit(1)
}
dbPath := filepath.Join(root(), dbName)
hits, err := runMatch(dbPath, queryArg, topN, minScore)
if err != nil {
fmt.Fprintf(os.Stderr, "fn match: %v\n", err)
os.Exit(1)
}
// Compute high_confidence flag. Doble gate:
// 1. RAW score >= 3.0 — al menos un token con match fuerte de campo
// (name=3.0 / tags=2.0 / signature=1.5 / description=1.0). Sin esto,
// la normalizacion devolveria 1.0 incluso para queries que no
// matchean nada bien (ej. "kelly criterion" -> graph_renderer score
// raw < 1.0 pero normalized = 1.0).
// 2. Gap top1/top2 > 1.5 (en raw, no normalized) — el top destaca
// sobre el siguiente, no es un cluster de matches mediocres.
const minRawForHighConf = 4.0
highConf := false
if len(hits) >= 1 && hits[0].RawScore >= minRawForHighConf {
if len(hits) >= 2 && hits[1].RawScore > 0 {
highConf = hits[0].RawScore/hits[1].RawScore > 1.5
} else {
highConf = true // solo un hit con raw alta
}
}
switch format {
case "text":
printMatchText(queryArg, hits, highConf)
default:
printMatchJSON(queryArg, hits, highConf)
}
}
func printMatchJSON(query string, hits []matchResult, highConf bool) {
out := matchOutput{
Query: query,
Top: hits,
HighConfidence: highConf,
}
if out.Top == nil {
out.Top = []matchResult{}
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
enc.Encode(out)
}
func printMatchText(query string, hits []matchResult, highConf bool) {
conf := ""
if highConf {
conf = " [HIGH CONFIDENCE]"
}
fmt.Printf("TOP MATCHES for: %s%s\n", query, conf)
if len(hits) == 0 {
fmt.Println(" (no matches above threshold)")
return
}
for _, h := range hits {
fmt.Printf(" [%.3f] %s\n", h.Score, h.ID)
fmt.Printf(" %s\n", h.Signature)
fmt.Printf(" %s\n", h.Snippet)
}
}
@@ -0,0 +1,101 @@
# 0087 — Capability Discovery Acceleration
**Status:** open
**Created:** 2026-05-14
**Related:** 0085 (telemetry), 0086 (delegation + capability groups)
## Problema
Crear funciones para reutilizar = bueno largo plazo, pero introduce un **coste de descubrimiento** cada sesion: Claude tiene que FTS-buscar antes de usar. Si la busqueda falla o es mediocre, Claude reinventa inline ("violation: reinvent_inline"). En la sesion del 2026-05-13/14 Claude reinvento taskkill+cp+start 5 veces seguidas pese a que `deploy_cpp_exe_to_windows` ya existia.
Objetivo: que **conocer las capacidades sea gratis o casi gratis** para Claude.
## Modelo de capas
Cinco capas escalonadas por coste de lookup. Cada una cubre un slice de casos:
| Capa | Coste lookup | Cobertura | Mecanismo |
|---|---|---|---|
| 1 | 0 (contexto base) | Top 20 funciones mas usadas + funciones creadas ultimos 7d | Bloque auto-generado en `CLAUDE.md` |
| 2 | 0 (cada turno) | Funciones recien creadas + violations recientes | Hook `UserPromptSubmit` injecta linea `FRESH:` / `TOP:` |
| 3 | 0 (mid-flight) | Patrones inline que matchean funcion existente | Hook `PreToolUse` con fuzzy FTS5 |
| 4 | 1 read on demand | Cualquier dominio especifico | `docs/capabilities/<group>.md` cargado solo si tarea matchea |
| 5 | 0 (persistente) | Insights cross-session | `MEMORY.md` linea por capacidad clave |
## Piezas (7)
| # | Pieza | Lang/Tipo | Aprox LOC | Tanda |
|---|---|---|---|---|
| 1 | `fn doctor capabilities --emit-claude-md` — top 20 + fresh 7d | Go subcommand | 80 | A |
| 2 | Hook `UserPromptSubmit` → linea `FRESH:` + `TOP:` | bash | 40 | B |
| 3 | `fn_match` binario fuzzy FTS5 sobre functions (input: command, output: top-N matches) | Go binary | 120 | A |
| 4 | Hook `PreToolUse` → consume `fn_match`, inyecta `USE:` hint si confidence alta | bash | 60 | B |
| 5 | `ids_naming.md` endurecer + validator en `fn_create_function` | Go + rules | 50 | A |
| 6 | `/fn_claude` post-creation auto-append `[[fn_id]] — purpose` a `MEMORY.md` | bash skill | 30 | C |
| 7 | Excepcion explicita en `registry_calls.md`: hooks pueden leer `registry.db` directo | rules | — | A |
### Fuzzy matching (pieza 3 + 4)
`fn_match`:
- **Input:** command string + optional tool context (Bash / Edit / heredoc).
- **Pipeline:**
1. Tokenizar comando: split en palabras, ignora flags (`-v`, `/F`), conserva tokens significativos.
2. Query FTS5 sobre `functions_fts MATCH 'tok1 OR tok2 OR ...'` ordenado por `bm25()`.
3. Re-score con weights: `name` match peso x3, `tags` peso x2, `description` peso x1.
4. Threshold dinamico: `top.score / second.score > 1.5` → confidence alta.
- **Output JSON:** `[{id, score, signature, snippet}, ...]` top 3.
- **Latencia objetivo:** < 50ms (hook PreToolUse no puede bloquear).
Hook `PreToolUse`:
- Captura tool + payload.
- Llama `fn_match` con timeout 200ms (no bloquea si tarda).
- Si confidence alta → inyecta `<system-reminder>FUZZY-MATCH: USE \`./fn run <id>\` instead. Signature: <sig>.</system-reminder>` en stderr (Claude lo lee).
- Si confidence baja → silencio.
### Excepcion hooks (pieza 7)
`.claude/rules/registry_calls.md` actualmente dice "NUNCA sqlite3 registry.db directo" pero esa regla apunta al AGENTE. Los hooks son lectores externos del proceso de Claude — necesitan acceso directo y rapido a FTS5 sin pasar por MCP (que requiere tool invocation por Claude). Anadir clausula:
> **Hooks (PreToolUse/PostToolUse/UserPromptSubmit) pueden leer `registry.db` directo via `sqlite3` o binario Go con read-only conn.** No estan sujetos a la regla MCP-first porque no son acciones del agente, son inspeccion automatizada del entorno. Excepcion: SOLO lectura. NUNCA escritura desde hooks.
## Plan de tandas (paralelizable)
### Tanda A — cero dependencias, lanzar en paralelo
- Pieza 1: `fn doctor capabilities --emit-claude-md` (Go).
- Pieza 3: `fn_match` binario (Go).
- Pieza 5: `ids_naming` validator + endurecer (Go + rules).
- Pieza 7: clausula en `registry_calls.md` (rules, instant).
### Tanda B — depende de tanda A (consume binarios)
- Pieza 2: hook `UserPromptSubmit` consume `fn doctor capabilities` output.
- Pieza 4: hook `PreToolUse` consume `fn_match`.
### Tanda C — refuerzos
- Pieza 6: extension de `/fn_claude` skill.
## Acceptance criteria
- [ ] `fn doctor capabilities --emit-claude-md` imprime bloque markdown valido pegable en CLAUDE.md.
- [ ] Hook `UserPromptSubmit` añade linea `FRESH: <id1>, <id2>, ...` cada turno (max 5 funciones de los ultimos 7d).
- [ ] `fn_match "taskkill registry_dashboard.exe"` devuelve `deploy_cpp_exe_to_windows_bash_infra` con score > threshold en < 50ms.
- [ ] Hook `PreToolUse` con `fn_match` muestra hint `USE: ...` en al menos 3 patrones reproducibles (taskkill, cp Desktop, cmd.exe start).
- [ ] Validator de `fn_create_function` rechaza nombres no predictibles (sin verbo o sin dominio).
- [ ] `/fn_claude` tras crear N funciones, MEMORY.md tiene N nuevas lineas `[[id]] — purpose`.
- [ ] Reglas: `registry_calls.md` documenta la excepcion explicita para hooks.
## Metricas de exito
Antes / despues, ventana 7d:
- `Reg %` (% calls con function_id != '') — esperado +10pp.
- `violations` count — esperado -50%.
- Tiempo medio entre "patron inline" y "uso de funcion correspondiente" — esperado dia 1 → mismo turno.
Visibles en Monitor tab del `registry_dashboard`.
## Notas
- Capa 3 (fuzzy interceptor) es la mas potente pero requiere **benchmark** de latencia antes de habilitar en cada Bash. Si supera 100ms, degradar a "solo en comandos > N chars" o "solo al final del turno".
- No introducir mas reglas: las que hay (`registry_first.md`, `delegation.md`, `registry_calls.md`) son suficientes. Esto es **infraestructura para que se cumplan**.
@@ -0,0 +1,83 @@
# ADR 0004 — Telemetria de ejecuciones de Claude como motor de crecimiento del registry
- **Fecha:** 2026-05-14
- **Estado:** accepted
- **Relacionados:** issue 0085 (telemetry + call_monitor), issue 0086 (delegation + capability groups), issue 0087 (capability discovery acceleration)
## Contexto
Hasta 0085, el registry crecia "manualmente": el humano (o Claude bajo supervision) detectaba patrones repetidos, redactaba proposals y un agente los implementaba. Costes observados en sesiones reales:
- Claude reinventaba inline logica que ya existia como funcion. Caso reciente (2026-05-13/14): `taskkill + cp Desktop + cmd /c start` ejecutado 5 veces seguidas pese a que `deploy_cpp_exe_to_windows_bash_infra` ya cubria el caso.
- Funciones recien creadas quedaban huerfanas (`calls_90d=0`) porque Claude no las encontraba via FTS5 con queries genericas.
- Tres pasos del bucle reactivo (`MEJORAR → APROBAR → CONSTRUIR`) requerian humano en el loop incluso para gaps obvios.
Sin medir lo que el agente realmente hace, las reglas (`registry_first.md`, `delegation.md`) son aspiracion no auditable. **No se puede mejorar lo que no se mide.**
## Decision
Tratar las **ejecuciones de Claude como senal de primera clase** del registry. Tres componentes encadenados:
1. **Capturar todo** (issue 0085) — Hook `PostToolUse` parsea cada Bash + cada `mcp__registry__*` y persiste en `projects/fn_monitoring/apps/call_monitor/operations.db`. Tablas: `calls`, `code_writes`, `test_runs`, `e2e_runs_fn`, `violations`, `patterns`, `sessions`. Vista `function_stats` agrega por `function_id`.
2. **Agrupar por capability** (issue 0086) — Tags planos (`notebook`, `metabase`, `deploy`, `cpp-windows`, ...) sobre funciones afines + pagina madre `docs/capabilities/<grupo>.md` con lista, ejemplo canonico y fronteras. Cargar un grupo cuesta **1 read**, no N busquedas FTS5.
3. **Acelerar descubrimiento** (issue 0087) — 5 capas escalonadas por coste de lookup:
- Top-20 + fresh-7d en bloque autogenerado de CLAUDE.md (coste 0, contexto base).
- Hook `UserPromptSubmit` injecta `FRESH:` / `TOP:` cada turno (coste 0).
- Hook `PreToolUse` con `fn_match` fuzzy FTS5 sugiere `USE: ./fn run <id>` mid-flight (coste <50ms).
- `docs/capabilities/<grupo>.md` cargado solo si tarea matchea dominio (coste 1 read).
- `MEMORY.md` con insights cross-session (coste 0, persistente).
Las 3 metricas norte (en Monitor tab del `registry_dashboard`):
- `Reg %` — fraccion de calls con `function_id != ''`. Sube cuando Claude usa registry en vez de heredoc.
- `MCP ratio` — fraccion de `tool_used IN (mcp_*, fn_run)`. Mide adopcion canonica.
- `violations_24h` — antipatrones detectados. Baja con cada vuelta del bucle.
Cualquier decision tecnica que choque con estas metricas esta mal priorizada.
## Alternativas consideradas
- **Reglas mas estrictas sin telemetria.** Descartado: ya teniamos `registry_first.md` y se ignoraba sin que nada lo detectase. Reglas sin metricas son aspiracion.
- **Auto-merge de proposals desde el bucle reactivo.** Descartado: el agente decide *que* construir, pero el humano sigue revisando antes de escribir codigo permanente. Limite duro en `/fn_claude`: max 5 funciones por invocacion, no push automatico.
- **MCP-only sin hooks de telemetria.** Descartado: heredocs y `sqlite3` directo seguirian invisibles. El hook `PostToolUse` es la red de captura unica que cubre todos los canales (MCP, fn_run, heredoc, Edit, Write).
- **Indice estatico (lista plana de IDs) en lugar de capability groups.** Descartado: una lista de 800 funciones no cabe en contexto. Grupos con pagina madre densa (~80 lineas) cubren 80% del trabajo con coste de lectura asumible.
## Consecuencias
### Nuevo (visible en este repo)
- Carpeta `docs/capabilities/` con pagina madre por grupo. Indice en `docs/capabilities/INDEX.md`. Auditado por `fn doctor capabilities`.
- Hook `UserPromptSubmit` injecta cada turno: `REGISTRY-FIRST` + `CAPABILITY-GROWTH: created_this_session=X used=Y orphan=Z`.
- Hook `PostToolUse` (en `.claude/settings.local.json`) escribe a `call_monitor.operations.db`. Solo `args_hash`, NUNCA valores de argumentos (privacidad).
- Slash command `/fn_claude` — auto-auditoria: detecta gaps en la sesion, lanza `fn-constructor` en paralelo, valida que la proxima sesion usara la nueva funcion.
- Subagente `fn-constructor` mandatorio cuando se vaya a escribir >=5 lineas reutilizables inline.
### Reglas nuevas / endurecidas
- `.claude/rules/delegation.md` — STOP + spawn `fn-constructor` mismo turno, paralelo cuando >1 funcion independiente, tag de grupo obligatorio.
- `.claude/rules/capability_groups.md` — convencion de tag plano + pagina madre + auditoria via `fn doctor capabilities`.
- `.claude/rules/registry_calls.md` — 3 patrones canonicos (inspect / run / compose), antipatrones, excepciones para `sqlite3` directo.
### Boundary explicito
La telemetria cubre al **agente** y a **invocaciones canonicas**. Quedan fuera:
- Funcion Go/C++ llamada internamente por app ya compilada.
- Service de produccion recibiendo HTTP sin pasar por `fn run`.
- Sub-agente (`Agent` tool) sin telemetria heredada (mitigado por env var `FN_TELEMETRY=1` propagada).
Compensar runtime invisible con `e2e_checks` (issue 0068) y `fn doctor uses-functions` (audit estatico).
### Riesgos asumidos
- **Latencia de `fn_match` (capa 3).** Objetivo <50ms. Si supera 100ms, degradar a "solo en comandos largos" o "al final del turno".
- **Falsos positivos en propuestas automaticas.** Limite duro: max 5 por invocacion, evidencia obligatoria (snippet real), sin auto-merge.
- **Telemetria genera datos sensibles.** Mitigacion estructural: solo se guarda `args_hash`. Snippets de error redactados por allowlist.
## Aprendizaje
A 2026-05-14, primer grupo `cpp-windows` creado en respuesta directa al patron taskkill+cp+start repetido: `is_cpp_app_running_windows_bash_infra`, `launch_cpp_app_windows_bash_infra`, pipeline `redeploy_cpp_app_windows_bash_pipelines`, pagina madre `docs/capabilities/cpp-windows.md`. Validacion empirica del bucle: detectar -> construir -> agrupar -> documentar -> esperar reduccion de violations en proximas sesiones.
Pendiente: medir el delta de `Reg %` y `violations_24h` a 7d para confirmar que el coste de descubrimiento bajo de "FTS5 N veces" a "1 read de pagina madre".
+3
View File
@@ -59,3 +59,6 @@ Qué se aprendió después. Útil cuando un ADR se supersede.
| # | Título | Estado |
|---|--------|--------|
| [0001](0001-gitbutler-experiment.md) | Experimento con GitButler para trabajo paralelo | rejected |
| [0002](0002-apps-analyses-as-dataforge-master.md) | Apps y analyses como sub-repos `dataforge/<name>` con branch master | accepted |
| [0003](0003-orphan-tu-as-separate-function-entry.md) | TU adicional de un parent function como entrada propia | accepted |
| [0004](0004-telemetry-driven-capability-growth.md) | Telemetria de ejecuciones de Claude como motor de crecimiento del registry | accepted |
+1
View File
@@ -21,6 +21,7 @@ Indice de grupos de capacidades del registry. Cada grupo agrupa >=3 funciones qu
| [metabase](metabase.md) | 106 | _(editar — promovido automaticamente)_ |
| [doctor](doctor.md) | 11 | Diagnostico read-only del registry: artefactos, servicios, drift, funciones huerfanas |
| [notebook](notebook.md) | 5 | Operar Jupyter Lab colaborativo (discover/read/exec/write/kernel) |
| [cpp-windows](cpp-windows.md) | 7 | Compilar, desplegar, lanzar y verificar apps C++ en Windows desde WSL2 |
## Como anadir grupo
+80
View File
@@ -0,0 +1,80 @@
# cpp-windows
Operar apps C++ del registry en Windows desde WSL2: compilar, desplegar, lanzar, verificar y relanzar.
## Funciones del grupo
| ID | Firma corta | Que hace |
|---|---|---|
| `build_cpp_windows_bash_infra` | `build_cpp_windows([target])` | Cross-compila apps C++ para Windows con mingw-w64 |
| `deploy_cpp_exe_to_windows_bash_infra` | `deploy_cpp_exe_to_windows(app_name, app_dir)` | Copia .exe + DLLs + assets al Desktop Windows, mata proceso previo |
| `launch_cpp_app_windows_bash_infra` | `launch_cpp_app_windows(app_name, [desktop_dir])` | Lanza .exe en Windows via cmd.exe /c start, retorna inmediatamente |
| `is_cpp_app_running_windows_bash_infra` | `is_cpp_app_running_windows(app_name)` | Exit 0 si el proceso esta vivo (tasklist.exe), stdout: `RUNNING: PID=N MEM=...K` |
| `launch_cpp_app_windows_bash_infra` | `launch_cpp_app_windows(app_name, [desktop_dir])` | Lanza .exe en Windows via cmd.exe /c start, retorna inmediatamente |
| `e2e_run_cpp_windows_bash_infra` | `e2e_run_cpp_windows(target, [--no-build], [--no-deploy])` | Build + deploy + run headless de app C++ (tests e2e tipo altsnap) |
| `redeploy_cpp_app_windows_bash_pipelines` | `redeploy_cpp_app_windows(app_name, app_dir, [--build])` | Pipeline completo: build? + deploy + launch + verify en un comando |
## Ejemplo canonico
### Redeploy rapido (build ya hecho)
```bash
source bash/functions/infra/deploy_cpp_exe_to_windows.sh
source bash/functions/infra/launch_cpp_app_windows.sh
source bash/functions/infra/is_cpp_app_running_windows.sh
deploy_cpp_exe_to_windows "registry_dashboard" \
"/home/lucas/fn_registry/projects/fn_monitoring/apps/registry_dashboard"
launch_cpp_app_windows "registry_dashboard"
sleep 1
if is_cpp_app_running_windows "registry_dashboard"; then
echo "OK: arrancado"
else
echo "ERROR: no arranco" >&2; exit 1
fi
```
### Via pipeline (un solo comando)
```bash
source bash/functions/pipelines/redeploy_cpp_app_windows.sh
# Sin recompilar
redeploy_cpp_app_windows "registry_dashboard" \
"/home/lucas/fn_registry/projects/fn_monitoring/apps/registry_dashboard"
# Con recompilacion previa
redeploy_cpp_app_windows "chart_demo" \
"/home/lucas/fn_registry/cpp/apps/chart_demo" --build
```
### Comprobar si esta vivo antes de decidir
```bash
source bash/functions/infra/is_cpp_app_running_windows.sh
if is_cpp_app_running_windows "registry_dashboard"; then
echo "Ya esta corriendo — skip launch"
else
launch_cpp_app_windows "registry_dashboard"
fi
```
## Fronteras
- No gestiona builds Linux ni assets de Linux — solo el flujo Windows.
- No verifica que la app funcione correctamente, solo que el proceso existe (`tasklist.exe`).
- No gestiona credenciales ni configuracion de la app — eso es responsabilidad de cada app via `local_files/`.
- Tests automatizados en CI no son posibles — requieren WSL2 con Windows activo.
## Prerequisitos
- WSL2 con interop a Windows habilitado (`tasklist.exe`, `cmd.exe`, `wslpath` accesibles).
- `mingw-w64` instalado en WSL para `build_cpp_windows` (`sudo apt install mingw-w64`).
- El .exe compilado en `cpp/build/windows/apps/<app>/` antes de `deploy_cpp_exe_to_windows`.
## Notas
El flujo estandar es: `build_cpp_windows``deploy_cpp_exe_to_windows``launch_cpp_app_windows``is_cpp_app_running_windows`. El pipeline `redeploy_cpp_app_windows` encapsula los pasos 2-4 (con 1 opcional via `--build`).
`deploy_cpp_exe_to_windows` ya incluye un `taskkill.exe` interno — no es necesario matar el proceso manualmente antes de llamarlo. `is_cpp_app_running_windows` es util cuando se quiere tomar la decision de matar/no matar de forma explicita antes del deploy.
+103
View File
@@ -0,0 +1,103 @@
# 2026-05-14
## 01:35 — Telemetria de Claude como motor de capacidades + grupo cpp-windows
El registry pasa a optimizarse sobre las propias ejecuciones del agente. Cada Bash, MCP call y heredoc queda capturado en `call_monitor.operations.db` (issue 0085) y alimenta el bucle: detectar patron inline -> proponer funcion -> agrupar en capability -> servir descubrimiento gratis al siguiente turno.
- Hecho: ADR 0004 `0004-telemetry-driven-capability-growth.md` aceptado — formaliza el bucle telemetria + capability groups + discovery acceleration.
- Hecho: Indice ADR `docs/adr/README.md` ahora lista 0001..0004 (faltaban 0002 y 0003).
- Hecho: Grupo de capacidades `cpp-windows` creado como caso piloto del bucle:
- `bash/functions/infra/is_cpp_app_running_windows.sh` + `.md``is_cpp_app_running_windows_bash_infra`.
- `bash/functions/infra/launch_cpp_app_windows.sh` + `.md``launch_cpp_app_windows_bash_infra`.
- `bash/functions/pipelines/redeploy_cpp_app_windows.sh` + `.md``redeploy_cpp_app_windows_bash_pipelines` (build? + deploy + launch + verify).
- Pagina madre `docs/capabilities/cpp-windows.md` con lista, ejemplo canonico y fronteras.
- Fila anadida a `docs/capabilities/INDEX.md`.
- Hecho: Issue `dev/issues/0087-capability-discovery-acceleration.md` con modelo de 5 capas + plan en 3 tandas (A paralelo, B dependiente, C refuerzos).
- Hecho: Slash command `/fn_claude` (`.claude/commands/fn_claude.md`) describe AUDIT -> GAP -> PROPOSE -> CONSTRUCT -> VALIDATE -> SELF-TEST como flujo autonomo. Limite duro max 5 funciones por invocacion, sin auto-merge.
- Hecho: `.claude/CLAUDE.md` raiz lleva ahora seccion "Delegacion + Capability Groups" como REGLA DURA (issue 0086).
### Por que ahora
Sesion 2026-05-13/14: Claude reinvento `taskkill + cp Desktop + cmd /c start` cinco veces pese a tener `deploy_cpp_exe_to_windows_bash_infra`. Evidencia directa de que sin telemetria + descubrimiento barato las reglas son aspiracion. La respuesta no es regla mas estricta, es **infraestructura para que la regla se cumpla**.
### Las 3 metricas norte (Monitor tab del `registry_dashboard`)
- `Reg %` — fraccion de calls con `function_id != ''`. Sube cuando se usa registry.
- `MCP ratio` — adopcion de patrones canonicos.
- `violations_24h` — antipatrones detectados. Baja cuando el bucle cierra.
### Pendiente
- Implementar Tanda A de 0087 en paralelo: `fn doctor capabilities --emit-claude-md`, binario `fn_match`, validator de IDs en `fn_create_function`, clausula hooks en `registry_calls.md`.
- Medir delta `Reg %` y `violations_24h` a 7d. Si no se mueven, replantear capas 1-2.
- Hook `PreToolUse` con `fn_match` requiere benchmark <50ms antes de habilitar global.
Referencias: ADR 0004, issue 0085, issue 0086, issue 0087, `docs/capabilities/cpp-windows.md`.
## 02:00 — Tandas A+B de issue 0087 cerradas + fn_match tuning + Failed Functions sub-tab
### Tanda A (paralelo, 3 fn-constructor + edits directos)
- `cmd/fn/match.go` — subcommand `fn match "<command>"`. Tokeniza comando, FTS5 query sobre `functions_fts`, re-score aditivo (name=+3, tags=+2, signature=+1.5, description=+1). Latencia 6-7ms. Devuelve `top[]` con `score` (normalizado top=1.0) y `raw_score` (absoluto pre-normalizacion). Bug fix critico: `high_confidence` ahora se computa sobre `raw_score`, no `score` — antes era SIEMPRE true por la normalizacion.
- `cmd/fn/doctor.go` — flag `--emit-claude-md` en subcomando `capabilities`. Emite bloque markdown con secciones TOP 20 (por `calls_total`), Fresh 7d, Pipelines top 5. Fallback si `call_monitor.operations.db` ausente.
- `functions/infra/emit_capabilities_md.go``EmitCapabilitiesMd` + `RenderCapabilitiesMd` (paquete `infra`).
- `projects/fn_monitoring/apps/call_monitor/sequences.go` + `migrations/006_function_sequences.sql` — subcomando `call_monitor sequences --detect [--propose]`. Detecta secuencias A->B(->C) con: same session, gap < 30s, occ >= 5, sess >= 2, success_rate >= 0.9. Genera proposals `new_pipeline` con evidencia. 41 secuencias capturadas en primer run; 0 candidatas (umbrales no alcanzados todavia).
- `.claude/rules/registry_calls.md` — clausula nueva: hooks e infraestructura de telemetria pueden leer `registry.db` directo con conexion read-only. NO sujeto a regla MCP-first porque no es accion del agente.
- `.claude/rules/function_growth_and_self_docs.md` (nuevo) + fila en `INDEX.md` — contrato `.md` autosuficiente (Ejemplo + Cuando usarla + Gotchas + Growth log) + crecimiento del registry por **promocion de composiciones** (no por inflar funciones individuales). Issue 0087.
- `.claude/CLAUDE.md` — Norte ampliado a 4 objetivos (anadido `PROMOVER COMPOSICIONES A PIPELINES`). Linea sobre auto-discovery zero-second-lookup.
### Tanda B (3 general-purpose en paralelo)
- `.claude/scripts/hook_fn_match.sh` (PreToolUse, Bash matcher) — denylist temprana (cmds < 20 chars, `ls/cd/pwd/cat/echo/grep`, `./fn run`, pure-cd). Llama `fn match` con `timeout 0.2`. Trigger: `high_confidence==true OR raw_score >= ~5`. Imprime `<system-reminder>FUZZY-MATCH: USE ./fn run <id>` a stderr. Latencia path trigger ~113ms, denylist ~32ms.
- `.claude/scripts/hook_capabilities_inject.sh` (UserPromptSubmit) — cache `~/.cache/fn_registry/capabilities.txt` TTL 1h. Llama `fn doctor capabilities --emit-claude-md`, awk-parse 3 secciones, emite JSON `hookSpecificOutput.additionalContext` con linea compacta `CAPABILITIES (cache 1h): TOP / FRESH / PIPELINES`. Latencia cold 33ms, warm 18ms.
- `~/.config/systemd/user/call_monitor_sequences.{service,timer}` + copias versionadas en `projects/fn_monitoring/apps/call_monitor/systemd/`. Timer OnCalendar `*-*-* 00/6:00:00` (cada 6h). Persistent=true. Service activo y verificado (exit 0).
### Tuning de fn_match
Tres iteraciones del threshold de `high_confidence`:
- v1: `top1.score / top2.score > 1.5` (normalizado). Resultado: SIEMPRE true. Inutil.
- v2: `raw_score >= 3.0`. Mejor pero deja falsos positivos en queries debiles (raw 3.2-3.65).
- v3: `raw_score >= 5.0`. Mata caso ejemplar `taskkill` (raw 4.06). Demasiado estricto.
- **v4 final**: `raw_score >= 4.0`. Balance ~93% precision contra patrones de `analysis/domain_coverage_gaps`. Prefer silencio en queries cortas; acepta 1 falso positivo conocido (`robots.txt user-agent` -> `agent_scaffold` por token "agent").
### Dashboard updates (sub-repo `registry_dashboard`)
- Migration 007 `projects/fn_monitoring/apps/call_monitor/migrations/007_calls_command_snippet.sql` — anade columna `command_snippet TEXT` a `calls`. Aditiva, idempotente. Solo se rellena cuando `function_id == ''`. Redactado de secrets (`password=`, `token=`, `secret=`, `api_key=`, `bearer=`) antes de persistir.
- `.claude/scripts/hook_call_monitor.sh``insert_call` acepta 4to arg `snippet`. Solo escribe si `fn_id` vacio. Llamadores Bash pasan `CMD_HEAD` (200 chars).
- Dashboard Recent Executions: columna `Function` muestra snippet `$ <cmd>` en gris cuando `function_id` vacio. Hover tooltip con snippet completo.
- Nueva sub-tab `Failed Functions` (5a): subset de `recent_executions` con `function_id != '' AND !success`. Columnas When/Function/Tool/Error class/Error snippet. Util para diagnostico cuando objetivo 1+2 caen.
- ImPlot scatter:
- Eje Y dinamico `0..max(visible) + 500ms` (rescale evita pillarse en picos viejos fuera de ventana).
- Combo `Scatter:` en toolbar (1m/5m/15m/1h/6h), independiente del filtro de KPIs.
- `ImPlot::GetStyle().UseLocalTime = true` — el eje X ahora muestra hora local, no UTC.
- `ImPlotAxisFlags_NoHighlight` en X y Y — sin iluminacion ruidosa al hover.
- Hover tooltip sobre punto: When, Function, Tool, Duration, Error (si fail).
### Bug fix critico: launch_cpp_app_windows
`cmd.exe /c "cd /d \"$dir\" && start \"$app\" \"$app.exe\""` rompia en cmd.exe — el `\"` final tras path termiando en `\` se interpretaba como escape de comilla, dejando string sin cerrar -> error "Windows cannot find \\". Fix: reescribir a PowerShell `Start-Process -FilePath ... -WorkingDirectory ...` que usa quoting simple literal.
### Discovery quality (tests vs fn_monitoring/analysis/domain_coverage_gaps)
Patrones del analysis testeados contra `fn match`:
- Hits correctos `high=true`: sharpe ratio, monte carlo, cdp browse, websocket stream, stable diffusion, taskkill exe.
- Falsos negativos coherentes `high=false`: binance kline, kelly criterion, VaR conditional, OLS regression, vectorized backtest, robots.txt -> coincide con gaps reales del analysis.
- Falso positivo persistente: 1 de 14 (robots.txt -> agent_scaffold).
- Cobertura validada: finance/browser/infra/ml estan cubiertos. Trading/quant/scraping confirmados como gaps reales del analysis.
### Validacion end-to-end
- PreToolUse hook test: `echo '{...taskkill...}' | bash .claude/scripts/hook_fn_match.sh` -> FUZZY-MATCH con `deploy_cpp_exe_to_windows`.
- UserPromptSubmit hook: cada turno user injecta `CAPABILITIES (cache 1h): TOP / FRESH / PIPELINES`.
- Timer: `systemctl --user list-timers` muestra proxima ejecucion. Manual run exit 0.
- Pipeline `redeploy_cpp_app_windows` ejecutado 3+ veces esta sesion via `./fn run`. Reemplaza ~6 commands manuales con 1 invocacion.
### Pendiente
- 2 bugs cosmeticos no bloqueantes: `cp: Permission denied` benigno en deploy (rsync salva); `PID=RUNNING: PID=N MEM=K` formato raro en mensaje final del pipeline redeploy.
- Tanda C de 0087 (refuerzos): `/fn_claude` auto-append a `MEMORY.md` al crear funciones, naming validator soft.
- Esperar telemetria de 7d para medir delta `Reg %` y `violations_24h`. Si no se mueven, replantear capa 3 (interceptor PreToolUse mas agresivo).
Referencias: ADR 0004, issues 0085/0086/0087, `.claude/rules/function_growth_and_self_docs.md`, `.claude/scripts/hook_{fn_match,capabilities_inject}.sh`, `cmd/fn/match.go`.
+318
View File
@@ -0,0 +1,318 @@
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("<!-- AUTO-GENERATED by `fn doctor capabilities --emit-claude-md` — do not edit by hand -->\n")
sb.WriteString(fmt.Sprintf("<!-- Last refresh: %s -->\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)
}