#!/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 del check) --- 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)); } # ── 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) ──────────────── check_ids() { hdr "colisión de ids en los ficheros de estado (C17)" local statefiles=( "$ROOT/doc/AVISOS.md" "$ROOT/doc/backlog.md" ) local any=0 f dups id for f in "${statefiles[@]}"; do [ -f "$f" ] || continue any=1 # id DEFINIDO = al principio de una fila de tabla (|) o item de lista (-*) en negrita. # Así una MENCIÓN inline ("ver D5") no cuenta como definición (evita falsos positivos). dups=$(grep -oE '^[[:space:]]*[|*_-]+[[:space:]]*\**~*\**(D|A|G|H|C|R|F|E|P)[0-9]+' "$f" \ | grep -oE '(D|A|G|H|C|R|F|E|P)[0-9]+' | sort | uniq -d) if [ -n "$dups" ]; then while IFS= read -r id; do [ -n "$id" ] && bad "id duplicado en $(basename "$f"): $id — renombra o fusiona (C17)" done <<< "$dups" else ok "$(basename "$f"): sin ids duplicados" fi done [ "$any" -eq 0 ] && skip "sin doc/AVISOS.md ni doc/backlog.md (repo sin corpus de estado)" } # ── 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 '' "$cm" || ! grep -qxF '' "$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 '/^$/{f=1;next} /^$/{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 `### `, 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 '### ' — 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 ( "$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 `### `. 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='' _PTR_CLOSE='' _ptr_block() { # escribe el bloque COMPLETO (marcadores incluidos) por stdout cat <<'PTREOF' ⛔ **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`.* 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 — 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" < — 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." } 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 nota="" [ "$WARNS" -gt 0 ] && nota=$(printf ' · %d nota(s) — no mueven el código de salida, pero se cuentan' "$WARNS") if [ "$FAILS" -eq 0 ]; then printf ' \033[32mPASA el Método\033[0m (0 avisos%s)\n' "$nota"; exit 0 else printf ' \033[31m%d aviso(s) — el Método NO pasa\033[0m%s\n' "$FAILS" "$nota"; exit 1 fi } 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 [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