git no tiene concepto de sesion, pero el mensaje si lo controlamos. Un trailer «Sesion: <rol>/<frente>» hace visibles en el log los filos del working tree compartido. Se juzga desde un punto de adopcion sellado por repo, asi que la historia anterior queda fuera por construccion. 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-70) (C26)"; 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
|