chore: auto-commit (799 archivos)
- .claude/CLAUDE.md - .claude/commands/subagentes.md - .claude/rules/INDEX.md - .mcp.json - bash/functions/cybersecurity/analyze_dns.md - bash/functions/cybersecurity/audit_http_headers.md - bash/functions/cybersecurity/audit_ssh_config.md - bash/functions/cybersecurity/check_firewall.md - bash/functions/cybersecurity/detect_suspicious_users.md - bash/functions/cybersecurity/encrypt_file.md - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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]
|
||||
tags: [cpp, compile, windows, mingw, cross-compile, deploy, pipeline, pendiente-usar]
|
||||
uses_functions:
|
||||
- resolve_cpp_app_dir_bash_infra
|
||||
- build_cpp_windows_bash_infra
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "full_git_pull() -> stdout: tabla resumen"
|
||||
description: "Pull automatico de fn_registry + todos los sub-repos locales + submodules + fn sync. Descubre repos locales, stashea dirty trees antes de pullear, hace pull --ff-only, actualiza submodulos del repo principal, pulla ~/.password-store, regenera registry.db con fn index y ejecuta fn sync."
|
||||
tags: [git, pull, sync, registry, pipeline]
|
||||
tags: [git, pull, sync, registry, pipeline, pendiente-usar]
|
||||
uses_functions:
|
||||
- discover_git_repos_bash_infra
|
||||
- git_pull_with_stash_bash_infra
|
||||
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "full_git_push(commit_message?: string) -> stdout: tabla resumen"
|
||||
description: "Push automatico de fn_registry + todos los sub-repos + fn sync. Descubre repos, escanea secrets (aborta si detecta), auto-inicializa apps/analyses sin .git via ensure_repo_synced, auto-commitea dirty trees, pushea solo repos adelantados, pushea ~/.password-store sin commitear, y ejecuta fn sync."
|
||||
tags: [git, push, sync, registry, pipeline]
|
||||
tags: [git, push, sync, registry, pipeline, pendiente-usar]
|
||||
uses_functions:
|
||||
- discover_git_repos_bash_infra
|
||||
- scan_secrets_in_dirty_bash_cybersecurity
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
---
|
||||
name: generate_capability_doc
|
||||
kind: pipeline
|
||||
lang: bash
|
||||
domain: pipelines
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "generate_capability_doc(group: string, --registry: string?, --out: string?) -> void"
|
||||
description: "Regenera la tabla de funciones de una pagina capability en docs/capabilities/<group>.md consultando registry.db. Preserva bloques curated (Ejemplo canonico, Fronteras, Prerequisitos, Notas). Si el archivo no existe lo crea con plantilla minima."
|
||||
tags: ["capability-groups", "docs", "doctor", "generator", "pipeline"]
|
||||
uses_functions:
|
||||
- audit_capability_groups_go_infra
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: group
|
||||
desc: "slug del capability group (ej. notebook, metabase). Coincide con el tag canonico en frontmatter de funciones."
|
||||
- name: --registry
|
||||
desc: "path opcional a registry.db o a su directorio padre (default: walk-up desde cwd hasta encontrar registry.db)."
|
||||
- name: --out
|
||||
desc: "path opcional del archivo de salida (default: <root>/docs/capabilities/<group>.md)."
|
||||
output: "path del archivo actualizado o creado + count de funciones a stdout. Exit 0 ok, exit 1 si error."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/pipelines/generate_capability_doc.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Regenerar tabla de notebook (ya existe, preserva Ejemplo canonico / Fronteras)
|
||||
./bash/functions/pipelines/generate_capability_doc.sh notebook
|
||||
# → /home/lucas/fn_registry/docs/capabilities/notebook.md updated (5 functions)
|
||||
|
||||
# Crear pagina nueva para un grupo sin pagina todavia
|
||||
./bash/functions/pipelines/generate_capability_doc.sh metabase
|
||||
# → /home/lucas/fn_registry/docs/capabilities/metabase.md created (12 functions)
|
||||
|
||||
# Especificar registry y destino custom
|
||||
./bash/functions/pipelines/generate_capability_doc.sh android \
|
||||
--registry /ruta/alternativa/registry.db \
|
||||
--out /tmp/android_cap.md
|
||||
# → /tmp/android_cap.md created (8 functions)
|
||||
|
||||
# Grupo sin funciones todavia (avisa pero no falla)
|
||||
./bash/functions/pipelines/generate_capability_doc.sh nuevo_grupo
|
||||
# WARN: El grupo 'nuevo_grupo' no tiene funciones con ese tag en registry.db.
|
||||
# → /home/lucas/fn_registry/docs/capabilities/nuevo_grupo.md created (0 functions)
|
||||
```
|
||||
|
||||
## Comportamiento detallado
|
||||
|
||||
### Resolucion de root
|
||||
|
||||
Walk-up desde `cwd` buscando `registry.db`. Si se pasa `--registry`:
|
||||
- Si es un archivo: toma el directorio padre.
|
||||
- Si es un directorio: lo usa directamente.
|
||||
|
||||
### Tabla generada (SQL)
|
||||
|
||||
```sql
|
||||
SELECT f.id, f.signature, f.description
|
||||
FROM functions f, json_each(f.tags) j
|
||||
WHERE j.value = '<group>'
|
||||
ORDER BY f.id;
|
||||
```
|
||||
|
||||
JOIN custom via `json_each` — usa `sqlite3` directo (excepcion autorizada para JOINs no expuestos por MCP).
|
||||
|
||||
### Formato de tabla
|
||||
|
||||
```markdown
|
||||
| ID | Firma | Que hace |
|
||||
|---|---|---|
|
||||
| `<id>` | `<signature>` | <description> |
|
||||
```
|
||||
|
||||
Los `|` dentro de signatures y descriptions se escapan como `\|`.
|
||||
|
||||
### Preservacion de bloques curated
|
||||
|
||||
Cuando el archivo ya existe:
|
||||
- El bloque entre `## Funciones` y la siguiente `## ` se reemplaza.
|
||||
- Todo lo anterior y posterior (Ejemplo canonico, Fronteras, Prerequisitos, Notas, etc.) se mantiene intacto.
|
||||
- Implementado con `awk`: copia hasta `## Funciones`, imprime nueva tabla, salta contenido viejo hasta encontrar `^## `, reanuda copia.
|
||||
|
||||
### Archivo nuevo
|
||||
|
||||
Si `docs/capabilities/<group>.md` no existe, se crea con plantilla minima:
|
||||
- Titulo `# Capability: <group>`.
|
||||
- Placeholder de descripcion (editable a mano).
|
||||
- Tabla generada.
|
||||
- Secciones vacias: `## Ejemplo canonico`, `## Fronteras`.
|
||||
|
||||
## Codigos de salida
|
||||
|
||||
| Codigo | Significado |
|
||||
|--------|-------------|
|
||||
| 0 | Exito |
|
||||
| 1 | Error: grupo no especificado, registry.db no encontrado, fallo SQL, awk vacio |
|
||||
|
||||
## Notas
|
||||
|
||||
- `uses_functions: []` — depende de `sqlite3` y `awk` del sistema, no de funciones del registry.
|
||||
- El tag del grupo debe ser plano (ej. `notebook`, `metabase`), no `tag:notebook`.
|
||||
- Enganchado en `docs/capabilities/INDEX.md` como el mecanismo de auto-generacion referenciado bajo `fn doctor capabilities --update`.
|
||||
- Seguro para re-ejecucion: idempotente si el registry no cambia.
|
||||
@@ -0,0 +1,231 @@
|
||||
#!/usr/bin/env bash
|
||||
# generate_capability_doc — regenera la tabla "Funciones" de una pagina capability
|
||||
# Preserva bloques curated (Ejemplo canonico, Fronteras, Prerequisitos, Notas, etc.)
|
||||
# Usage: generate_capability_doc <group> [--registry <path>] [--out <path>]
|
||||
set -euo pipefail
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# --------------------------------------------------------------------------
|
||||
die() { echo "ERROR: $*" >&2; exit 1; }
|
||||
warn() { echo "WARN: $*" >&2; }
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Parse args
|
||||
# --------------------------------------------------------------------------
|
||||
GROUP=""
|
||||
REGISTRY_PATH=""
|
||||
OUT_PATH=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--registry)
|
||||
shift
|
||||
REGISTRY_PATH="${1:-}"
|
||||
[[ -z "$REGISTRY_PATH" ]] && die "--registry requiere un valor"
|
||||
shift
|
||||
;;
|
||||
--out)
|
||||
shift
|
||||
OUT_PATH="${1:-}"
|
||||
[[ -z "$OUT_PATH" ]] && die "--out requiere un valor"
|
||||
shift
|
||||
;;
|
||||
-*)
|
||||
die "Opcion desconocida: $1"
|
||||
;;
|
||||
*)
|
||||
[[ -n "$GROUP" ]] && die "Solo se acepta un <group>. Ya se especifico: '$GROUP'"
|
||||
GROUP="$1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -z "$GROUP" ]] && die "Uso: generate_capability_doc <group> [--registry <path>] [--out <path>]"
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Resolver registry root
|
||||
# --------------------------------------------------------------------------
|
||||
find_registry_root() {
|
||||
local dir
|
||||
dir="$(pwd)"
|
||||
while [[ "$dir" != "/" ]]; do
|
||||
if [[ -f "$dir/registry.db" ]]; then
|
||||
echo "$dir"
|
||||
return 0
|
||||
fi
|
||||
dir="$(dirname "$dir")"
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
if [[ -n "$REGISTRY_PATH" ]]; then
|
||||
# Si se pasa un path que termina en registry.db, tomar el directorio
|
||||
if [[ -f "$REGISTRY_PATH" ]]; then
|
||||
REGISTRY_ROOT="$(dirname "$(realpath "$REGISTRY_PATH")")"
|
||||
elif [[ -d "$REGISTRY_PATH" ]]; then
|
||||
REGISTRY_ROOT="$(realpath "$REGISTRY_PATH")"
|
||||
else
|
||||
die "registry no encontrado en: $REGISTRY_PATH"
|
||||
fi
|
||||
else
|
||||
REGISTRY_ROOT="$(find_registry_root)" || die "No se encontro registry.db. Ejecutar desde dentro del registry o pasar --registry."
|
||||
fi
|
||||
|
||||
DB="$REGISTRY_ROOT/registry.db"
|
||||
[[ -f "$DB" ]] || die "registry.db no encontrado en: $DB"
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Resolver output path
|
||||
# --------------------------------------------------------------------------
|
||||
if [[ -z "$OUT_PATH" ]]; then
|
||||
OUT_PATH="$REGISTRY_ROOT/docs/capabilities/${GROUP}.md"
|
||||
fi
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Consultar funciones del grupo
|
||||
# --------------------------------------------------------------------------
|
||||
# JOIN custom entre functions y json_each(tags) — excepcion autorizada para sqlite3 directo.
|
||||
# Usamos U+001F (ASCII Unit Separator) como separador de campos para evitar conflictos
|
||||
# con el caracter | que aparece en signatures Python (ej. "list[int] | None").
|
||||
SEP=$'\x1f'
|
||||
ROWS="$(sqlite3 -separator "$SEP" "$DB" \
|
||||
"SELECT f.id, f.signature, f.description
|
||||
FROM functions f, json_each(f.tags) j
|
||||
WHERE j.value = '${GROUP}'
|
||||
ORDER BY f.id;" 2>/dev/null)" || die "Error al consultar registry.db"
|
||||
|
||||
# Escapar | en un valor para que no rompa la tabla Markdown
|
||||
escape_pipe() {
|
||||
printf '%s' "$1" | sed 's/|/\\|/g'
|
||||
}
|
||||
|
||||
# Construir tabla Markdown
|
||||
TABLE_HEADER="| ID | Firma | Que hace |
|
||||
|---|---|---|"
|
||||
|
||||
TABLE_ROWS=""
|
||||
FUNC_COUNT=0
|
||||
|
||||
if [[ -n "$ROWS" ]]; then
|
||||
while IFS="$SEP" read -r id signature description; do
|
||||
# Limpiar espacios extra
|
||||
id="${id// /}"
|
||||
signature="$(printf '%s' "$signature" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
||||
description="$(printf '%s' "$description" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
||||
# Escapar pipes internos en firma y descripcion
|
||||
sig_esc="$(escape_pipe "$signature")"
|
||||
desc_esc="$(escape_pipe "$description")"
|
||||
TABLE_ROWS="${TABLE_ROWS}| \`${id}\` | \`${sig_esc}\` | ${desc_esc} |
|
||||
"
|
||||
FUNC_COUNT=$((FUNC_COUNT + 1))
|
||||
done <<< "$ROWS"
|
||||
fi
|
||||
|
||||
if [[ $FUNC_COUNT -eq 0 ]]; then
|
||||
warn "El grupo '${GROUP}' no tiene funciones con ese tag en registry.db."
|
||||
TABLE_CONTENT="_No hay funciones con tag ${GROUP}._"
|
||||
else
|
||||
TABLE_CONTENT="${TABLE_HEADER}
|
||||
${TABLE_ROWS}"
|
||||
fi
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Crear o actualizar el archivo
|
||||
# --------------------------------------------------------------------------
|
||||
mkdir -p "$(dirname "$OUT_PATH")"
|
||||
|
||||
if [[ ! -f "$OUT_PATH" ]]; then
|
||||
# --- Archivo nuevo: plantilla minima ---
|
||||
cat > "$OUT_PATH" <<TEMPLATE
|
||||
# Capability: ${GROUP}
|
||||
|
||||
_(Descripcion del grupo — editar a mano)_
|
||||
|
||||
## Funciones
|
||||
|
||||
${TABLE_CONTENT}
|
||||
|
||||
## Ejemplo canonico
|
||||
|
||||
_(Anadir 1-2 bloques de codigo end-to-end)_
|
||||
|
||||
## Fronteras
|
||||
|
||||
_(Que NO cubre este grupo)_
|
||||
TEMPLATE
|
||||
echo "${OUT_PATH} created (${FUNC_COUNT} functions)"
|
||||
else
|
||||
# --- Archivo existente: reemplazar SOLO el bloque "## Funciones" ---
|
||||
# Estrategia awk:
|
||||
# - Copiar todo hasta (e incluyendo) "## Funciones"
|
||||
# - Imprimir linea en blanco + nueva tabla + linea en blanco
|
||||
# - Saltar lineas hasta encontrar la proxima seccion "^## "
|
||||
# - Reanudar copia desde esa seccion en adelante
|
||||
|
||||
# Escribir tabla en archivo temporal para pasarla a awk sin problemas de escaping
|
||||
TMP_TABLE="$(mktemp)"
|
||||
trap 'rm -f "$TMP_TABLE"' EXIT
|
||||
printf '%s\n' "$TABLE_CONTENT" > "$TMP_TABLE"
|
||||
|
||||
TMP_OUT="$(mktemp)"
|
||||
trap 'rm -f "$TMP_TABLE" "$TMP_OUT"' EXIT
|
||||
|
||||
awk -v table_file="$TMP_TABLE" '
|
||||
BEGIN {
|
||||
in_functions = 0
|
||||
done = 0
|
||||
# Leer tabla nueva en una variable
|
||||
table_content = ""
|
||||
while ((getline line < table_file) > 0) {
|
||||
table_content = table_content line "\n"
|
||||
}
|
||||
close(table_file)
|
||||
}
|
||||
|
||||
# Detectar inicio de la seccion Funciones
|
||||
/^## Funciones$/ && !done {
|
||||
print $0
|
||||
printf "\n"
|
||||
printf "%s", table_content
|
||||
in_functions = 1
|
||||
next
|
||||
}
|
||||
|
||||
# Mientras estamos en el bloque Funciones, saltar lineas hasta proxima seccion
|
||||
in_functions && /^## / {
|
||||
in_functions = 0
|
||||
done = 1
|
||||
printf "\n"
|
||||
print $0
|
||||
next
|
||||
}
|
||||
|
||||
in_functions {
|
||||
# Saltar contenido viejo del bloque Funciones
|
||||
next
|
||||
}
|
||||
|
||||
# Fuera del bloque: copiar tal cual
|
||||
{ print $0 }
|
||||
|
||||
END {
|
||||
# Si no habia seccion siguiente (Funciones era la ultima), cerrar bien
|
||||
if (in_functions) {
|
||||
printf "\n"
|
||||
}
|
||||
}
|
||||
' "$OUT_PATH" > "$TMP_OUT"
|
||||
|
||||
# Verificar que el archivo resultante no este vacio
|
||||
if [[ ! -s "$TMP_OUT" ]]; then
|
||||
rm -f "$TMP_OUT"
|
||||
die "Error: awk produjo un archivo vacio. El archivo original no fue modificado."
|
||||
fi
|
||||
|
||||
mv "$TMP_OUT" "$OUT_PATH"
|
||||
echo "${OUT_PATH} updated (${FUNC_COUNT} functions)"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -7,7 +7,7 @@ version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "install_systemd_service --name <N> --exec <PATH> [opts] -> json"
|
||||
description: "Pipeline que registra una app como servicio systemd del sistema: genera el unit, lo instala en /etc/systemd/system/, hace daemon-reload, enable, start y devuelve status. Requiere sudo sin password para systemctl y escritura en /etc/systemd/system/."
|
||||
tags: [systemd, service, local, infra, pipeline, install]
|
||||
tags: [systemd, service, local, infra, pipeline, install, pendiente-usar]
|
||||
uses_functions:
|
||||
- systemd_local_install_unit_bash_infra
|
||||
- systemd_local_enable_bash_infra
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
name: propose_capability_groups
|
||||
kind: pipeline
|
||||
lang: bash
|
||||
domain: pipelines
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "propose_capability_groups([--min-count N], [--max-domains M], [--json], [--apply tag]) -> report"
|
||||
description: "Analiza tags candidatos a capability group (issue 0086). Filtra via blocklist + cap de dominios. Lista candidatos o promociona con --apply (llama generate_capability_doc + actualiza INDEX.md)."
|
||||
tags: ["capability-groups", "doctor", "audit", "pipeline"]
|
||||
uses_functions:
|
||||
- generate_capability_doc_bash_pipelines
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: --min-count
|
||||
desc: "minimo de funciones con el tag para considerarlo candidato (default 3)"
|
||||
- name: --max-domains
|
||||
desc: "maximo de dominios distintos entre funciones del tag (default 3, filtra tags genericos que cruzan muchos dominios)"
|
||||
- name: --json
|
||||
desc: "salida JSON estructurada en vez de texto (util para agentes)"
|
||||
- name: --apply
|
||||
desc: "promociona el tag dado a capability group: genera doc con generate_capability_doc + actualiza docs/capabilities/INDEX.md (idempotente)"
|
||||
output: "tabla de candidatos a stdout (tag, count, domains, already_group, samples), o efectos en disco si --apply (nuevo .md + fila en INDEX.md)"
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/pipelines/propose_capability_groups.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Listar candidatos con al menos 10 funciones
|
||||
bash bash/functions/pipelines/propose_capability_groups.sh --min-count 10
|
||||
|
||||
# Listar con salida JSON (para agentes)
|
||||
bash bash/functions/pipelines/propose_capability_groups.sh --min-count 5 --json
|
||||
|
||||
# Promocionar un tag a capability group
|
||||
bash bash/functions/pipelines/propose_capability_groups.sh --apply metabase
|
||||
|
||||
# Via fn run
|
||||
./fn run propose_capability_groups --min-count 10
|
||||
```
|
||||
|
||||
## Logica
|
||||
|
||||
### Modo listar (sin --apply)
|
||||
|
||||
1. Resuelve la raiz del registry (walk-up hasta `registry.db`).
|
||||
2. Filtra tags via **blocklist** hardcodeada (idiomas, dominios, CRUD generico, verbos super-genericos, primitivas, estados).
|
||||
3. Query SQL con `json_each(f.tags)` + `GROUP BY` + `HAVING cnt >= N AND domains <= M`.
|
||||
4. Marca cada candidato con `already_group: yes/no` parseando los links `[tag](tag.md)` de `docs/capabilities/INDEX.md`.
|
||||
5. Imprime tabla formateada con maximo 3 IDs de muestra por candidato.
|
||||
6. Resumen: total candidatos, ya-grupos, nuevos.
|
||||
|
||||
### Modo --apply
|
||||
|
||||
1. Valida que el tag no esta en blocklist y pasa el filtro de count/domains.
|
||||
2. Llama a `bash/functions/pipelines/generate_capability_doc.sh <tag>`.
|
||||
3. Inserta fila en `docs/capabilities/INDEX.md` (idempotente — no duplica si ya existe).
|
||||
4. Imprime checklist de edicion manual para el usuario.
|
||||
|
||||
## Blocklist
|
||||
|
||||
Tags en la blocklist nunca son candidatos (caso exacto):
|
||||
|
||||
- **Idioma**: `go py bash ps ts python cpp`
|
||||
- **Dominio**: `core infra finance datascience cybersecurity shell tui pipelines browser`
|
||||
- **Kind/purity**: `function pipeline component pure impure`
|
||||
- **CRUD generico**: `add create delete list update get set remove insert`
|
||||
- **Verbo super-generico**: `compose convert combine append empty exists check find format parse render`
|
||||
- **Estructural**: `generic helper utility wrapper test`
|
||||
- **Primitivas**: `string number int float array slice map dict value key`
|
||||
- **Estados**: `pending-usar pendiente-usar`
|
||||
|
||||
## Notas
|
||||
|
||||
- Usa `sqlite3` directo para el JOIN custom `functions + json_each(tags)` (autorizado por la regla registry_calls.md — JOINs custom no expuestos por el MCP).
|
||||
- La insercion en INDEX.md usa `python3` embebido para manejar la posicion relativa a la cabecera de tabla de forma portable.
|
||||
- El flag `--max-domains` es la heuristica clave: un tag como `add` aparece en 8 dominios → filtrado; `metabase` aparece en 2 → candidato valido.
|
||||
@@ -0,0 +1,346 @@
|
||||
#!/usr/bin/env bash
|
||||
# propose_capability_groups — analiza tags candidatos a capability group (issue 0086)
|
||||
# Filtra via blocklist + cap de dominios. Lista candidatos o promociona con --apply.
|
||||
set -euo pipefail
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Blocklist: tags genericos que nunca son capability groups
|
||||
# ---------------------------------------------------------------------------
|
||||
BLOCKLIST=(
|
||||
# idioma
|
||||
go py bash ps ts python cpp
|
||||
# dominio
|
||||
core infra finance datascience cybersecurity shell tui pipelines browser
|
||||
# kind / purity
|
||||
function pipeline component pure impure
|
||||
# CRUD generico
|
||||
add create delete list update get set remove insert
|
||||
# verbo super-generico
|
||||
compose convert combine append empty exists check find format parse render
|
||||
# estructural
|
||||
generic helper utility wrapper test
|
||||
# primitivas
|
||||
string number int float array slice map dict value key
|
||||
# estados
|
||||
pending-usar pendiente-usar
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Resolver raiz del registry (walk-up hasta registry.db)
|
||||
# ---------------------------------------------------------------------------
|
||||
find_registry_root() {
|
||||
local dir
|
||||
dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
while [[ "$dir" != "/" ]]; do
|
||||
if [[ -f "$dir/registry.db" ]]; then
|
||||
echo "$dir"
|
||||
return 0
|
||||
fi
|
||||
dir="$(dirname "$dir")"
|
||||
done
|
||||
echo "ERROR: registry.db no encontrado en ningún directorio padre" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Defaults
|
||||
# ---------------------------------------------------------------------------
|
||||
MIN_COUNT=3
|
||||
MAX_DOMAINS=4
|
||||
JSON_MODE=false
|
||||
APPLY_TAG=""
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parse args
|
||||
# ---------------------------------------------------------------------------
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--min-count)
|
||||
MIN_COUNT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--max-domains)
|
||||
MAX_DOMAINS="$2"
|
||||
shift 2
|
||||
;;
|
||||
--json)
|
||||
JSON_MODE=true
|
||||
shift
|
||||
;;
|
||||
--apply)
|
||||
APPLY_TAG="$2"
|
||||
shift 2
|
||||
;;
|
||||
--help|-h)
|
||||
echo "Uso: propose_capability_groups [--min-count N] [--max-domains M] [--json] [--apply <tag>]"
|
||||
echo ""
|
||||
echo " --min-count N Minimo de funciones con el tag (default: 3)"
|
||||
echo " --max-domains M Maximo de dominios distintos (default: 3)"
|
||||
echo " --json Salida JSON"
|
||||
echo " --apply <tag> Promociona el tag a capability group"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: argumento desconocido: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
REGISTRY_ROOT="$(find_registry_root)"
|
||||
DB="$REGISTRY_ROOT/registry.db"
|
||||
INDEX_MD="$REGISTRY_ROOT/docs/capabilities/INDEX.md"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Construir lista de tags bloqueados como CSV quoted para SQL IN (...)
|
||||
# ---------------------------------------------------------------------------
|
||||
build_blocklist_sql() {
|
||||
local csv=""
|
||||
for tag in "${BLOCKLIST[@]}"; do
|
||||
csv="${csv}'${tag}',"
|
||||
done
|
||||
# quitar coma final
|
||||
echo "${csv%,}"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parsear tags ya en INDEX.md: extraer slugs de [tag](tag.md)
|
||||
# ---------------------------------------------------------------------------
|
||||
get_existing_groups() {
|
||||
if [[ ! -f "$INDEX_MD" ]]; then
|
||||
return
|
||||
fi
|
||||
grep -oP '\[([^\]]+)\]\(\1\.md\)' "$INDEX_MD" | grep -oP '\[([^\]]+)\]' | tr -d '[]' || true
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Comprobar si un tag esta en blocklist
|
||||
# ---------------------------------------------------------------------------
|
||||
in_blocklist() {
|
||||
local tag="$1"
|
||||
for blocked in "${BLOCKLIST[@]}"; do
|
||||
if [[ "$blocked" == "$tag" ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MODO --apply: promocionar un tag a capability group
|
||||
# ---------------------------------------------------------------------------
|
||||
apply_tag() {
|
||||
local tag="$1"
|
||||
|
||||
# Validar: no en blocklist
|
||||
if in_blocklist "$tag"; then
|
||||
echo "ERROR: '$tag' está en la blocklist de tags genericos. No se puede promocionar." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validar: count >= min y domains <= max
|
||||
local blocklist_sql
|
||||
blocklist_sql="$(build_blocklist_sql)"
|
||||
local row
|
||||
row="$(sqlite3 "$DB" "
|
||||
SELECT COUNT(*) AS cnt, COUNT(DISTINCT f.domain) AS domains
|
||||
FROM functions f, json_each(f.tags) j
|
||||
WHERE j.value = '${tag}'
|
||||
GROUP BY j.value;
|
||||
" 2>/dev/null || true)"
|
||||
|
||||
if [[ -z "$row" ]]; then
|
||||
echo "ERROR: tag '$tag' no encontrado en el registry o no tiene funciones." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local cnt domains
|
||||
cnt="$(echo "$row" | cut -d'|' -f1)"
|
||||
domains="$(echo "$row" | cut -d'|' -f2)"
|
||||
|
||||
if [[ "$cnt" -lt "$MIN_COUNT" ]]; then
|
||||
echo "ERROR: tag '$tag' tiene $cnt funciones, minimo requerido es $MIN_COUNT." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$domains" -gt "$MAX_DOMAINS" ]]; then
|
||||
echo "ERROR: tag '$tag' aparece en $domains dominios distintos (maximo $MAX_DOMAINS). Probablemente es generico." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Promocionando tag '$tag' a capability group..."
|
||||
echo " funciones: $cnt dominios: $domains"
|
||||
echo ""
|
||||
|
||||
# Paso 1: llamar a generate_capability_doc
|
||||
local gen_script="$REGISTRY_ROOT/bash/functions/pipelines/generate_capability_doc.sh"
|
||||
if [[ ! -f "$gen_script" ]]; then
|
||||
echo "ERROR: no se encontro generate_capability_doc.sh en $gen_script" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "=> Generando docs/capabilities/${tag}.md ..."
|
||||
bash "$gen_script" "$tag"
|
||||
echo " OK"
|
||||
|
||||
# Paso 2: anadir fila a INDEX.md (idempotente)
|
||||
if [[ ! -f "$INDEX_MD" ]]; then
|
||||
echo "ERROR: no se encontro INDEX.md en $INDEX_MD" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local row_pattern
|
||||
row_pattern="| \[${tag}\](${tag}.md)"
|
||||
if grep -qF "$row_pattern" "$INDEX_MD"; then
|
||||
echo "=> Fila para '$tag' ya existe en INDEX.md — sin cambios."
|
||||
else
|
||||
echo "=> Anadiendo fila a INDEX.md ..."
|
||||
# Insertar despues de la linea de cabecera |---|---|---| de la tabla "Grupos vigentes"
|
||||
# Buscamos la linea del separador de cabecera de tabla que va despues de "## Grupos vigentes"
|
||||
local new_row="| [${tag}](${tag}.md) | ${cnt} | _(editar — promovido automaticamente)_ |"
|
||||
# Usar Python para insertar la linea de forma portable (awk no maneja bien insercion relativa)
|
||||
python3 - "$INDEX_MD" "$new_row" <<'PYEOF'
|
||||
import sys
|
||||
|
||||
index_path = sys.argv[1]
|
||||
new_row = sys.argv[2]
|
||||
|
||||
with open(index_path, "r") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
# Encontrar el bloque "Grupos vigentes" y luego la linea separadora |---|---|---|
|
||||
in_section = False
|
||||
insert_after = -1
|
||||
for i, line in enumerate(lines):
|
||||
if "## Grupos vigentes" in line:
|
||||
in_section = True
|
||||
if in_section and line.strip().startswith("|---|"):
|
||||
insert_after = i
|
||||
break
|
||||
|
||||
if insert_after == -1:
|
||||
print("ERROR: no se encontro la tabla 'Grupos vigentes' en INDEX.md", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
lines.insert(insert_after + 1, new_row + "\n")
|
||||
|
||||
with open(index_path, "w") as f:
|
||||
f.writelines(lines)
|
||||
|
||||
print(f" Fila insertada en posicion {insert_after + 1}")
|
||||
PYEOF
|
||||
echo " OK"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "HECHO. Pasos manuales pendientes:"
|
||||
echo " 1. Editar docs/capabilities/${tag}.md:"
|
||||
echo " - Anadir parrafo de descripcion del grupo."
|
||||
echo " - Completar seccion 'Ejemplo canonico' con codigo real."
|
||||
echo " - Completar seccion 'Fronteras' (que NO hace el grupo)."
|
||||
echo " - Anadir 'Notas' si aplica."
|
||||
echo " 2. Actualizar la frase descripcion en docs/capabilities/INDEX.md"
|
||||
echo " (reemplazar el placeholder con descripcion real)."
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MODO LISTAR: analizar candidatos
|
||||
# ---------------------------------------------------------------------------
|
||||
list_candidates() {
|
||||
local blocklist_sql
|
||||
blocklist_sql="$(build_blocklist_sql)"
|
||||
|
||||
# Query: tags con suficientes funciones y no demasiados dominios
|
||||
local query
|
||||
query="
|
||||
SELECT
|
||||
j.value AS tag,
|
||||
COUNT(*) AS cnt,
|
||||
COUNT(DISTINCT f.domain) AS domains,
|
||||
GROUP_CONCAT(DISTINCT f.domain) AS domain_list,
|
||||
GROUP_CONCAT(f.id) AS function_ids
|
||||
FROM functions f, json_each(f.tags) j
|
||||
WHERE j.value NOT IN (${blocklist_sql})
|
||||
GROUP BY j.value
|
||||
HAVING cnt >= ${MIN_COUNT} AND domains <= ${MAX_DOMAINS}
|
||||
ORDER BY cnt DESC;
|
||||
"
|
||||
|
||||
local raw_results
|
||||
raw_results="$(sqlite3 "$DB" "$query" 2>/dev/null || true)"
|
||||
|
||||
if [[ -z "$raw_results" ]]; then
|
||||
echo "No se encontraron candidatos con min-count=${MIN_COUNT} y max-domains=${MAX_DOMAINS}."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Obtener grupos ya existentes
|
||||
local existing_groups
|
||||
existing_groups="$(get_existing_groups)"
|
||||
|
||||
if $JSON_MODE; then
|
||||
# Salida JSON
|
||||
echo "["
|
||||
local first=true
|
||||
while IFS='|' read -r tag cnt domains domain_list function_ids; do
|
||||
[[ -z "$tag" ]] && continue
|
||||
local already_group="false"
|
||||
if echo "$existing_groups" | grep -qxF "$tag" 2>/dev/null; then
|
||||
already_group="true"
|
||||
fi
|
||||
# Tomar hasta 3 samples
|
||||
local samples
|
||||
samples="$(echo "$function_ids" | tr ',' '\n' | head -3 | tr '\n' ',' | sed 's/,$//')"
|
||||
if $first; then
|
||||
first=false
|
||||
else
|
||||
echo ","
|
||||
fi
|
||||
printf ' {"tag":"%s","count":%s,"domains":%s,"domain_list":"%s","already_group":%s,"samples":[%s]}' \
|
||||
"$tag" "$cnt" "$domains" "$domain_list" "$already_group" \
|
||||
"$(echo "$samples" | sed 's/,/","/g' | sed 's/^/"/' | sed 's/$/"/')"
|
||||
done <<< "$raw_results"
|
||||
echo ""
|
||||
echo "]"
|
||||
return
|
||||
fi
|
||||
|
||||
# Salida texto humano
|
||||
local total=0
|
||||
local already=0
|
||||
local nuevos=0
|
||||
|
||||
# Acumular lineas para contar primero
|
||||
local lines_output=()
|
||||
while IFS='|' read -r tag cnt domains domain_list function_ids; do
|
||||
[[ -z "$tag" ]] && continue
|
||||
local already_group="no"
|
||||
if echo "$existing_groups" | grep -qxF "$tag" 2>/dev/null; then
|
||||
already_group="yes"
|
||||
((already++)) || true
|
||||
else
|
||||
((nuevos++)) || true
|
||||
fi
|
||||
((total++)) || true
|
||||
# Tomar hasta 3 samples
|
||||
local samples
|
||||
samples="$(echo "$function_ids" | tr ',' '\n' | head -3 | paste -sd ',' -)"
|
||||
lines_output+=("$(printf "%-22s %-6s %-25s %-13s %s" "$tag" "$cnt" "$domain_list" "$already_group" "$samples")")
|
||||
done <<< "$raw_results"
|
||||
|
||||
printf "%-22s %-6s %-25s %-13s %s\n" "TAG" "COUNT" "DOMAINS" "ALREADY_GROUP" "SAMPLES"
|
||||
printf "%-22s %-6s %-25s %-13s %s\n" "----------------------" "------" "-------------------------" "-------------" "-------"
|
||||
for line in "${lines_output[@]}"; do
|
||||
echo "$line"
|
||||
done
|
||||
echo ""
|
||||
echo "${total} candidatos. ${already} ya son grupo. ${nuevos} son nuevos."
|
||||
echo "Promociona con: bash bash/functions/pipelines/propose_capability_groups.sh --apply <tag>"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Entry point
|
||||
# ---------------------------------------------------------------------------
|
||||
if [[ -n "$APPLY_TAG" ]]; then
|
||||
apply_tag "$APPLY_TAG"
|
||||
else
|
||||
list_candidates
|
||||
fi
|
||||
Reference in New Issue
Block a user