feat(infra): auto-commit con 29 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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**.
|
||||
|
||||
---
|
||||
|
||||
@@ -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.
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
Executable
+121
@@ -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
|
||||
Executable
+133
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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: []
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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".
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user