C26 mide presencia, no autoria: cualquier valor vale y una persona escribe el suyo. Dice quien ESCRIBIO, no quien publico, y es autodeclarado — caza la ausencia, nunca un rol equivocado. Sesion: superadmin Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1073 lines
66 KiB
Bash
1073 lines
66 KiB
Bash
#!/usr/bin/env bash
|
|
# metodo — CLI del Método. v0: sólo el linter `metodo check`.
|
|
#
|
|
# Modelo: bin/sync.sh --check de manabo-ui (raíl C11). Su anatomía, que copiamos:
|
|
# - UN solo script (nada de dos implementaciones que diverjan).
|
|
# - El testigo es una comprobación CRUDA del fichero real, no un hash guardado.
|
|
# - Veredicto DISCRETO por check, con la reparación impresa al lado.
|
|
# - Exit-code apto para CI: 0 si pasa, 1 si algo incumple.
|
|
# - Fallo benigno EXPLÍCITO (raíl 8): lo que no aplica se SALTA (SKIP), no falla.
|
|
#
|
|
# Cada AVISO cita la regla Cn de constitucion-de-facto.md que lo respalda.
|
|
# Uso: metodo check [ruta] (por defecto, el repo actual)
|
|
|
|
set -uo pipefail # -u: variable sin definir es error · pipefail: un pipe hereda el fallo.
|
|
# (NO ponemos -e: los grep sin match devuelven 1 y no queremos abortar por eso.)
|
|
|
|
ROOT="."
|
|
FAILS=0
|
|
WARNS=0 # notas: se cuentan y se dicen, pero NO mueven el código de salida (ver check 8)
|
|
SELF_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd)" # raíz del repo del script
|
|
|
|
# sha256 portable (Git Bash trae sha256sum; si no, shasum -a 256; si ninguno, la deriva no se mide)
|
|
_hash() {
|
|
if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" 2>/dev/null | awk '{print $1}'
|
|
elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" 2>/dev/null | awk '{print $1}'
|
|
else return 127; fi
|
|
}
|
|
# Lo que se estampa y se vigila. Desde el 2026-08-29 (Xavier: «vendorizar todo») incluye los
|
|
# WORKFLOWS TRANSVERSALES de la matriz, no sólo la constitución y el linter. El motivo es medido:
|
|
# un repo que se clona SOLO —el de la aplicación viaja a otra máquina— no tiene `repos/metodo/` al
|
|
# lado, así que **ningún puntero a esa ruta le resuelve**. Los principios viajaban en `metodo.md` y
|
|
# el PROCEDIMIENTO no viajaba: quien lo leyó allí no encontró el saludo invertido ni sus cinco
|
|
# líneas, que era justo lo que se le había explicado.
|
|
# ⛔ EXCEPCIÓN, y es de fondo: `sesiones-activas.md` **NO se vendoriza**. No es un documento: es un
|
|
# objeto VIVO y compartido —el semáforo del árbol—, y 27 copias de un semáforo son exactamente lo
|
|
# contrario de un semáforo. Vive sólo en la matriz, con su historial.
|
|
# ⛔ UNA SOLA LISTA. Antes había dos —el censo excluía dos ficheros y los bucles de copia
|
|
# sólo uno— y no fallaba: simplemente `sesion-ordenacion-docs.md` VIAJABA a los 27 repos
|
|
# contra su propio comentario, y encima FUERA de CHECKSUMS, así que su deriva no la medía
|
|
# nadie. La colisión de nombre que la exclusión existe para evitar llegó a estar VIVA.
|
|
# (Medido 2026-08-30 por una sesión de ejecución. Dos listas que dicen lo mismo son una
|
|
# fuente doble: §7 aplicado a la propia herramienta.)
|
|
_wf_no_viaja() { # ¿este workflow NO se vendoriza? — la ÚNICA lista
|
|
case "$1" in sesiones-activas.md|sesion-ordenacion-docs.md) return 0;; esac
|
|
return 1
|
|
}
|
|
_vendored_files() {
|
|
printf '%s\n' "metodo.md" "bin/metodo"
|
|
local f b
|
|
for f in "$SELF_ROOT"/workflows/*.md; do
|
|
[ -e "$f" ] || continue
|
|
b=$(basename "$f")
|
|
# ⛔ NO viajan: el semáforo (objeto vivo del árbol multi-repo, no un documento) y el de
|
|
# ordenación de docs — éste por COLISIÓN DE NOMBRE medida: el repo de la aplicación ya tiene
|
|
# el suyo propio (6,9 KB frente a 38 KB), y vendorizar dejaría **dos ficheros llamados igual**
|
|
# en el mismo repo, que es el riesgo que el árbol tiene escrito: «una sesión puede abrir el que
|
|
# no era y creer que su ritual de arranque es ése». Exclusión TEMPORAL: cae cuando el duplicado
|
|
# se resuelva (fusionar es un MERGE, no una limpieza — sus versiones han derivado).
|
|
_wf_no_viaja "$b" && continue
|
|
printf 'workflows/%s\n' "$b"
|
|
done
|
|
}
|
|
_write_checksums() { # $1 = dir .metodo del destino
|
|
local md="$1" f h; : > "$md/CHECKSUMS"
|
|
for f in $(_vendored_files); do
|
|
h=$(_hash "$md/$f") || { rm -f "$md/CHECKSUMS"; return 1; }
|
|
printf '%s %s\n' "$h" "$f" >> "$md/CHECKSUMS"
|
|
done
|
|
}
|
|
_is_matriz() { [ -f "$SELF_ROOT/constitucion-de-facto.md" ]; } # marca inequívoca de la matriz
|
|
|
|
# --- veredictos (los cinco colores) + 'info', que es CENSO y no veredicto ---
|
|
hdr() { printf '\n\033[1m== %s ==\033[0m\n' "$*"; }
|
|
ok() { printf ' \033[32mok \033[0m %s\n' "$*"; }
|
|
bad() { printf ' \033[31mAVISO\033[0m %s\n' "$*"; FAILS=$((FAILS+1)); }
|
|
skip() { printf ' \033[33mSKIP\033[0m %s\n' "$*"; }
|
|
warn() { printf ' \033[33mnota\033[0m %s\n' "$*"; WARNS=$((WARNS+1)); }
|
|
info() { printf ' \033[36minfo\033[0m %s\n' "$*"; } # censo, NO veredicto: no mueve ningún contador
|
|
|
|
# ── Check 1 · integridad de la copia vendorizada (C11) ───────────────────────
|
|
# El máster vive en OTRO repo (la matriz), así que el testigo no es el máster sino el
|
|
# CHECKSUM sellado al distribuir (metodo init/update). Caza si alguien editó la copia a mano.
|
|
# ⛔ Gate explícito (2026-08-30): los APÉNDICES no viajan. Hasta hoy eso se cumplía SOLO
|
|
# porque el glob `workflows/*.md` no recursa — una protección implícita, o sea algo que
|
|
# funciona por casualidad. Si alguien cambia ese glob, los apéndices empiezan a viajar a
|
|
# 27 repos en silencio. Este check lo dice.
|
|
# ⛔ Gate del semáforo (2026-08-30). Es DECIDIBLE SIN ADIVINAR —está el marcador o no está—
|
|
# y por eso se ata. Su hermana, «ninguna sesión se cierra con fila viva», NO se ata a
|
|
# propósito: el fichero no puede saber si el dueño respira, y un gate que lo adivinara daría
|
|
# rojos crónicos, que es la ceguera que este mismo Método prohíbe. Queda como criterio
|
|
# humano, y se dice — o alguien lo mecanizará «por completitud» y creará el rojo permanente.
|
|
# ⛔ Gate de FORMA de la tabla (2026-08-31). Decidible sin adivinar ⇒ se ata.
|
|
# Nace de que una fila heredó una celda de más al copiarse de otra, y NADA avisó: se pilló
|
|
# contando celdas a mano. ⚠️ Y el contador tiene su propia trampa, pagada al escribirlo:
|
|
# `awk -F'|'` NO distingue `\|` de `|`, así que decía «sigue rota» sobre una fila ya arreglada.
|
|
# Se cuentan pipes NO precedidos por barra invertida, carácter a carácter. Sin atajos.
|
|
# ⛔ Gate del REPARTO (2026-08-31). Decidible sin adivinar ⇒ se ata. Nace de una ventana real:
|
|
# tres raíles entraron a la matriz y NO se repartieron, así que la copia sellada de los 27 repos
|
|
# iba por detrás — y un puntero a `.metodo/metodo.md` apuntaba a un sitio donde el raíl no estaba.
|
|
# ⭐ Y eso es el propio raíl de §6 en su forma más pura: escrito, correcto, e invisible desde donde
|
|
# se usa. Un puntero roto NO avisa: no encuentras lo que buscabas y concluyes que no existe.
|
|
# `update` sella aquí lo que reparte; `check` compara. Sólo aplica a la MATRIZ.
|
|
_huella_reparto() { { tr -d '\r' < "$SELF_ROOT/metodo.md"; tr -d '\r' < "$SELF_ROOT/bin/metodo"; } | md5sum | cut -d' ' -f1; }
|
|
# ⛔ INCREMENTOS DECLARADOS (2026-08-31). Un repo puede necesitar documentos que el Método no
|
|
# prevé — nacen de lo que ese frente entrega, y eso no se deduce del Método. ⛔ Pero **un
|
|
# incremento SIN DECLARAR es indistinguible de una DERIVA**: quien mire el repo mañana ve un corpus
|
|
# que no cuadra y no puede saber si es «tiene un incremento aprobado» o «se fue por su cuenta».
|
|
# ⇒ Se declara en `.metodo/INCREMENTO` (del repo; NO se vendoriza), una línea por documento:
|
|
# <ruta relativa> <AAAA-MM-DD> <por qué ESTE repo lo necesita>
|
|
# Misma forma que EXCEPCIONES, que ya está probada: nominal, fechada, motivada, contada e IMPRESA.
|
|
# ⚠️ Se ata lo decidible: que lo declarado EXISTA. ⛔ Lo contrario —cazar un fichero que está y no
|
|
# está declarado— NO se ata, y se dice por qué: exigiría una definición de «línea base» del corpus
|
|
# que hoy no existe, y un gate que la adivinara daría rojos crónicos. Queda como criterio humano.
|
|
# ⛔ C26 · QUIÉN LO HIZO (2026-08-31, idea de Xavier). `git` no tiene concepto de sesión: sólo ve un
|
|
# working tree y un usuario, el mismo para todas. **El mensaje sí lo controlamos.** Un trailer con el
|
|
# ROL —no el nombre de sesión, que rota en cada reinicio— convierte los cuatro filos del árbol
|
|
# compartido en algo VISIBLE: un arrastre, un --amend ajeno o un push de lo de otra se leen en el log.
|
|
# Sesion: <rol>/<frente> p.ej. Sesion: orquestadora/securizacion · Sesion: superadmin
|
|
# Se juzga desde `sesion_desde:` de .metodo/VERSION (sellado la primera vez que este check llega al
|
|
# repo) ⇒ la historia anterior queda FUERA POR CONSTRUCCIÓN, no por una lista de excepciones que
|
|
# crecería para siempre.
|
|
check_sesion_trailer() {
|
|
local md="$ROOT/.metodo" desde n mal
|
|
[ -d "$md" ] || md="$ROOT/.metodo"
|
|
git -C "$ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1 || { skip "no es repo git — sin commits que medir"; return 0; }
|
|
hdr "cada commit dice QUIÉN lo hizo (C26)"
|
|
desde=$(grep -E '^sesion_desde:' "$md/VERSION" 2>/dev/null | sed 's/sesion_desde:[[:space:]]*//')
|
|
[ -n "$desde" ] || { info "sin punto de adopción todavía — se sella en el próximo 'metodo update'"; return 0; }
|
|
git -C "$ROOT" cat-file -e "$desde^{commit}" 2>/dev/null || { skip "el punto de adopción $desde no existe en este repo"; return 0; }
|
|
n=0; mal=0
|
|
while read -r sha; do
|
|
[ -z "$sha" ] && continue
|
|
n=$((n+1))
|
|
git -C "$ROOT" log -1 --format='%B' "$sha" | grep -qE '^Sesion:[[:space:]]*\S' || {
|
|
bad "commit sin 'Sesion:' — $(git -C "$ROOT" log -1 --format='%h %s' "$sha" | cut -c1-60) (C26) — si lo hiciste A MANO, vale 'Sesion: xavier'; si aún no está empujado, un 'git commit --amend' sobre TU commit lo cierra sin excepciones"; mal=$((mal+1)); }
|
|
done <<EOF
|
|
$(git -C "$ROOT" rev-list "$desde..HEAD" 2>/dev/null)
|
|
EOF
|
|
[ "$mal" -eq 0 ] && ok "$n commit(s) desde la adopción, todos dicen quién los hizo"
|
|
}
|
|
|
|
check_incremento() {
|
|
local f="$ROOT/.metodo/INCREMENTO"
|
|
hdr "incrementos declarados (lo que este repo tiene DE MÁS)"
|
|
[ -f "$f" ] || { ok "ninguno declarado — el corpus es el del Método"; return 0; }
|
|
local n=0 falta=0 ruta fecha resto
|
|
while read -r ruta fecha resto; do
|
|
case "$ruta" in ''|'#'*) continue;; esac
|
|
n=$((n+1))
|
|
if [ -e "$ROOT/$ruta" ]; then info "$ruta ($fecha) — $resto"
|
|
else bad "declarado y NO EXISTE: $ruta ($fecha) — un incremento declarado que no está es un puntero roto"; falta=$((falta+1)); fi
|
|
done < "$f"
|
|
[ "$falta" -eq 0 ] && ok "$n incremento(s) declarado(s), todos presentes"
|
|
}
|
|
|
|
check_reparto_al_dia() {
|
|
_is_matriz || return 0
|
|
hdr "la matriz está repartida (lo editado ha viajado)"
|
|
local sello ahora
|
|
sello=$(cat "$SELF_ROOT/.metodo/ULTIMO-REPARTO" 2>/dev/null)
|
|
ahora=$(_huella_reparto)
|
|
if [ -z "$sello" ]; then
|
|
info "sin sello de reparto todavía — se escribe en el próximo 'metodo update'"
|
|
elif [ "$sello" = "$ahora" ]; then
|
|
ok "lo que hay en la matriz es lo que se repartió"
|
|
else
|
|
bad "la MATRIZ ha cambiado desde el último reparto — corre 'metodo update' en los clientes, o su copia sellada (y cualquier puntero a ella) va por detrás"
|
|
fi
|
|
}
|
|
|
|
check_semaforo_forma() {
|
|
local f="$ROOT/workflows/sesiones-activas.md"
|
|
[ -f "$f" ] || return 0
|
|
hdr "forma de la tabla del semáforo (4 celdas por fila)"
|
|
local mal
|
|
mal=$(awk '/^\| 2026/ { l=$0; c=0; for(i=1;i<=length(l);i++){ ch=substr(l,i,1); if(ch=="|" && substr(l,i-1,1)!="\\") c++ } if(c!=5) printf "%d ", NR }' "$f")
|
|
if [ -n "$mal" ]; then
|
|
bad "fila(s) con celdas de más o de menos en línea(s): ${mal}— un | sin escapar parte la celda y el estado se lee en la columna equivocada"
|
|
else
|
|
ok "$(grep -c '^| 2026' "$f") filas, todas con 4 celdas"
|
|
fi
|
|
}
|
|
|
|
check_semaforo_en_vuelo() {
|
|
local f="$ROOT/workflows/sesiones-activas.md"
|
|
[ -f "$f" ] || return 0 # sólo aplica donde vive el semáforo (el árbol)
|
|
hdr "filas vivas con bloque EN VUELO"
|
|
# ⛔ El sujeto es LA CELDA DE ESTADO, no la línea. La v1 hacía `grep` de 🟢 sobre la línea entera
|
|
# y marcó como viva una fila CERRADA que usaba 🟢 como PROSA dentro de una predicción — el gate
|
|
# perdió su sujeto. Y una fila CERRADA/CANCELADA queda fuera pase lo que pase con su texto.
|
|
# ⛔ Una fila 🟠 DUEÑO AUSENTE NO cuenta como viva: no tiene quien sostenga su estado.
|
|
# Se excluye EXPLÍCITAMENTE, no por accidente de que el marcador rompa el ancla.
|
|
# ⛔ Y NO se usa `grep -c`: imprime 0 **y además sale con código 1**, así que un `|| echo 0`
|
|
# añadía un segundo cero y `[ -gt ]` reventaba — el gate no podía salir VERDE.
|
|
local vivas sin
|
|
vivas=$(grep -c "| 🟢" "$f" || true)
|
|
sin=$(grep "| 🟢" "$f" | grep -v "DUEÑO AUSENTE" | grep -vc "EN VUELO" || true)
|
|
if [ "$sin" -gt 0 ]; then
|
|
bad "$sin de $vivas fila(s) viva(s) sin bloque EN VUELO — una fila viva sin estado en vuelo no releva a nadie"
|
|
else
|
|
ok "$vivas fila(s) viva(s), todas con EN VUELO"
|
|
fi
|
|
}
|
|
check_apendices_no_viajan() {
|
|
hdr "los apéndices NO viajan (son de proyecto, no de método)"
|
|
local d="$ROOT/.metodo/workflows/apendices" n
|
|
if [ -d "$d" ]; then
|
|
n=$(ls "$d"/*.md 2>/dev/null | wc -l)
|
|
bad "hay $n apéndice(s) vendorizado(s) en .metodo/workflows/apendices — un apéndice presupone este árbol y NO debe viajar"
|
|
else
|
|
ok "ningún apéndice vendorizado"
|
|
fi
|
|
}
|
|
check_drift() {
|
|
hdr "integridad de la copia vendorizada (C11)"
|
|
local md="$ROOT/.metodo"
|
|
[ -f "$md/VERSION" ] || { skip "no hay .metodo/VERSION — repo sin el Método vendorizado"; return; }
|
|
local ver estado
|
|
ver=$(grep -E '^version:' "$md/VERSION" | sed 's/version:[[:space:]]*//')
|
|
estado=$(grep -E '^estado:' "$md/VERSION" | sed 's/estado:[[:space:]]*//')
|
|
ok "VERSION presente (version: ${ver:-?}, estado: ${estado:-?})"
|
|
if [ ! -f "$md/CHECKSUMS" ]; then
|
|
skip "sin CHECKSUMS — corre 'metodo init|update' para sellar la línea base de integridad"
|
|
return
|
|
fi
|
|
if ! _hash "$md/CHECKSUMS" >/dev/null 2>&1; then
|
|
skip "sin sha256sum/shasum disponible → no se puede comprobar la deriva"
|
|
return
|
|
fi
|
|
local rec f cur drift=0
|
|
while read -r rec f; do
|
|
[ -n "$f" ] || continue
|
|
cur=$(_hash "$md/$f")
|
|
if [ -z "$cur" ]; then bad "falta la copia vendorizada: $f (C11)"; drift=$((drift+1))
|
|
elif [ "$cur" != "$rec" ]; then bad "DERIVA: .metodo/$f editado a mano — nunca se edita la copia (C11); 'metodo update' o revierte"; drift=$((drift+1)); fi
|
|
done < "$md/CHECKSUMS"
|
|
[ "$drift" -eq 0 ] && ok "copia vendorizada íntegra (coincide con lo distribuido)"
|
|
}
|
|
|
|
# ── Check 2 · colisión de ids en los ficheros de estado (C17) ────────────────
|
|
# ⚠️ Este check estuvo CIEGO del 2026-08-08 al 2026-08-22 (`A6`): su regex de «id DEFINIDO» no
|
|
# admitía la COMILLA INVERTIDA, y los dos corpus más grandes del árbol escriben así sus
|
|
# decisiones — ``- **`D1` (técnica)** — …``. Veía **0** ids donde hay **130** y **118**, y con
|
|
# ellos **46 duplicados que nunca reportó**, incluidas las 18 `D1` que son la cicatriz de `D6`.
|
|
# El arreglo tiene TRES piezas, y la tercera es la que importa:
|
|
# (1) La vista se ENSANCHA: se admite cualquier adorno de markdown antes del id (`*` negrita,
|
|
# `~` tachado y la comilla invertida que faltaba).
|
|
# (2) El veredicto se PARTE, porque no todo duplicado es cerrable y un gate incerrable acaba
|
|
# apagado:
|
|
# · esquema NUEVO (nº ≥ 100, o id definido en doc/decisiones.md) → ROJO que mueve el rc.
|
|
# Es real y CERRABLE: el contador del repo es único y monótono, no debe pasar jamás.
|
|
# · legacy en backlog.md → UNA nota con el CONTEO. Renumerar está PROHIBIDO (C17) y la
|
|
# migración conserva el id como `fase·Dn` ⇒ una lista por-id sería una alarma que su
|
|
# dueño no puede cerrar. Es un MEDIDOR que drena a 0 con cada migración (D6).
|
|
# · AVISOS.md no entra en el reparto: sus ids son un contador único del repo, ahí un
|
|
# duplicado SÍ se cierra fusionando, y sigue en ROJO como hasta hoy.
|
|
# (3) ANTI-CEGUERA, que es la causa raíz y no el regex: el check DICE cuántos ids VIO por
|
|
# fichero —«vistos: 0» fue indistinguible de «no hay» durante dos semanas— y si un fichero
|
|
# tiene líneas con FORMA de id y 0 parseados, saca nota de posible ceguera. Es el
|
|
# gate-sin-sujeto aplicado al propio linter.
|
|
# Prefijos de id que reconoce el linter. ⚠️ **`B` y `M` entraron el 2026-08-22 (`D100`)**: medusa
|
|
# numera su backlog `M0`…`Mn` y hermes-sitad sus avisos `B1`…`B9`, y el linter era CIEGO a los dos
|
|
# corpus ENTEROS — 49 y 9 líneas, 0 vistas, aprobando sobre la nada desde el día uno. Lo cazó la
|
|
# nota anti-ceguera de aquí abajo en su primera ejecución sobre corpus reales.
|
|
_IDPFX='D|A|B|G|H|C|M|R|F|E|P'
|
|
|
|
# ── el reconocedor de ids ────────────────────────────────────────────────────
|
|
# ⚠️ Está en **awk y no en un `grep -oE` encadenado**, y el motivo es el LÍMITE POR LA DERECHA
|
|
# (`D101`): no se puede expresar sin lookahead. Se intentó con un segundo `grep -vE` y el filtro
|
|
# casaba `D1` + `7` dentro de `D17` ⇒ **declaraba sub-id a todo id de dos cifras**. Aquí el id y su
|
|
# cola se miran POR SEPARADO, que es la única forma de que la pregunta sea la que parece.
|
|
#
|
|
# modo `def` → **id DEFINIDO**: POSICIÓN de definición + adorno (negrita, tachado, comilla
|
|
# invertida) + prefijo de `_IDPFX`. Así una MENCIÓN inline («ver D5») no define
|
|
# nada y no da falsos positivos. Las posiciones, y de dónde salió cada una:
|
|
# · `|` fila de tabla · `-`/`*`/`_` item de lista (desde el día uno)
|
|
# · `[ ]`/`[x]`/`[~]` **casilla de ítem** — `D102`, disparada el 2026-08-23:
|
|
# `- [ ] **M5.1**` define un id tan real como el de una decisión, y eran
|
|
# **403 líneas** del árbol sin auditar.
|
|
# · `>` **blockquote** — `A43` del edge, 2026-08-23: tres decisiones de USUARIO
|
|
# de Xavier (`D23`/`D24`/`D25`) vivían ahí y **ningún censo las vio nunca**;
|
|
# peor, `check_decisiones_en_backlog` declaraba `ok` sobre ellas.
|
|
# ⛔ **Un `>` a secas NO basta, y esto es lo que separa la señal del ruido**: en
|
|
# markdown el `>` va en **TODAS** las líneas de un párrafo citado, así que una
|
|
# línea de continuación que empieza por un id («> `D6` del Método):*») no define
|
|
# nada. Medido en el árbol: admitirlo a secas metía **4 falsos positivos**
|
|
# (asgard `H33`, hub `R9`, JISR `G9`, medusa `A2`) y **3 más** en el propio
|
|
# fichero del edge (`D6`, `P11`, `D3`).
|
|
# ⇒ cuando la posición es SÓLO `>` se exige **negrita**; si el `>` viene con un
|
|
# marcador de lista o tabla detrás (`> - **D5**`), manda ése y no hace falta.
|
|
# Con la regla: 3/3 las tres `D` de `A43`, 0 falsos positivos en todo el árbol.
|
|
# modo `forma` → **¿tiene esta línea FORMA de definición?**, con CUALQUIER prefijo de letra,
|
|
# CUALQUIER adorno y `>` **a secas** (sin exigir negrita). Es a propósito agnóstico
|
|
# de `_IDPFX`, del adorno, del límite y **de la restricción de posición**: un
|
|
# detector de ceguera construido sobre el criterio que audita sólo caza la ceguera
|
|
# que ya se le ha quitado. Es la **cuarta cara** de la misma familia —prefijo
|
|
# (`gate-sin-sujeto`), adorno, límite, y ahora POSICIÓN—, y es literalmente por
|
|
# qué `A43` no saltó: las tres líneas de blockquote salían **0** también aquí
|
|
# (medido: 0/3 antes, 3/3 después), así que no estaban ni en la nota ni en el
|
|
# `no adm.` del censo. Estaban en ningún sitio.
|
|
#
|
|
# El LÍMITE POR LA DERECHA vale sólo para el estricto: `P9b`/`P9c` NO son `P9`, `E8.a` NO es `E8` y
|
|
# `R5-bis` NO es `R5` —son **sub-ids**, no colisiones, y contarlos como el padre fabricaba
|
|
# duplicados que no existen—; y `E2E` («end-to-end») no es un id en absoluto. Ninguno de los dos
|
|
# casos se puede distinguir del otro por su forma, así que ninguno cuenta como definición.
|
|
_id_scan() { # $1 = fichero · $2 = def|defnr|subid|forma
|
|
awk -v pfx="$_IDPFX" -v modo="$2" '
|
|
{ if (modo != "forma") { if (match($0, "^[[:space:]]*(>[[:space:]]*)*[|*_-]*[[:space:]]*([[].[]][[:space:]]*)?[*~`]*") == 0) next }
|
|
# ⚠️ El laxo NO lleva el grupo `(>[[:space:]]*)*` del estricto, y NO es un olvido: su adorno
|
|
# es `[^[:alnum:]]*`, que ya se traga el `>`. Se midió — con el grupo puesto, su mutación
|
|
# NO mataba ni una cara, o sea era código que no se puede romper. Quien deja aquí la
|
|
# capacidad de ver el blockquote es la GUARDA de abajo, y por eso su clase lleva el `>`.
|
|
else { if (match($0, "^[[:space:]]*[|*_-]?[[:space:]]*([[].[]])?[^[:alnum:]]*") == 0) next }
|
|
pre = substr($0, 1, RLENGTH)
|
|
# Con todo opcional, el patrón casa la cadena VACÍA en cualquier línea ⇒ hace falta exigir
|
|
# que se haya visto AL MENOS un marcador de posición. Sin esto, una frase que empieza por
|
|
# un id sería una definición.
|
|
if (pre !~ /[|*>_-]/) next
|
|
# ⛔ El `>` de markdown NO marca «aquí empieza algo»: marca «esta línea está citada», y va en
|
|
# todas las del párrafo. Sólo cuenta como posición si trae NEGRITA (o un marcador de lista).
|
|
if (modo != "forma" && pre ~ /^[[:space:]]*>/ && pre !~ /[|_-]/ && pre !~ /\*\*/) next
|
|
rest = substr($0, RLENGTH + 1)
|
|
re = (modo == "forma") ? "^[A-Z][A-Z]?[0-9]+" : "^(" pfx ")[0-9]+"
|
|
if (match(rest, re) == 0) next
|
|
id = substr(rest, 1, RLENGTH); cola = substr(rest, RLENGTH + 1)
|
|
# ⚠️ El LÍMITE POR LA DERECHA (`D101`) NO se aplica al modo `forma`: ese es el detector de
|
|
# «el sujeto está y no lo veo», y un detector que descarta lo mismo que el criterio deja de
|
|
# detectar nada. Se midió: con el límite puesto, medusa decía 12 líneas con forma de id
|
|
# donde hay 49. Es la MISMA lección que ya obligó a hacerlo agnóstico de prefijo.
|
|
if (modo != "forma") {
|
|
if (cola ~ /^[[:alnum:]]/ ) { if (modo == "subid") print "1"; next } # P9b / E2E
|
|
if (cola ~ /^[.-][[:alnum:]]/ ) { if (modo == "subid") print "1"; next } # E8.a / R5-bis
|
|
}
|
|
if (modo == "def") print id
|
|
else if (modo == "defnr") print NR "\t" id # el consumidor necesita SABER en qué línea
|
|
else if (modo == "forma") print "1" }' "$1"
|
|
}
|
|
_ids_def() { _id_scan "$1" def; }
|
|
_lineas_forma_id() { _id_scan "$1" forma | grep -c "^1$"; }
|
|
_ids_def_nr() { _id_scan "$1" defnr; } # <línea> <id> — para quien necesite CASAR con el fichero
|
|
_subids() { _id_scan "$1" subid | grep -c "^1$"; } # lo que el LÍMITE tira: se dice, no se calla
|
|
|
|
check_ids() {
|
|
hdr "colisión de ids en los ficheros de estado (C17)"
|
|
# Ids del esquema NUEVO que ya viven en la cuarta capa: un duplicado suyo es rojo aunque su
|
|
# número sea < 100 (un legacy MIGRADO ya tiene dueño que puede cerrarlo).
|
|
local dec="$ROOT/doc/decisiones.md" nuevos_dec=""
|
|
[ -f "$dec" ] && nuevos_dec=$(awk '/^### /{ s=$0; gsub(/[*`]/,"",s); split(s,a,"[ \t]+"); if (a[2]!="") print a[2] }' "$dec")
|
|
local any=0 f base ids vistos sueltos sub fuera detalle dups id num rojos legacy
|
|
for f in "$ROOT/doc/AVISOS.md" "$ROOT/doc/backlog.md"; do
|
|
[ -f "$f" ] || continue
|
|
any=1; base=$(basename "$f")
|
|
ids=$(_ids_def "$f")
|
|
vistos=$(printf '%s\n' "$ids" | grep -c '[^[:space:]]')
|
|
if [ "$vistos" -eq 0 ]; then
|
|
sueltos=$(_lineas_forma_id "$f")
|
|
if [ "${sueltos:-0}" -gt 0 ]; then
|
|
warn "$base: 0 ids DEFINIDOS parseados pero ${sueltos} línea(s) con forma de id — posible CEGUERA del regex (A6): el verde de este fichero no prueba nada, compruébalo a mano"
|
|
else
|
|
info "$base: 0 ids definidos y 0 líneas con forma de id — no hay sujeto que medir"
|
|
fi
|
|
continue
|
|
fi
|
|
# ── el CENSO, y es censo COMPLETO a propósito ────────────────────────────────────────────
|
|
# Dice lo que VE, lo que TIRA (`D100` descarta sub-ids: `P9b` no es `P9`) y lo que NO ADMITE
|
|
# (casillas `- [ ]`, prefijos fuera de `_IDPFX`). Un descarte callado es un cap silencioso:
|
|
# medusa tiene 7 ids y **49** líneas con forma de id, y sin esta línea el censo diría «7» a
|
|
# secas. La nota de ceguera de arriba sólo salta con `vistos == 0` ⇒ la ceguera PARCIAL se
|
|
# escaparía entera.
|
|
sub=$(_subids "$f"); sueltos=$(_lineas_forma_id "$f")
|
|
fuera=$(( sueltos - vistos - sub )); [ "$fuera" -lt 0 ] && fuera=0
|
|
detalle=""
|
|
[ "${sub:-0}" -gt 0 ] && detalle="$detalle · ${sub} sub-id(s) descartados (D100)"
|
|
[ "$fuera" -gt 0 ] && detalle="$detalle · ${fuera} línea(s) con forma de id que el censo estricto NO admite"
|
|
info "$base: ${vistos} id(s) DEFINIDOS vistos${detalle}"
|
|
rojos=0; legacy=0
|
|
dups=$(printf '%s\n' "$ids" | sort | uniq -d)
|
|
if [ -n "$dups" ]; then
|
|
while IFS= read -r id; do
|
|
[ -n "$id" ] || continue
|
|
num=${id#[A-Z]}
|
|
if { [ "${num:-0}" -ge 100 ] 2>/dev/null; } || printf '%s\n' "$nuevos_dec" | grep -qxF "$id"; then
|
|
bad "id duplicado en $base: $id — id de ESQUEMA NUEVO: citar uno gobierna con el otro (C17); renumera el nuevo desde el contador del repo"; rojos=$((rojos+1))
|
|
elif [ "$base" = "backlog.md" ]; then
|
|
legacy=$((legacy+1))
|
|
else
|
|
bad "id duplicado en $base: $id — renombra o fusiona (C17)"; rojos=$((rojos+1))
|
|
fi
|
|
done <<< "$dups"
|
|
fi
|
|
[ "$legacy" -gt 0 ] && warn "$base: ${legacy} id(s) legacy duplicados (de ${vistos} vistos) — se resuelven MIGRANDO a doc/decisiones.md y citando \`fase·Dn\`, NUNCA renumerando (C17 · D6); es un medidor que drena, no una alarma"
|
|
[ "$rojos" -eq 0 ] && [ "$legacy" -eq 0 ] && ok "$base: sin ids duplicados"
|
|
done
|
|
[ "$any" -eq 0 ] && skip "sin doc/AVISOS.md ni doc/backlog.md (repo sin corpus de estado)"
|
|
return 0
|
|
}
|
|
|
|
# ── Check 3 · encabezados sin marca de estado (C16) ──────────────────────────
|
|
check_headings() {
|
|
hdr "encabezados sin marca de estado (C16)"
|
|
local f hits=0 line
|
|
# ⛔ `workflows/` entró el 2026-08-30: sus enlaces NO los miraba nadie, y por eso
|
|
# `comunicacion-entre-sesiones.md` llevaba 2 enlaces ROTOS en la matriz Y en los 27 repos.
|
|
# Un fichero cuyo único trabajo es LLEVAR al Método apuntaba a un sitio que no existe.
|
|
for f in "$ROOT"/*.md "$ROOT"/doc/*.md "$ROOT"/workflows/*.md "$ROOT"/workflows/apendices/*.md; do
|
|
[ -f "$f" ] || continue
|
|
# Un encabezado ## no lleva emoji de ESTADO (el estado vive en las casillas).
|
|
# ⚠️ se excluye a propósito: en un título es "bandera/aviso", no estado de sección.
|
|
while IFS= read -r line; do
|
|
bad "encabezado con estado en $(basename "$f"): ${line} — el estado va en las casillas (C16)"
|
|
hits=$((hits+1))
|
|
done < <(grep -nE '^#{2,6} .*(✅|🟢|🔴|🟠|🟡|❌)' "$f")
|
|
done
|
|
[ "$hits" -eq 0 ] && ok "ningún encabezado ##+ lleva marca de estado"
|
|
}
|
|
|
|
# ── Check 4 · enlaces locales resuelven (integridad del corpus) ──────────────
|
|
check_links() {
|
|
hdr "enlaces locales resuelven (integridad)"
|
|
local f dir tgt found=0 broken=0
|
|
# ⛔ `workflows/` entró el 2026-08-30: sus enlaces NO los miraba nadie, y por eso
|
|
# `comunicacion-entre-sesiones.md` llevaba 2 enlaces ROTOS en la matriz Y en los 27 repos.
|
|
# Un fichero cuyo único trabajo es LLEVAR al Método apuntaba a un sitio que no existe.
|
|
for f in "$ROOT"/*.md "$ROOT"/doc/*.md "$ROOT"/workflows/*.md "$ROOT"/workflows/apendices/*.md; do
|
|
[ -f "$f" ] || continue
|
|
dir=$(dirname "$f")
|
|
# process substitution (< <(...)) para que el while corra en ESTE shell y bad() cuente
|
|
# (un `grep | while` correría en un subshell y perdería el contador — raíl 1: el testigo
|
|
# tiene que medir de verdad, no en un proceso que se traga el resultado).
|
|
while IFS= read -r tgt; do
|
|
case "$tgt" in http*|\#*|mailto:*|"") continue;; esac
|
|
tgt="${tgt%%#*}" # quita el ancla #seccion
|
|
[ -z "$tgt" ] && continue
|
|
found=$((found+1))
|
|
[ -e "$dir/$tgt" ] || { bad "enlace roto en $(basename "$f"): $tgt"; broken=$((broken+1)); }
|
|
done < <(grep -oE '\]\([^)]+\)' "$f" | sed -E 's/^\]\(//; s/\)$//')
|
|
done
|
|
[ "$broken" -eq 0 ] && ok "$found enlaces locales, todos resuelven"
|
|
}
|
|
|
|
# ── Check 5 · commits Conventional DESDE LA ADOPCIÓN (C25) ───────────────────
|
|
# Sólo se juzga lo NUEVO: la adopción es brownfield HACIA ADELANTE, el historial previo no se
|
|
# reescribe → no se juzga (juzgarlo dejaría un repo brownfield rojo para siempre — raíl 5/C35).
|
|
# El punto de adopción se sella en .metodo/VERSION (adopted_commit); sin sello = greenfield → todo.
|
|
check_commits() {
|
|
hdr "commits Conventional desde la adopción (C25)"
|
|
git -C "$ROOT" rev-parse --git-dir >/dev/null 2>&1 || { skip "no es un repo git"; return; }
|
|
local since range n s bad_c=0
|
|
since=$(grep -E '^adopted_commit:' "$ROOT/.metodo/VERSION" 2>/dev/null | sed 's/adopted_commit:[[:space:]]*//')
|
|
if [ -n "$since" ] && git -C "$ROOT" rev-parse --verify -q "$since^{commit}" >/dev/null 2>&1; then
|
|
range="${since}..HEAD"
|
|
else
|
|
range="HEAD"; since="" # sin marcador: repo nuevo → se juzga todo el historial
|
|
fi
|
|
n=$(git -C "$ROOT" rev-list --count $range 2>/dev/null || echo 0)
|
|
[ "${n:-0}" -eq 0 ] && { skip "sin commits nuevos desde la adopción (nada que revisar)"; return; }
|
|
# `semaforo` es un tipo PROPIO de esta casa, añadido el 2026-08-28 (`D109`, Xavier). No es
|
|
# Vocabulario ampliado el 2026-08-30 con LAS CUATRO CAPAS del corpus (`avisos`, `bitacora`,
|
|
# `decisiones`, `backlog`) + `corpus`, `evidencia`, `banco`. Motivo medido: un frente salió con
|
|
# 16 avisos C25 y NINGUNO era un commit mal escrito — eran `avisos:` y `bitacora:`, que en este
|
|
# método SON tipos de cambio de primera clase. La vara estaba mal, no el trabajo.
|
|
# Conventional estándar: nombra las escrituras en el semáforo del árbol, que desde ese día vive
|
|
# en la matriz y lo editan sesiones de TODOS los frentes. Se admitió porque el alternativo era
|
|
# dejar el gate en rojo permanente por un commit ya empujado — y un gate que no puede ponerse
|
|
# verde entrena a ignorar los rojos (`D107`, misma razón que lo puso verde por primera vez).
|
|
# ⛔ Lo que NO se hizo: mover `adopted_commit` hacia delante para callarlo. Eso es editar la
|
|
# declaración hasta que la alerta calle (§1), y además perdona todo lo que haya en medio.
|
|
# ── EXCEPCIONES DECLARADAS (2026-08-29, Xavier) ────────────────────────────
|
|
# El problema que resuelven, medido: el PRIMERO que rompe C25 con historia YA EMPUJADA condena su
|
|
# repo a rojo permanente — reescribir está descartado (los shas se citan en corpus de OTROS
|
|
# frentes y hay commits encima) y mover `adopted_commit` está prohibido (§1: editar la declaración
|
|
# hasta que la alerta calle). A partir de ahí `metodo check` deja de significar nada en ese repo,
|
|
# y ÉSE es el daño, no los commits.
|
|
# ⚠️ Y no vale ensanchar la gramática como con `semaforo:`: aquello era un TIPO legítimo y
|
|
# recurrente; un mensaje mal escrito es un ERROR, y para el error no había salida ninguna.
|
|
# ⇒ Se admite excepción, pero **DECLARADA**, con las cuatro propiedades que la separan de callar:
|
|
# (1) NOMINAL, por sha exacto y uno por línea: no hay rangos ni patrones, así que no se puede
|
|
# "declarar de más" sin escribir cada caso a mano.
|
|
# (2) FECHADA Y MOTIVADA: la línea dice cuándo y por qué no se puede arreglar.
|
|
# (3) VISIBLE: se CUENTAN y se IMPRIMEN siempre. El gate no dice «pasa» a secas, dice con
|
|
# cuántas excepciones pasa — el coste se sigue viendo; lo que desaparece es el ruido eterno.
|
|
# (4) NO PERDONA EL FUTURO: sólo silencia los shas escritos. Ésa es la diferencia con mover el
|
|
# sello, que además perdona todo lo que haya en medio. Probado con control negativo.
|
|
# Formato de `.metodo/EXCEPCIONES` (del repo; NO se vendoriza — cada uno lleva las suyas):
|
|
# C25 <sha> <AAAA-MM-DD> <motivo en una línea>
|
|
local excf="$ROOT/.metodo/EXCEPCIONES" exc_c=0 sha subj
|
|
while IFS=$'\t' read -r sha subj; do
|
|
[ -n "$sha" ] || continue
|
|
printf '%s' "$subj" | grep -qE '^(feat|fix|docs|ci|refactor|test|chore|build|perf|deploy|doc|semaforo|semáforo|avisos|semaforo|semáforo|avisos|bitacora|bitácora|decisiones|backlog|corpus|evidencia|banco|fix|docs|ci|refactor|test|chore|build|perf|semaforo|semáforo)(\(.+\))?!?: .' && continue
|
|
if [ -f "$excf" ] && grep -qE "^C25[[:space:]]+${sha}([[:space:]]|$)" "$excf"; then
|
|
exc_c=$((exc_c+1)); continue
|
|
fi
|
|
bad "commit no-Conventional: \"$subj\" ($sha) (C25) — arréglalo, o decláralo en .metodo/EXCEPCIONES si ya está empujado"
|
|
bad_c=$((bad_c+1))
|
|
done < <(git -C "$ROOT" log --format="%h%x09%s" $range 2>/dev/null)
|
|
[ "$exc_c" -gt 0 ] && warn "C25: ${exc_c} commit(s) con EXCEPCIÓN DECLARADA en .metodo/EXCEPCIONES (historia ya empujada; NO perdona lo que venga después)"
|
|
[ "$bad_c" -eq 0 ] && ok "los ${n} commit(s) desde la adopción siguen Conventional$([ "$exc_c" -gt 0 ] && printf ' (%s con excepción declarada)' "$exc_c")"
|
|
}
|
|
|
|
# ── Check 6 · el puntero de enrutado existe y RESUELVE (raíl 8) ──────────────
|
|
# Un puntero sin gate se pudre: es la línea que nadie lee de mañana. Y no basta con que el bloque
|
|
# esté — su DESTINO tiene que resolver, porque un puntero a la nada enruta a la nada (y encima con
|
|
# la conciencia tranquila de haberlo puesto). Sabe ponerse rojo de tres formas distintas:
|
|
# sin CLAUDE.md · con CLAUDE.md pero sin bloque · con bloque cuyo destino no existe.
|
|
check_pointer() {
|
|
hdr "puntero de enrutado en CLAUDE.md (raíl 8)"
|
|
[ -d "$ROOT/.metodo" ] || { skip "no hay .metodo/ — repo sin el Método vendorizado"; return; }
|
|
local cm="$ROOT/CLAUDE.md"
|
|
[ -f "$cm" ] || { bad "no hay CLAUDE.md — el Método VIAJA pero nada lo enruta (raíl 8); 'metodo update' desde la matriz"; return; }
|
|
if ! grep -qxF '<!-- metodo:puntero -->' "$cm" || ! grep -qxF '<!-- /metodo:puntero -->' "$cm"; then
|
|
bad "CLAUDE.md sin el bloque 'metodo:puntero' — el Método viaja y nada lo abre; 'metodo update'"; return
|
|
fi
|
|
ok "CLAUDE.md lleva el bloque del puntero"
|
|
local tgt found=0 broken=0 l
|
|
while IFS= read -r l; do
|
|
tgt="${l%%#*}"; [ -z "$tgt" ] && continue
|
|
found=$((found+1))
|
|
[ -e "$ROOT/$tgt" ] || { bad "el puntero apunta a algo que NO EXISTE: $tgt"; broken=$((broken+1)); }
|
|
done < <(awk '/^<!-- metodo:puntero -->$/{f=1;next} /^<!-- \/metodo:puntero -->$/{f=0} f' "$cm" \
|
|
| grep -oE '\]\([^)]+\)' | sed -E 's/^\]\(//; s/\)$//')
|
|
[ "$found" -eq 0 ] && { bad "el bloque del puntero no lleva NINGÚN enlace — no enruta a nada"; return; }
|
|
[ "$broken" -eq 0 ] && ok "el destino del puntero resuelve ($found enlace(s) comprobado(s))"
|
|
}
|
|
|
|
# ── Check 7 · la capa de DECISIONES: ids únicos (rojo) y campos (nota) ───────
|
|
# Cuarta capa del corpus (`D6`, 2026-08-22). Existe porque una decisión guardada en el backlog se
|
|
# numera por FASE, y la numeración por fase colisiona: 18 decisiones distintas llamadas `D1` en un
|
|
# solo backlog.md. Por eso el id duplicado va en ROJO y no en nota — es AMBIGÜEDAD DE CITA sobre lo
|
|
# peligroso del repo: un nombre que apunta a dos decisiones distintas hace que citar una gobierne
|
|
# con la otra. Los CAMPOS van en nota: un repo que acaba de estrenar la capa no se pone rojo por
|
|
# terminar de rellenarla.
|
|
# Una entrada se DEFINE con un encabezado `### <id> — <título>`, y el id es el primer token: así una
|
|
# mención inline ("ver D107") no define nada y el testigo no puede dar falsos positivos.
|
|
check_decisiones() {
|
|
hdr "capa de decisiones · ids y campos (C17 · D6)"
|
|
local f="$ROOT/doc/decisiones.md"
|
|
[ -f "$f" ] || { skip "sin doc/decisiones.md — la cuarta capa se adopta cuando la orquestadora del repo migre sus 'D' (D6)"; return; }
|
|
local ids n dups id
|
|
ids=$(awk '/^### /{ s=$0; gsub(/\*/,"",s); split(s,a,"[ \t]+"); if (a[2]!="") print a[2] }' "$f")
|
|
n=$(printf '%s\n' "$ids" | grep -c '[^[:space:]]')
|
|
if [ "$n" -eq 0 ]; then
|
|
warn "doc/decisiones.md no tiene NINGUNA entrada '### <id> — <título>' — un registro vacío no gatea nada (D6)"
|
|
return
|
|
fi
|
|
dups=$(printf '%s\n' "$ids" | sort | uniq -d)
|
|
if [ -n "$dups" ]; then
|
|
while IFS= read -r id; do
|
|
[ -n "$id" ] && bad "id duplicado en decisiones.md: $id — dos decisiones con el mismo nombre: citar una gobierna con la otra (C17); renumera la NUEVA desde el contador del repo"
|
|
done <<< "$dups"
|
|
else
|
|
ok "decisiones.md: $n entrada(s), sin ids duplicados"
|
|
fi
|
|
# Campos obligatorios de cada entrada: dueño (Xavier / técnica — una técnica escalada como suya
|
|
# ya coló) y estado (vigente / sustituida — sin él, una decisión muerta sigue mandando).
|
|
local faltan miss
|
|
faltan=$(awk '
|
|
function flush( m){ if (id!="") { m=""; if(!hd) m=m" dueño"; if(!he) m=m" estado";
|
|
if (m!="") print id "\t" m } id=""; hd=0; he=0 }
|
|
/^### / { flush(); s=$0; gsub(/\*/,"",s); split(s,a,"[ \t]+"); id=a[2]; next }
|
|
/^#{1,2} / { flush(); next }
|
|
{ if ($0 ~ /\*\*due/) hd=1
|
|
if ($0 ~ /\*\*estado/) he=1 }
|
|
END { flush() }' "$f")
|
|
if [ -n "$faltan" ]; then
|
|
while IFS=$'\t' read -r id miss; do
|
|
[ -n "$id" ] && warn "decisiones.md · «$id» sin:$miss — el dueño y el estado son el gate de la entrada (D6)"
|
|
done <<< "$faltan"
|
|
else
|
|
ok "todas las entradas llevan dueño y estado"
|
|
fi
|
|
}
|
|
|
|
# ── Check 8 · textos de DECISIÓN dentro del backlog (nota) ───────────────────
|
|
# Invierte el gradiente de comodidad: escribir la decisión en el sitio equivocado pasa a ser lo que
|
|
# cuesta. Se mide el BLOQUE —la línea que define `Dn` más sus líneas de continuación indentadas—;
|
|
# más de 3 líneas ya es un cuerpo de decisión, no una referencia.
|
|
# ⚠️ CALIBRADO para no enrojecer a los backlogs legacy que aún no han migrado, y SÍ se pueden
|
|
# distinguir: los ids nuevos nacen en `D100` (rango limpio — ningún legacy del árbol llega a 100).
|
|
# ⇒ `Dn` con n>=100 y cuerpo = decisión escrita HOY en el sitio equivocado → nota nominal.
|
|
# ⇒ `Dn` con n<100 = deuda de migración de la orquestadora de ese repo → se CUENTA y se dice, no se
|
|
# juzga. Y ninguno de los dos mueve el código de salida a propósito: la capa se estrena hoy y
|
|
# romper el gate de 21 repos por deuda ajena es cómo se aprende a apagar el gate.
|
|
check_decisiones_en_backlog() {
|
|
hdr "textos de decisión dentro del backlog (D6)"
|
|
local f="$ROOT/doc/backlog.md"
|
|
[ -f "$f" ] || { skip "sin doc/backlog.md (repo sin corpus de estado)"; return; }
|
|
# ⚠️ `A8`: qué línea ES una definición de id lo decide **`_id_scan` y sólo él**. Este check
|
|
# llevaba su propia copia del patrón, y por eso NO obedecía a `D101` (un `D9b` contaba como
|
|
# `D9`). Es la lección de `A6` en su forma más literal —*el detector no hereda ni DUPLICA el
|
|
# criterio que audita*— y estaba escrita desde el día uno en la cabecera del script: «UN solo
|
|
# script, nada de dos implementaciones que diverjan». Aquí sólo se añade lo propio: quedarse
|
|
# con los ids `D` y medir el TAMAÑO del bloque.
|
|
# ⚠️ Las definiciones entran por `-v`, NO como segundo fichero: con el truco `NR==FNR` y la
|
|
# lista VACÍA (un backlog sin ids reconocidos) la regla se traga el fichero entero y el check
|
|
# mide sobre la nada **en silencio**. Lo destapó el arnés: el mutante que le devuelve su copia
|
|
# al check 8 no llegaba a ejecutarse nunca.
|
|
local defs; defs=$(_ids_def_nr "$f")
|
|
local out legacy=0 nuevos=0 id len num
|
|
out=$(awk -v defs="$defs" '
|
|
BEGIN { n=split(defs, L, "\n"); for (i=1;i<=n;i++) if (split(L[i], P, "\t")==2) DEF[P[1]+0]=P[2] }
|
|
function flush(){ if (id!="" && len>3) print id "\t" len; id=""; len=0 }
|
|
(FNR in DEF) { flush(); if (DEF[FNR] ~ /^D[0-9]+$/) { id=DEF[FNR]; len=1 } ; next }
|
|
/^[[:space:]]+[^[:space:]]/ { if (id!="") { len++; next } }
|
|
{ flush() }
|
|
END { flush() }' "$f")
|
|
if [ -n "$out" ]; then
|
|
while IFS=$'\t' read -r id len; do
|
|
[ -n "$id" ] || continue
|
|
num=${id#D}
|
|
if [ "$num" -ge 100 ] 2>/dev/null; then
|
|
nuevos=$((nuevos+1))
|
|
warn "«$id» lleva $len líneas de texto de decisión dentro de backlog.md — las decisiones viven en doc/decisiones.md (D6)"
|
|
else
|
|
legacy=$((legacy+1))
|
|
fi
|
|
done <<< "$out"
|
|
fi
|
|
[ "$legacy" -gt 0 ] && ok "$legacy texto(s) de decisión con id legacy (<D100) en backlog.md: migran cuando su orquestadora migre (D6) — las NUEVAS ya no se escriben aquí"
|
|
[ "$nuevos" -eq 0 ] && [ "$legacy" -eq 0 ] && ok "ninguna decisión con cuerpo dentro de backlog.md"
|
|
return 0
|
|
}
|
|
|
|
# ── Check 9 · decisiones con forma de PROSA, sin id (nota) ───────────────────
|
|
# `A43`(c), 2026-08-23. ⛔ **El linter NO intenta leer prosa**: reconocer decisiones por su
|
|
# contenido es un pozo, y la decisión (superadmin) fue explícita en no bajar ahí. Esto no lee
|
|
# prosa — mira un **marcador que el autor ya escribió**, en MAYÚSCULAS, y pregunta una sola cosa:
|
|
# ¿hay un id de registro cerca? Si no lo hay, saca **nota**. Señala, no gatea.
|
|
#
|
|
# La cicatriz: `I11.14` del hub —decisión de XAVIER, con dueño y fecha en su propia ficha— nunca
|
|
# tuvo id. Camino del registro se resumió, y el resumen **invertía el sujeto** («al edge le
|
|
# consta» por «a este hub le consta») y **perdía la mitad estrecha** («de ese llamante»): habría
|
|
# entrado en el registro mintiendo desde la primera línea. Un id no es burocracia — es lo que
|
|
# ata una decisión a **su texto**.
|
|
#
|
|
# ⚠️ La forma canónica sigue siendo UNA (`D6`): una entrada en `doc/decisiones.md` con id. Esta
|
|
# nota sólo dice *«aquí hay una decisión escrita a mano y sin número»*; **quién le pone el número
|
|
# es su dueño**, no una sesión de paso y desde luego no el linter.
|
|
_PROSA_MARCA='DECISI' # MAYÚSCULAS a propósito: «la decisión de X» en prosa corriente no es marca
|
|
_PROSA_CERCA=2 # líneas arriba/abajo donde vale un id de registro para dar por citada
|
|
_PROSA_TOPE=5 # tope de notas por fichero — y lo que caiga fuera se DICE, no se calla
|
|
check_decisiones_en_prosa() {
|
|
hdr "decisiones con forma de prosa, sin id (D6 · A43)"
|
|
local any=0 f base out n total line txt
|
|
for f in "$ROOT/doc/backlog.md" "$ROOT/doc/AVISOS.md"; do
|
|
[ -f "$f" ] || continue
|
|
any=1; base=$(basename "$f")
|
|
# El id "cerca" se busca como `D<n>` —el vocabulario del REGISTRO— y no como id cualquiera:
|
|
# `I11.14` es el id del ÍTEM, y tenerlo al lado es justo el caso que hay que denunciar.
|
|
out=$(awk -v M="$_PROSA_MARCA" -v N="$_PROSA_CERCA" '
|
|
{ L[NR]=$0 }
|
|
END { for (i=1;i<=NR;i++) {
|
|
if (L[i] !~ M) continue
|
|
cerca=0
|
|
for (j=i-N; j<=i+N; j++) if (j>=1 && j<=NR && L[j] ~ /D[0-9]+/) cerca=1
|
|
if (!cerca) print i "\t" substr(L[i], 1, 110) } }' "$f")
|
|
total=$(printf '%s\n' "$out" | grep -c '[^[:space:]]')
|
|
if [ "$total" -eq 0 ]; then
|
|
ok "$base: ningún marcador de decisión sin id de registro cerca"
|
|
continue
|
|
fi
|
|
n=0
|
|
while IFS=$'\t' read -r line txt; do
|
|
[ -n "$line" ] || continue
|
|
n=$((n+1)); [ "$n" -gt "$_PROSA_TOPE" ] && continue
|
|
warn "decisión con forma de prosa, sin id — $base:$line · ${txt} … ⇒ si es una decisión, va a doc/decisiones.md con id y su TEXTO ÍNTEGRO (D6); si no lo es, baja el marcador a minúsculas"
|
|
done <<< "$out"
|
|
[ "$total" -gt "$_PROSA_TOPE" ] && warn "$base: y ${total} en total — sólo se listan las primeras ${_PROSA_TOPE} (el tope se dice, no se calla)"
|
|
done
|
|
[ "$any" -eq 0 ] && skip "sin doc/backlog.md ni doc/AVISOS.md (repo sin corpus de estado)"
|
|
return 0
|
|
}
|
|
|
|
# ── plantillas de doc/ (sólo se crean si faltan — brownfield: no pisar) ───────
|
|
_scaffold_docs() { # $1 = repo destino
|
|
# La casa de docs es UNA y se llama doc/ (el linter ancla ahí el corpus de estado).
|
|
[ -d "$1/doc" ] && { echo " (doc/ ya existe → no scaffoldeo el corpus)"; return 0; }
|
|
# Una casa con otro nombre NO se duplica en silencio: se pide el rename (cicatriz: kubernetes
|
|
# tenía documentacion/, el guard no la conocía y el init dejó DOS carpetas de documentación).
|
|
local alt; for alt in docs documentacion documentation; do
|
|
[ -d "$1/$alt" ] && { echo " ⚠ $alt/ existe pero la convención es doc/ — haz 'git mv $alt doc' (si no, el linter no ve el corpus de estado). No scaffoldeo."; return 0; }
|
|
done
|
|
local d="$1/doc"; mkdir -p "$d"
|
|
[ -f "$d/backlog.md" ] || cat > "$d/backlog.md" <<'EOF'
|
|
# Backlog
|
|
|
|
> Cuatro capas: **este backlog** = el eje del TROCEO (metas + items; un item ≈ una sesión) ·
|
|
> decisiones.md = lo peligroso, todo junto · AVISOS.md = bandera (se lee primero) ·
|
|
> bitacora.md = relato append-only. Método vendorizado en ../.metodo/metodo.md.
|
|
> ⛔ Las decisiones son **inmutables**: contradecir = PARAR y reportar. Un `##` no lleva marca de
|
|
> estado; abierto = tiene `[ ]`.
|
|
|
|
## Decisiones
|
|
|
|
Viven en **[decisiones.md](decisiones.md)** (cuarta capa, `D6`). Aquí se **citan por id**, no se
|
|
copian: escribirlas en los dos sitios es crear la deriva que §7 existe para impedir.
|
|
|
|
## Abierto
|
|
- [ ] (siembra aquí el trabajo ABIERTO de hoy — brownfield: el pasado no se reconstruye)
|
|
EOF
|
|
[ -f "$d/decisiones.md" ] || cat > "$d/decisiones.md" <<'EOF'
|
|
# Decisiones
|
|
|
|
> **Cuarta capa del corpus** (`D6`, Xavier 2026-08-22): backlog = el eje del troceo ·
|
|
> **este fichero = lo peligroso, TODO JUNTO** · AVISOS.md = bandera, se lee primero ·
|
|
> bitacora.md = relato append-only. Los raíles, en ../.metodo/metodo.md (§7: el destino se decide
|
|
> UNA vez).
|
|
>
|
|
> ⛔ **Una decisión `vigente` no se contradice**: o **PARAS y reportas**, o la **sustituyes**
|
|
> explícitamente — y la sustitución **edita la entrada vieja** (⚰️ + puntero a la nueva), nunca
|
|
> deja las dos vivas.
|
|
> ⛔ **Antes de cerrar nada, recorre el ámbito y mira QUÉ CONTRADICES.** Ésa es la fuerza que
|
|
> mantiene vivo este documento: no hay gate automático que la sustituya.
|
|
>
|
|
> **Orden: por ÁMBITO, no por fase** — numerar por fase es lo que fabricó **18 decisiones distintas
|
|
> llamadas `D1`** en un solo backlog. **Ids nuevos: contador único del repo desde `D100`**,
|
|
> monótono, jamás reiniciado por sección. Los legacy migran con su **id INTACTO**, citados
|
|
> `fase·Dn` (p. ej. `I11·D18`): C17 prohíbe renumerar, y renumerar rompe las citas cruzadas.
|
|
>
|
|
> **Formato**: una entrada = un `### <id> — <título>`. El id es el primer token (es lo que lee
|
|
> `metodo check`). Los campos **dueño** y **estado** son el gate de la entrada.
|
|
|
|
## Ámbito: (nombre del ámbito — p. ej. "transporte", "identidad", "panel")
|
|
|
|
### D100 — (título corto, sin punto final)
|
|
|
|
- **dueño**: Xavier · **fecha**: 2026-01-01 · **estado**: vigente
|
|
- **sustituye**: — · **testigo**: — *(si la decisión AFIRMA algo medible, el testigo y su condición
|
|
van AQUÍ, en esta misma línea: la clase de fallo del «VALIDADO» con su condición 116 líneas
|
|
más abajo)*
|
|
|
|
(Texto **ÍNTEGRO** de la decisión, en las palabras en que se tomó. **Nunca un resumen**: un resumen
|
|
adquiere invariantes que la decisión no tiene, y ya gobernó una sesión entera en lugar de la `D`.
|
|
Sustituye este ejemplo por tu primera decisión real — si dejas el `D100` puesto, la primera de
|
|
verdad colisiona con él y `metodo check` se pone rojo.)
|
|
EOF
|
|
[ -f "$d/AVISOS.md" ] || cat > "$d/AVISOS.md" <<'EOF'
|
|
# ⚠️ AVISOS
|
|
|
|
**Lo que hay que saber el primer día.** Se lee antes que el backlog. Cada aviso dice quién lo cierra.
|
|
|
|
| # | Qué | Estado | Quién lo cierra |
|
|
|---|---|---|---|
|
|
EOF
|
|
[ -f "$d/bitacora.md" ] || cat > "$d/bitacora.md" <<'EOF'
|
|
# Bitácora
|
|
|
|
*Relato append-only. Lo nuevo va ABAJO. Se lee por fecha.*
|
|
|
|
---
|
|
EOF
|
|
}
|
|
|
|
# ── el `.gitattributes` de RAÍZ (bloque gestionado) ──────────────────────────
|
|
# ⚰️ **OJO CON EL MOTIVO: el que se escribió primero era FALSO y está medido como tal.** Se dijo
|
|
# —y lo decía ya el ítem de partida— que un `git worktree add` limpio hacía que `metodo check`
|
|
# contara **el doble** (299 avisos contra 123). Es cierto que los cuenta, pero **no por el EOL**:
|
|
# medido el 2026-08-23 con dos worktrees del hub **a la misma profundidad**, uno con 14 ficheros
|
|
# en CRLF y otro con 0, la salida del linter es **IDÉNTICA** —mismos 123 avisos, mismas 6 notas,
|
|
# mismos 19 enlaces rotos—. Los 176 de más eran **enlaces relativos que salen del repo**
|
|
# (`../../../workflows/…`), que sólo resuelven desde el árbol de trabajo: se estaba midiendo la
|
|
# UBICACIÓN del checkout, no su final de línea. Y el mecanismo que se le atribuía tampoco existe:
|
|
# `check_links` captura hasta el `)` y el `\r` va **después**, al final de la línea.
|
|
#
|
|
# ⇒ **`metodo check` es EOL-independiente, y eso ya es un resultado**: retira una hipótesis que
|
|
# estaba a punto de convertirse en doctrina. Es la TERCERA vez que un mecanismo del CRLF se cae
|
|
# al carearlo con su consumidor real (antes: `kubectl` normalizando el `\r` en dos de las cinco
|
|
# mordidas registradas).
|
|
#
|
|
# ⇒ Entonces **por qué se estampa igual**, dicho sin inflarlo: por **higiene y uniformidad**, y
|
|
# porque el principio *«lo que un gate o un instrumento lee se fija en LF»* ya estaba aprobado por
|
|
# Xavier para los SUJETOS de los arneses —donde sí hay cicatriz medida: un `sed` anclado en `$`
|
|
# se apaga—. Aquí **no hay cicatriz**: el coste es cero y el beneficio es que el árbol deja de
|
|
# depender de cómo se hizo cada checkout. ⚠️ Si algún día estorba, se quita sin perder nada.
|
|
#
|
|
# ⛔ **ACOTADO, y NUNCA `* text=auto`.** Esto sí está medido, y en el sentido destructivo: en
|
|
# `Solidaria` hay **15 PDF** bajo `app/templates/`, y un `text` sobre ellos les reescribe en cada
|
|
# checkout los bytes que parezcan finales de línea; `hermes-hub` tiene binarios en LFS y su propio
|
|
# `.gitattributes` documenta por qué no la pone. Aquí se declaran **exactamente los dos globs que
|
|
# el linter recorre** — los `*.md` de RAÍZ y `doc/*.md` — y ni un patrón más.
|
|
#
|
|
# ⚠️ **Declarar NO es refrescar, y renormalizar el ÍNDICE tampoco**: `git add --renormalize` deja
|
|
# el worktree en CRLF (`i/lf w/crlf`), y `git checkout --` a secas **salta** los ficheros que el
|
|
# caché de `stat` da por al día. Hay que **borrarlos y volver a sacarlos**. Por eso el estampado
|
|
# **mide** (`git ls-files --eol`) y lo dice en voz alta.
|
|
_GA_OPEN='# >>> metodo:eol'
|
|
_GA_CLOSE='# <<< metodo:eol'
|
|
|
|
_ga_block() { # el bloque COMPLETO (marcadores incluidos) por stdout
|
|
cat <<'GAB'
|
|
# >>> metodo:eol
|
|
# Gestionado por `metodo init/update` — NO editar a mano (se re-estampa).
|
|
# ACOTADO a los dos globs que el linter recorre: los `*.md` de RAÍZ y `doc/*.md` (glob de shell,
|
|
# sólo hijos directos). Es HIGIENE, no una avería: medido el 2026-08-23 en hermes-hub con dos
|
|
# worktrees a la misma profundidad —uno con 14 ficheros en CRLF y otro con 0—, `metodo check`
|
|
# da EXACTAMENTE la misma salida. Se fija el EOL para que el árbol no dependa de cómo se hizo
|
|
# cada checkout, no porque el linter se rompa.
|
|
# ⛔ Nunca `* text=auto` aquí: hay repos con binarios en LFS y con PDF versionados — ESO sí es
|
|
# destructivo y está medido.
|
|
/*.md text eol=lf
|
|
doc/*.md text eol=lf
|
|
# <<< metodo:eol
|
|
GAB
|
|
}
|
|
|
|
_stamp_gitattributes() { # $1 = repo destino
|
|
local ga="$1/.gitattributes" blk tmp a b
|
|
blk=$(mktemp); _ga_block > "$blk"; tmp=$(mktemp)
|
|
if [ ! -f "$ga" ]; then
|
|
cat "$blk" > "$ga"; rm -f "$blk" "$tmp"; echo " .gitattributes creado con el bloque eol (*.md de raíz · doc/*.md)"; return
|
|
fi
|
|
a=$(grep -nxF "$_GA_OPEN" "$ga" | head -1 | cut -d: -f1)
|
|
b=$(grep -nxF "$_GA_CLOSE" "$ga" | head -1 | cut -d: -f1)
|
|
if [ -n "$a" ] && [ -n "$b" ] && [ "$b" -ge "$a" ]; then
|
|
# ⛔ Se sustituye SÓLO el bloque, y con `head`/`tail`, NO con `awk`/`sed`. Medido el
|
|
# 2026-08-23 en Git Bash: **awk y sed abren en modo texto y se comen el `\r`** ⇒ reescribir el
|
|
# fichero entero con ellos NORMALIZA en silencio los bytes de un fichero que no es nuestro
|
|
# (`hermes-hub`, `kubernetes` y `mcp-edge` tienen su `.gitattributes` en CRLF o mixto).
|
|
# `head`/`tail` cortan por línea sin tocar los bytes que no atraviesan.
|
|
{ head -n $((a-1)) "$ga"; cat "$blk"; tail -n +$((b+1)) "$ga"; } > "$tmp" && mv "$tmp" "$ga"
|
|
rm -f "$blk"; echo " .gitattributes: bloque eol re-estampado (sólo el bloque; el resto, byte a byte)"; return
|
|
fi
|
|
# Existe y no lo lleva: se AÑADE AL FINAL. En gitattributes gana la última regla que casa, así
|
|
# que el bloque no puede quedar tapado por lo que el repo ya declarara para `.md`.
|
|
{ [ -s "$ga" ] && printf '\n'; cat "$blk"; } >> "$ga"
|
|
rm -f "$blk" "$tmp"; echo " .gitattributes: bloque eol añadido al final (lo de antes, intacto)"
|
|
}
|
|
|
|
# ⚠️ Los dos pathspec llevan `:(glob)` a propósito: sin él, el `*` de git cruza `/` y `doc/*.md`
|
|
# se tragaría `doc/img/README.md`, que el linter NO lee. La verificación tiene que mirar el
|
|
# MISMO conjunto que declara el bloque — si mira otro, es un gate sin sujeto.
|
|
_GA_SUJETOS=":(glob)*.md :(glob)doc/*.md"
|
|
_ga_verifica() { # $1 = repo destino — «declarar NO es refrescar»: se MIDE el worktree
|
|
local tgt="$1" crlf
|
|
git -C "$tgt" rev-parse --git-dir >/dev/null 2>&1 || return 0
|
|
crlf=$(git -C "$tgt" ls-files --eol -- $_GA_SUJETOS 2>/dev/null | awk '$2=="w/crlf"' | wc -l)
|
|
if [ "${crlf:-0}" -gt 0 ]; then
|
|
echo " ⚠ .gitattributes declarado pero el worktree NO está refrescado: ${crlf} fichero(s) en CRLF."
|
|
echo " ⇒ git -C $tgt add --renormalize -- $_GA_SUJETOS (y commitea)"
|
|
echo " Sin esto el linter sigue midiendo el CHECKOUT y no el repo."
|
|
else
|
|
echo " eol verificado: *.md de raíz y doc/*.md en LF en el worktree (git ls-files --eol)"
|
|
fi
|
|
}
|
|
|
|
# ── el PUNTERO de enrutado (bloque gestionado en CLAUDE.md) ──────────────────
|
|
# Por qué en CLAUDE.md y sólo ahí: es el ÚNICO fichero que el harness carga solo en toda sesión
|
|
# sobre el repo. Un puntero en cualquier otro sitio reproduce la enfermedad que viene a curar
|
|
# ("protege si alguien lo abre") — medido el 2026-08-19: la copia viajaba a 9 de 18 repos sin que
|
|
# nada la abriera. El bloque va entre marcadores y `update` sustituye SÓLO su interior: los
|
|
# CLAUDE.md ricos son del repo, no nuestros, y no se toca un byte fuera.
|
|
_PTR_OPEN='<!-- metodo:puntero -->'
|
|
_PTR_CLOSE='<!-- /metodo:puntero -->'
|
|
|
|
_ptr_block() { # escribe el bloque COMPLETO (marcadores incluidos) por stdout
|
|
cat <<'PTREOF'
|
|
<!-- metodo:puntero -->
|
|
⛔ **Antes de tocar nada, lee el Método: [`.metodo/metodo.md`](.metodo/metodo.md)** — los raíles
|
|
transversales, completos y con sus cicatrices. **Viaja con este repo y aquí NO se edita**: su fuente
|
|
es el repo `metodo` (la matriz), que lo re-estampa con `metodo update` — este bloque incluido.
|
|
⇒ Del corpus de este repo, **`doc/AVISOS.md` se lee ANTES que el backlog**; **la bitácora NO se lee
|
|
al arrancar** (`§7`): se consulta por su índice. El gate de todo esto es **`metodo check .`** — y si
|
|
sale rojo, no lo apagues: arréglalo o dilo.
|
|
⇒ **Y si vas a trabajar con más de una sesión a la vez**, el procedimiento del canal viaja aquí
|
|
dentro: [`.metodo/workflows/comunicacion-entre-sesiones.md`](.metodo/workflows/comunicacion-entre-sesiones.md)
|
|
— el saludo invertido con sus cinco líneas, quién avisa a quién y qué NO viaja por mensaje.
|
|
⇒ ⛔ **Y si trabajas dentro del árbol multi-repo, apúntate ANTES de escribir** en el semáforo,
|
|
`repos/metodo/workflows/sesiones-activas.md`, y mira si tu alcance solapa con una fila viva.
|
|
Existe por una colisión real: dos sesiones mutando los mismos ficheros a la vez. **Solo lectura
|
|
no necesita apuntarse.** *(Si has clonado este repo suelto, esa ruta no existe y no te aplica:
|
|
el semáforo arbitra un árbol de varios repos, no un repo.)*
|
|
⇒ *Existe porque el 2026-08-19 se midió que el Método viajaba a 9 de 18 repos **sin que nada lo
|
|
abriera**. Una constitución solo gobierna si alguien la lee: es su propio raíl `§8`.*
|
|
<!-- /metodo:puntero -->
|
|
PTREOF
|
|
}
|
|
|
|
_stamp_pointer() { # $1 = repo destino
|
|
local tgt="$1" cm="$1/CLAUDE.md" blk tmp
|
|
blk=$(mktemp); _ptr_block > "$blk"
|
|
tmp=$(mktemp)
|
|
if [ ! -f "$cm" ]; then
|
|
# No hay CLAUDE.md: se crea MÍNIMO (título + bloque y nada más). Lo que la matriz no puede
|
|
# mantener no se siembra: se pudriría (raíl 7).
|
|
{ printf '# %s\n\n' "$(basename "$(cd "$tgt" && pwd)")"; cat "$blk"; } > "$cm"
|
|
rm -f "$blk" "$tmp"; echo " CLAUDE.md creado (mínimo) con el puntero"; return
|
|
fi
|
|
if grep -qxF "$_PTR_OPEN" "$cm" && grep -qxF "$_PTR_CLOSE" "$cm"; then
|
|
# Ya está: se sustituye SÓLO el interior de los marcadores (idempotente).
|
|
awk -v blk="$blk" -v o="$_PTR_OPEN" -v c="$_PTR_CLOSE" '
|
|
$0 == o { inb=1; while ((getline l < blk) > 0) print l; close(blk); next }
|
|
$0 == c { inb=0; next }
|
|
inb { next }
|
|
{ print }' "$cm" > "$tmp" && mv "$tmp" "$cm"
|
|
rm -f "$blk"; echo " CLAUDE.md: puntero re-estampado (sólo el interior de los marcadores)"; return
|
|
fi
|
|
# Existe pero sin bloque: se inserta ANTES del primer `## `; si no hay, tras el H1; si no, arriba.
|
|
awk -v blk="$blk" '
|
|
function put( l){ while ((getline l < blk) > 0) print l; close(blk); print ""; done=1 }
|
|
!done && /^## / { put() }
|
|
{ print }
|
|
NR==1 && $0 ~ /^# / { h1=1 }
|
|
END { if (!done) exit 9 }' "$cm" > "$tmp"
|
|
if [ $? -eq 9 ]; then
|
|
# sin ningún `## `: tras el H1 (o al principio si no hay H1)
|
|
awk -v blk="$blk" '
|
|
function put( l){ print ""; while ((getline l < blk) > 0) print l; done=1 }
|
|
NR==1 { print; if ($0 ~ /^# /) put(); else { print ""; } ; next }
|
|
NR==2 && !done { put(); print; next }
|
|
{ print }' "$cm" > "$tmp"
|
|
fi
|
|
mv "$tmp" "$cm"; rm -f "$blk"
|
|
echo " CLAUDE.md: puntero insertado antes del primer encabezado"
|
|
}
|
|
|
|
# ── metodo init <repo> — vendoriza el Método a un repo por primera vez ────────
|
|
cmd_init() {
|
|
local tgt="${1:-.}"
|
|
_is_matriz || { echo "metodo init: sólo se corre desde la MATRIZ, no desde una copia vendorizada."; exit 2; }
|
|
[ -d "$tgt" ] || { echo "metodo init: no existe el destino: $tgt"; exit 2; }
|
|
local md="$tgt/.metodo"
|
|
[ -e "$md" ] && { echo "metodo init: $tgt ya tiene .metodo/ — usa 'metodo update $tgt'."; exit 2; }
|
|
mkdir -p "$md/bin"
|
|
cp "$SELF_ROOT/metodo.md" "$md/metodo.md"
|
|
cp "$SELF_ROOT/bin/metodo" "$md/bin/metodo"; chmod +x "$md/bin/metodo" 2>/dev/null || true
|
|
# los workflows transversales viajan también (2026-08-29): un repo que se clona solo no
|
|
# tiene la matriz al lado, así que un puntero a su ruta no le resuelve. El semáforo NO.
|
|
mkdir -p "$md/workflows"
|
|
for _wf in "$SELF_ROOT"/workflows/*.md; do
|
|
[ -e "$_wf" ] || continue
|
|
_wf_no_viaja "$(basename "$_wf")" && continue
|
|
cp "$_wf" "$md/workflows/$(basename "$_wf")"
|
|
done
|
|
# Y RETIRA lo que ya no debe estar. ⛔ El criterio es **ausencia en el MÁSTER**, no
|
|
# pertenencia a la lista de exclusión: un workflow RETIRADO o RENOMBRADO en la matriz
|
|
# tampoco debe sobrevivir aquí. Con el criterio antiguo se quedaba vivo Y fuera de
|
|
# CHECKSUMS ⇒ `check` decía «copia íntegra, rc=0» con un fichero fantasma dentro.
|
|
# (Medido 2026-08-30 por la sesión que construyó los núcleos, con control positivo.)
|
|
# que un bucle anterior ya depositó. Sin esto, el fichero mal vendorizado sobrevive.
|
|
for _old in "$md"/workflows/*.md; do
|
|
[ -e "$_old" ] || continue
|
|
_b=$(basename "$_old")
|
|
if _wf_no_viaja "$_b" || [ ! -e "$SELF_ROOT/workflows/$_b" ]; then rm -f "$_old"; fi
|
|
done
|
|
local mver adopt
|
|
mver=$(grep -E '^version:' "$SELF_ROOT/.metodo/VERSION" 2>/dev/null | sed 's/version:[[:space:]]*//')
|
|
adopt=$(git -C "$tgt" rev-parse HEAD 2>/dev/null || echo "") # marca de adopción brownfield
|
|
{ echo "# Copia vendorizada del Método (raíl C11). Gestionada por 'metodo init/update'."
|
|
echo "master: metodo"; echo "version: ${mver:-?}"; echo "estado: vendored"
|
|
[ -n "$adopt" ] && echo "adopted_commit: $adopt"
|
|
# C26: punto de adopción del trailer de sesión — se sella UNA vez y se preserva.
|
|
_sd=$(grep -E "^sesion_desde:" "$md/VERSION" 2>/dev/null | sed "s/sesion_desde:[[:space:]]*//")
|
|
[ -z "$_sd" ] && _sd=$(git -C "$tgt" rev-parse HEAD 2>/dev/null)
|
|
[ -n "$_sd" ] && echo "sesion_desde: $_sd"; } > "$md/VERSION.new" && mv "$md/VERSION.new" "$md/VERSION"
|
|
_write_checksums "$md" || echo " (aviso: sin sha256 → CHECKSUMS no escrito; la deriva no se medirá)"
|
|
printf '* text eol=lf\n' > "$md/.gitattributes" # un clone en Windows no mete CRLF ni falsea el checksum
|
|
[ -f "$md/realimentacion.md" ] || cat > "$md/realimentacion.md" <<EOF
|
|
# Realimentación del Método — repo: $(basename "$(cd "$tgt" && pwd)")
|
|
|
|
*Log append-only local, viaja con el repo. Anota cuando trabajar aquí enseñe algo del Método.
|
|
Etiquetas: [NUEVA-REGLA] · [REFINA Cn] · [Cn-NO-ENCAJA] · [FALSA]. Se cosecha con 'metodo harvest'.*
|
|
|
|
---
|
|
EOF
|
|
_scaffold_docs "$tgt"
|
|
_stamp_gitattributes "$tgt"
|
|
_ga_verifica "$tgt"
|
|
_stamp_pointer "$tgt"
|
|
_is_matriz && _huella_reparto > "$SELF_ROOT/.metodo/ULTIMO-REPARTO"
|
|
echo "metodo init: $tgt inicializado — .metodo/ (máster $mver) + doc/ de CUATRO capas (backlog · decisiones · AVISOS · bitácora)."
|
|
echo " siguiente: siembra doc/backlog.md con el trabajo abierto de hoy y corre 'metodo check .'"
|
|
}
|
|
|
|
# ── metodo update <repo> — re-estampa el máster+linter en un repo ya iniciado ─
|
|
cmd_update() {
|
|
local tgt="${1:-.}"
|
|
_is_matriz || { echo "metodo update: sólo se corre desde la MATRIZ."; exit 2; }
|
|
local md="$tgt/.metodo"
|
|
[ -d "$md" ] || { echo "metodo update: $tgt no tiene .metodo/ — usa 'metodo init $tgt'."; exit 2; }
|
|
cp "$SELF_ROOT/metodo.md" "$md/metodo.md"
|
|
cp "$SELF_ROOT/bin/metodo" "$md/bin/metodo"; chmod +x "$md/bin/metodo" 2>/dev/null || true
|
|
# los workflows transversales viajan también (2026-08-29): un repo que se clona solo no
|
|
# tiene la matriz al lado, así que un puntero a su ruta no le resuelve. El semáforo NO.
|
|
mkdir -p "$md/workflows"
|
|
for _wf in "$SELF_ROOT"/workflows/*.md; do
|
|
[ -e "$_wf" ] || continue
|
|
_wf_no_viaja "$(basename "$_wf")" && continue
|
|
cp "$_wf" "$md/workflows/$(basename "$_wf")"
|
|
done
|
|
# Y RETIRA lo que ya no debe estar. ⛔ El criterio es **ausencia en el MÁSTER**, no
|
|
# pertenencia a la lista de exclusión: un workflow RETIRADO o RENOMBRADO en la matriz
|
|
# tampoco debe sobrevivir aquí. Con el criterio antiguo se quedaba vivo Y fuera de
|
|
# CHECKSUMS ⇒ `check` decía «copia íntegra, rc=0» con un fichero fantasma dentro.
|
|
# (Medido 2026-08-30 por la sesión que construyó los núcleos, con control positivo.)
|
|
# que un bucle anterior ya depositó. Sin esto, el fichero mal vendorizado sobrevive.
|
|
for _old in "$md"/workflows/*.md; do
|
|
[ -e "$_old" ] || continue
|
|
_b=$(basename "$_old")
|
|
if _wf_no_viaja "$_b" || [ ! -e "$SELF_ROOT/workflows/$_b" ]; then rm -f "$_old"; fi
|
|
done
|
|
local mver adopt
|
|
mver=$(grep -E '^version:' "$SELF_ROOT/.metodo/VERSION" 2>/dev/null | sed 's/version:[[:space:]]*//')
|
|
adopt=$(grep -E '^adopted_commit:' "$md/VERSION" 2>/dev/null | sed 's/adopted_commit:[[:space:]]*//') # se preserva
|
|
{ echo "# Copia vendorizada del Método (raíl C11). Gestionada por 'metodo init/update'."
|
|
echo "master: metodo"; echo "version: ${mver:-?}"; echo "estado: vendored"
|
|
[ -n "$adopt" ] && echo "adopted_commit: $adopt"
|
|
# C26: punto de adopción del trailer de sesión — se sella UNA vez y se preserva.
|
|
_sd=$(grep -E "^sesion_desde:" "$md/VERSION" 2>/dev/null | sed "s/sesion_desde:[[:space:]]*//")
|
|
[ -z "$_sd" ] && _sd=$(git -C "$tgt" rev-parse HEAD 2>/dev/null)
|
|
[ -n "$_sd" ] && echo "sesion_desde: $_sd"; } > "$md/VERSION.new" && mv "$md/VERSION.new" "$md/VERSION"
|
|
_write_checksums "$md" || echo " (aviso: sin sha256 → CHECKSUMS no escrito)"
|
|
printf '* text eol=lf\n' > "$md/.gitattributes"
|
|
_stamp_gitattributes "$tgt"
|
|
_ga_verifica "$tgt"
|
|
_stamp_pointer "$tgt"
|
|
_is_matriz && _huella_reparto > "$SELF_ROOT/.metodo/ULTIMO-REPARTO"
|
|
# Un destino SIN git recibe la copia igual, pero esa copia no tiene historial ni
|
|
# marcha atrás. ⚠️ NO se afirma que «no tenga dueño»: eso es una INFERENCIA y el update no puede
|
|
# medirla — una carpeta a medias suele ser una PAUSA de su dueño, no un defecto. Se ve, y ya.
|
|
if ! git -C "$tgt" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
echo " nota: $tgt no es un repo git — la copia queda sin historial y sin marcha atrás."
|
|
echo " (Se dice, no se juzga: una carpeta sin git puede ser una PAUSA de su dueño. Lo que"
|
|
echo " se mide aquí es el git, no el dueño — el dueño no se puede medir desde fuera.)"
|
|
fi
|
|
echo "metodo update: $tgt actualizado al máster $mver (metodo.md + linter, byte a byte); realimentacion.md y adopted_commit preservados."
|
|
}
|
|
|
|
# El veredicto final (`A7`, 2026-08-22). Regla: **la palabra y el `rc` se derivan del MISMO
|
|
# número**, así que no hay camino por el que puedan discrepar. Y el `rc` se IMPRIME al lado de
|
|
# la palabra, porque la única forma medida de ver «NO pasa» con rc=0 es leer esta salida por
|
|
# una TUBERÍA (`… | tail`), que se come el exit code del script y deja el texto como único
|
|
# testigo. Un testigo que no sobrevive a cómo se le lee no es un testigo.
|
|
# Vocabulario, que es la mitad del arreglo: `AVISO` GATEA (mueve el rc) · `nota` es
|
|
# informativa y NO gatea · `info` es censo · `SKIP` es fallo benigno · `ok` es verde.
|
|
cmd_check() {
|
|
printf 'metodo check — %s\n' "$ROOT"
|
|
check_drift
|
|
check_apendices_no_viajan
|
|
check_sesion_trailer
|
|
check_incremento
|
|
check_reparto_al_dia
|
|
check_semaforo_forma
|
|
check_semaforo_en_vuelo
|
|
check_ids
|
|
check_headings
|
|
check_links
|
|
check_commits
|
|
check_decisiones
|
|
check_decisiones_en_backlog
|
|
check_decisiones_en_prosa
|
|
check_pointer
|
|
hdr "resultado"
|
|
local rc=0
|
|
[ "$FAILS" -gt 0 ] && rc=1
|
|
if [ "$rc" -ne 0 ]; then
|
|
printf ' \033[31mNO pasa el Método — %d aviso(s) que GATEAN\033[0m (y %d nota(s) que no) · rc=%d\n' \
|
|
"$FAILS" "$WARNS" "$rc"
|
|
elif [ "$WARNS" -gt 0 ]; then
|
|
printf ' \033[32mPASA el Método\033[0m con %d nota(s) informativa(s) — 0 avisos que gateen; las notas se cuentan y NO mueven el rc · rc=%d\n' \
|
|
"$WARNS" "$rc"
|
|
else
|
|
printf ' \033[32mPASA el Método\033[0m (0 avisos, 0 notas) · rc=%d\n' "$rc"
|
|
fi
|
|
exit "$rc"
|
|
}
|
|
case "${1:-}" in
|
|
check) ROOT="${2:-.}"; cmd_check ;;
|
|
init) cmd_init "${2:-.}" ;;
|
|
update) cmd_update "${2:-.}" ;;
|
|
harvest) printf "metodo harvest: aún no implementado (recogerá los .metodo/realimentacion.md al buzón de la matriz).\n"; exit 2 ;;
|
|
""|help|-h|--help) printf 'uso: metodo <check|init|update> [ruta]\n check linter · init vendoriza a un repo nuevo · update re-estampa (desde la matriz)\n'; exit 2 ;;
|
|
*) printf "metodo: subcomando desconocido: %s\n" "$1"; exit 2 ;;
|
|
esac
|