smtp-relay/.metodo/bin/metodo
sirxavor d2df969153 docs(metodo): re-estampar el linter — A6 (check_ids ve, y parte el veredicto) + A7 (palabra == rc)
check_ids admite la comilla invertida (veía 0 ids donde hay 130/118), parte el
veredicto en rojo-de-esquema-nuevo vs nota-con-conteo-legacy, y DICE cuántos ids
vio por fichero (anti-ceguera, con detector agnóstico de prefijo). El veredicto
final deriva palabra y rc del mismo número y IMPRIME el rc al lado de la palabra,
para que una tubería no se lo coma. Copia vendorizada: aquí NO se edita.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 16:08:52 +02:00

561 lines
31 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
}
_vendored_files() { printf '%s\n' "metodo.md" "bin/metodo"; } # lo que se estampa y se vigila
_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.
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.
_IDPFX='(D|A|G|H|C|R|F|E|P)'
# id DEFINIDO = al principio de una fila de tabla (|) o item de lista (-*), con el adorno que
# sea. Así una MENCIÓN inline ("ver D5") no cuenta como definición (evita falsos positivos).
_RE_ID_DEF="^[[:space:]]*[|*_-]+[[:space:]]*[*~\`]*${_IDPFX}[0-9]+"
# LAXO: la MISMA posición de definición, pero con CUALQUIER adorno (hasta 12 caracteres no
# alfanuméricos: casillas `- [ ]`, emoji, comillas…) y —esto es lo que importa— con CUALQUIER
# PREFIJO de letra, no los de `_IDPFX`. Un detector de ceguera construido sobre el vocabulario
# que audita sólo sabe cazar la ceguera de ADORNO, jamás la de PREFIJO: sería el gate-sin-sujeto
# un piso más arriba. Medido el 2026-08-22: con `_IDPFX` no veía los nueve `B` de hermes-sitad.
# Es a propósito más ancho que el estricto y mucho más estrecho que «el id en cualquier sitio de
# la línea» — una mención en mitad de una frase no tiene forma de definición. Sólo se usa cuando
# el estricto ha visto 0: es el detector de «el sujeto está y no lo veo», no un criterio de id.
# La casilla de markdown (`- [ ]` / `- [x]` / `- [~]`) se salta APARTE: lleva un alfanumérico
# dentro y un item CERRADO define tanto como uno abierto.
_RE_ID_LOOSE="^[[:space:]]*[|*_-][[:space:]]*(\[.\])?[^[:alnum:]]{0,12}[A-Z]{1,2}[0-9]+"
_ids_def() { grep -oE "$_RE_ID_DEF" "$1" | grep -oE "${_IDPFX}[0-9]+"; }
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 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=$(grep -cE "$_RE_ID_LOOSE" "$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
info "$base: ${vistos} id(s) DEFINIDOS vistos"
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
for f in "$ROOT"/*.md "$ROOT"/doc/*.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
for f in "$ROOT"/*.md "$ROOT"/doc/*.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; }
while IFS= read -r s; do
printf '%s' "$s" | grep -qE '^(feat|fix|docs|ci|refactor|test|chore|build|perf)(\(.+\))?!?: .' \
|| { bad "commit no-Conventional: \"$s\" (C25)"; bad_c=$((bad_c+1)); }
done < <(git -C "$ROOT" log --format=%s $range 2>/dev/null)
[ "$bad_c" -eq 0 ] && ok "los ${n} commit(s) desde la adopción siguen Conventional"
}
# ── 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; }
local out legacy=0 nuevos=0 id len num
out=$(awk '
function flush(){ if (id!="" && len>3) print id "\t" len; id=""; len=0 }
/^[[:space:]]*[-*|_]+[[:space:]]*[*~`]*D[0-9]+/ {
flush(); match($0, /D[0-9]+/); id=substr($0, RSTART, RLENGTH); 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
}
# ── 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 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**. El gate de todo esto es
**`metodo check .`** — y si sale rojo, no lo apagues: arréglalo o dilo.
⇒ *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
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"; } > "$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_pointer "$tgt"
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
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"; } > "$md/VERSION"
_write_checksums "$md" || echo " (aviso: sin sha256 → CHECKSUMS no escrito)"
printf '* text eol=lf\n' > "$md/.gitattributes"
_stamp_pointer "$tgt"
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_ids
check_headings
check_links
check_commits
check_decisiones
check_decisiones_en_backlog
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