@bi-analytics/arness-back 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -0
- package/README.md +156 -2
- package/bin/arness.mjs +710 -0
- package/harness/AGENTS.md +234 -0
- package/harness/agents/implementador.md +42 -0
- package/harness/agents/lider.md +71 -0
- package/harness/agents/revisor.md +62 -0
- package/harness/decisiones/README.md +17 -0
- package/harness/decisiones/_plantilla.md +24 -0
- package/harness/init.sh +696 -0
- package/harness/playbooks/add-database.md +16 -0
- package/harness/playbooks/new-endpoint.md +88 -0
- package/harness/playbooks/new-module.md +21 -0
- package/harness/playbooks/new-monitor-widget.md +31 -0
- package/harness/playbooks/review.md +40 -0
- package/harness/reference/configuracion.md +104 -0
- package/harness/reference/contrato-http.md +138 -0
- package/harness/reference/novedades.json +107 -0
- package/harness/reference/progress-plantilla.md +50 -0
- package/harness/skills/commit-hygiene/SKILL.md +137 -0
- package/harness/skills/nest-base/SKILL.md +167 -0
- package/harness/skills/nest-base/templates/config/configuration.spec.ts +84 -0
- package/harness/skills/nest-base/templates/config/configuration.ts +143 -0
- package/harness/skills/nest-base/templates/cors/cors.spec.ts +53 -0
- package/harness/skills/nest-base/templates/cors/cors.ts +75 -0
- package/harness/skills/nest-base/templates/docs/EVOLUTIVO.md +22 -0
- package/harness/skills/nest-base/templates/docs/PENDIENTES.md +20 -0
- package/harness/skills/nest-base/templates/docs/TECNICO.md +43 -0
- package/harness/skills/nest-base/templates/e2e/cors.e2e-spec.ts +60 -0
- package/harness/skills/nest-base/templates/e2e/monitor.e2e-spec.ts +134 -0
- package/harness/skills/nest-base/templates/editorconfig +12 -0
- package/harness/skills/nest-base/templates/eslint.boundaries.mjs +65 -0
- package/harness/skills/nest-base/templates/eslint.config.mjs +82 -0
- package/harness/skills/nest-base/templates/logging/logger-options.spec.ts +57 -0
- package/harness/skills/nest-base/templates/logging/logger-options.ts +85 -0
- package/harness/skills/nest-base/templates/monitor/monitor-login-redirect.filter.ts +16 -0
- package/harness/skills/nest-base/templates/monitor/monitor-login.dto.ts +8 -0
- package/harness/skills/nest-base/templates/monitor/monitor-page.spec.ts +41 -0
- package/harness/skills/nest-base/templates/monitor/monitor-page.ts +190 -0
- package/harness/skills/nest-base/templates/monitor/monitor-session.guard.spec.ts +46 -0
- package/harness/skills/nest-base/templates/monitor/monitor-session.guard.ts +36 -0
- package/harness/skills/nest-base/templates/monitor/monitor-session.service.spec.ts +57 -0
- package/harness/skills/nest-base/templates/monitor/monitor-session.service.ts +77 -0
- package/harness/skills/nest-base/templates/monitor/monitor-widget.interface.ts +18 -0
- package/harness/skills/nest-base/templates/monitor/monitor.constants.ts +25 -0
- package/harness/skills/nest-base/templates/monitor/monitor.controller.ts +63 -0
- package/harness/skills/nest-base/templates/monitor/monitor.decorators.ts +11 -0
- package/harness/skills/nest-base/templates/monitor/monitor.module.ts +30 -0
- package/harness/skills/nest-base/templates/monitor/monitor.service.spec.ts +55 -0
- package/harness/skills/nest-base/templates/monitor/monitor.service.ts +49 -0
- package/harness/skills/nest-base/templates/monitor/widgets/endpoints-total.widget.spec.ts +51 -0
- package/harness/skills/nest-base/templates/monitor/widgets/endpoints-total.widget.ts +53 -0
- package/harness/skills/nest-base/templates/prettierignore +6 -0
- package/harness/skills/nest-base/templates/prettierrc.json +8 -0
- package/package.json +44 -3
package/harness/init.sh
ADDED
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# init.sh — Revisa el estado del proyecto antes y después de cualquier cambio (AGENTS.md §0).
|
|
3
|
+
#
|
|
4
|
+
# Uso (desde cualquier carpeta): ./harness/init.sh [opciones]
|
|
5
|
+
# --strict Los hallazgos fallan (exit 1) en lugar de quedar como advertencias
|
|
6
|
+
# --json Imprime el resultado como JSON en stdout (el detalle legible va a stderr)
|
|
7
|
+
# --quick Solo harness, estructura y toolchain (sin lint, build, tests ni health)
|
|
8
|
+
# --e2e Además ejecuta pnpm test:e2e
|
|
9
|
+
# --no-health No consulta /health
|
|
10
|
+
# --install Si faltan node_modules, ejecuta pnpm install --frozen-lockfile
|
|
11
|
+
# -h, --help Muestra esta ayuda
|
|
12
|
+
#
|
|
13
|
+
# Por defecto los hallazgos son advertencias y el script termina en 0: el proyecto sigue
|
|
14
|
+
# funcionando. Única excepción: .env o .env.local versionado en git (fuga de secretos).
|
|
15
|
+
#
|
|
16
|
+
# Configuración del repo (opcional, sync nunca la toca): harness/arness.config.json
|
|
17
|
+
# { "strict": false,
|
|
18
|
+
# "ignore": { "<id>": "<motivo>" }, silencia un chequeo
|
|
19
|
+
# "required": ["<id>"], ese chequeo falla aunque no haya --strict
|
|
20
|
+
# "declined": { "<novedad>": "<motivo>" } } novedad que el repo no adopta (lo lee arness doctor)
|
|
21
|
+
#
|
|
22
|
+
# Variables opcionales:
|
|
23
|
+
# HEALTH_URL URL del health (default http://localhost:$PORT/health)
|
|
24
|
+
# HEALTH_TIMEOUT_S Segundos de espera al arrancar la app (default 30)
|
|
25
|
+
#
|
|
26
|
+
# Salida: 0 = operativo (puede haber advertencias) · 1 = fallo crítico o --strict con hallazgos · 2 = uso incorrecto
|
|
27
|
+
|
|
28
|
+
set -o pipefail
|
|
29
|
+
|
|
30
|
+
# El script vive en harness/, pero verifica desde la raíz del proyecto.
|
|
31
|
+
HARNESS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
32
|
+
ROOT="$(dirname "$HARNESS_DIR")"
|
|
33
|
+
cd "$ROOT" || exit 2
|
|
34
|
+
H=harness
|
|
35
|
+
CONFIG="$H/arness.config.json"
|
|
36
|
+
|
|
37
|
+
STRICT=0; JSON=0; QUICK=0; E2E=0; HEALTH=1; INSTALL=0
|
|
38
|
+
for arg in "$@"; do
|
|
39
|
+
case "$arg" in
|
|
40
|
+
--strict) STRICT=1 ;;
|
|
41
|
+
--json) JSON=1 ;;
|
|
42
|
+
--quick) QUICK=1 ;;
|
|
43
|
+
--e2e) E2E=1 ;;
|
|
44
|
+
--no-health) HEALTH=0 ;;
|
|
45
|
+
--install) INSTALL=1 ;;
|
|
46
|
+
-h|--help) awk 'NR>1 && /^#/{sub(/^# ?/,""); print; next} NR>1{exit}' "$0"; exit 0 ;;
|
|
47
|
+
*) echo "Opción desconocida: $arg (usa --help)" >&2; exit 2 ;;
|
|
48
|
+
esac
|
|
49
|
+
done
|
|
50
|
+
|
|
51
|
+
# Con --json, el detalle legible va a stderr y stdout queda solo para el JSON (fd 3).
|
|
52
|
+
if [ "$JSON" -eq 1 ]; then exec 3>&1 1>&2; fi
|
|
53
|
+
|
|
54
|
+
if [ -t 1 ] && [ "$JSON" -eq 0 ]; then
|
|
55
|
+
RED=$'\033[31m'; GREEN=$'\033[32m'; YELLOW=$'\033[33m'; BLUE=$'\033[34m'; BOLD=$'\033[1m'; RESET=$'\033[0m'
|
|
56
|
+
else
|
|
57
|
+
RED=''; GREEN=''; YELLOW=''; BLUE=''; BOLD=''; RESET=''
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
CRITS=0; FAILS=0; WARNS=0; IGNORED=0
|
|
61
|
+
SECTION=''
|
|
62
|
+
LOG_DIR="$(mktemp -d "${TMPDIR:-/tmp}/init-sh.XXXXXX")"
|
|
63
|
+
ISSUES="$LOG_DIR/issues.tsv"; : >"$ISSUES"
|
|
64
|
+
APP_PID=''
|
|
65
|
+
|
|
66
|
+
cleanup() {
|
|
67
|
+
if [ -n "$APP_PID" ] && kill -0 "$APP_PID" 2>/dev/null; then
|
|
68
|
+
kill "$APP_PID" 2>/dev/null
|
|
69
|
+
wait "$APP_PID" 2>/dev/null
|
|
70
|
+
fi
|
|
71
|
+
rm -rf "$LOG_DIR"
|
|
72
|
+
}
|
|
73
|
+
trap cleanup EXIT INT TERM
|
|
74
|
+
|
|
75
|
+
# ── Configuración del repo ──────────────────────────────────────────────────
|
|
76
|
+
CFG_IGNORE=' '; CFG_REQUIRED=' '; CFG_ERROR=''
|
|
77
|
+
if [ -f "$CONFIG" ]; then
|
|
78
|
+
cfg="$(node -e '
|
|
79
|
+
let c; try { c = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")); }
|
|
80
|
+
catch (e) { console.log("error\t" + e.message); process.exit(0); }
|
|
81
|
+
if (c.strict === true) console.log("strict");
|
|
82
|
+
for (const [id, why] of Object.entries(c.ignore || {})) console.log(["ignore", id, String(why || "").trim()].join("\t"));
|
|
83
|
+
for (const id of c.required || []) console.log("required\t" + id);
|
|
84
|
+
' "$CONFIG" 2>&1)"
|
|
85
|
+
while IFS="$(printf '\t')" read -r kind id why; do
|
|
86
|
+
case "$kind" in
|
|
87
|
+
strict) STRICT=1 ;;
|
|
88
|
+
ignore) CFG_IGNORE="$CFG_IGNORE$id "; [ -z "$why" ] && CFG_ERROR="${CFG_ERROR}ignore.$id sin motivo; " ;;
|
|
89
|
+
required) CFG_REQUIRED="$CFG_REQUIRED$id " ;;
|
|
90
|
+
error) CFG_ERROR="${CFG_ERROR}JSON inválido: $id; " ;;
|
|
91
|
+
'') ;;
|
|
92
|
+
*) CFG_ERROR="${CFG_ERROR}$kind; " ;;
|
|
93
|
+
esac
|
|
94
|
+
done <<EOF
|
|
95
|
+
$cfg
|
|
96
|
+
EOF
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
# ── Salida ──────────────────────────────────────────────────────────────────
|
|
100
|
+
record() { printf '%s\t%s\t%s\t%s\n' "$1" "$2" "$SECTION" "$3" >>"$ISSUES"; }
|
|
101
|
+
section() { SECTION="$1"; printf '\n%s%s▸ %s%s\n' "$BOLD" "$BLUE" "$1" "$RESET"; }
|
|
102
|
+
ok() { printf ' %s✔%s %s\n' "$GREEN" "$RESET" "$1"; }
|
|
103
|
+
info() { printf ' %sℹ%s %s\n' "$BLUE" "$RESET" "$1"; }
|
|
104
|
+
# Aviso informativo: nunca falla, ni siquiera con --strict.
|
|
105
|
+
warn() { printf ' %s⚠%s %s\n' "$YELLOW" "$RESET" "$1"; WARNS=$((WARNS + 1)); record warning "${2:--}" "$1"; }
|
|
106
|
+
|
|
107
|
+
# Hallazgo con id: advertencia por defecto; fallo con --strict o si el repo lo marca como required.
|
|
108
|
+
issue() { # id mensaje
|
|
109
|
+
local id="$1" msg="$2"
|
|
110
|
+
case "$CFG_REQUIRED" in *" $id "*) ;; *)
|
|
111
|
+
case "$CFG_IGNORE" in *" $id "*)
|
|
112
|
+
printf ' %s·%s %s %s(silenciado: %s)%s\n' "$BLUE" "$RESET" "$msg" "$BLUE" "$id" "$RESET"
|
|
113
|
+
IGNORED=$((IGNORED + 1)); record ignored "$id" "$msg"; return ;;
|
|
114
|
+
esac ;;
|
|
115
|
+
esac
|
|
116
|
+
if [ "$STRICT" -eq 1 ] || case "$CFG_REQUIRED" in *" $id "*) true ;; *) false ;; esac; then
|
|
117
|
+
printf ' %s✖%s %s %s[%s]%s\n' "$RED" "$RESET" "$msg" "$BLUE" "$id" "$RESET"
|
|
118
|
+
FAILS=$((FAILS + 1)); record failure "$id" "$msg"
|
|
119
|
+
else
|
|
120
|
+
printf ' %s⚠%s %s %s[%s]%s\n' "$YELLOW" "$RESET" "$msg" "$BLUE" "$id" "$RESET"
|
|
121
|
+
WARNS=$((WARNS + 1)); record warning "$id" "$msg"
|
|
122
|
+
fi
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# Fallo crítico: siempre exit 1 y no se puede silenciar.
|
|
126
|
+
crit() { # id mensaje
|
|
127
|
+
printf ' %s✖ CRÍTICO:%s %s %s[%s]%s\n' "$RED$BOLD" "$RESET" "$2" "$BLUE" "$1" "$RESET"
|
|
128
|
+
CRITS=$((CRITS + 1)); record critical "$1" "$2"
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
need_file() { if [ -f "$2" ]; then ok "$2"; else issue "$1" "Falta el archivo $2"; fi; }
|
|
132
|
+
need_dir() { if [ -d "$2" ]; then ok "$2/"; else issue "$1" "Falta el directorio $2/"; fi; }
|
|
133
|
+
|
|
134
|
+
# Ejecuta un comando, guarda su salida y muestra el final solo si falla.
|
|
135
|
+
run_step() { # id etiqueta comando...
|
|
136
|
+
local id="$1" label="$2"; shift 2
|
|
137
|
+
local log="$LOG_DIR/$(echo "$label" | tr -c 'a-zA-Z0-9' '_').log"
|
|
138
|
+
if "$@" >"$log" 2>&1; then
|
|
139
|
+
ok "$label"
|
|
140
|
+
else
|
|
141
|
+
issue "$id" "$label ($*)"
|
|
142
|
+
tail -n 25 "$log" | sed 's/^/ │ /'
|
|
143
|
+
fi
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if [ -f "$CONFIG" ]; then
|
|
147
|
+
section "Configuración"
|
|
148
|
+
info "$CONFIG: modo $([ "$STRICT" -eq 1 ] && echo estricto || echo advertencias)"
|
|
149
|
+
[ -n "$CFG_ERROR" ] && warn "$CONFIG: $CFG_ERROR" config.invalid
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
153
|
+
section "Harness"
|
|
154
|
+
|
|
155
|
+
for f in $H/AGENTS.md $H/init.sh $H/decisiones/README.md $H/decisiones/_plantilla.md \
|
|
156
|
+
$H/skills/nest-base/SKILL.md $H/reference/configuracion.md $H/reference/contrato-http.md \
|
|
157
|
+
$H/reference/progress-plantilla.md CLAUDE.md GEMINI.md .cursor/rules/agents.mdc; do
|
|
158
|
+
need_file harness.files "$f"
|
|
159
|
+
done
|
|
160
|
+
|
|
161
|
+
if [ -f $H/AGENTS.md ]; then
|
|
162
|
+
missing_sections=''
|
|
163
|
+
for n in 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17; do
|
|
164
|
+
grep -q "^## $n\. " $H/AGENTS.md || missing_sections="$missing_sections §$n"
|
|
165
|
+
done
|
|
166
|
+
if [ -z "$missing_sections" ]; then ok "$H/AGENTS.md tiene las secciones §0–§17"
|
|
167
|
+
else issue harness.agents-md "$H/AGENTS.md incompleto; faltan:$missing_sections"; fi
|
|
168
|
+
|
|
169
|
+
# Todo archivo del harness referenciado en AGENTS.md debe existir.
|
|
170
|
+
for ref in $(grep -oE "$H/(playbooks|reference|skills|agents|decisiones)/[A-Za-z0-9_./-]+\.md" $H/AGENTS.md | grep -v NNNN | sort -u); do
|
|
171
|
+
[ -f "$ref" ] || issue harness.refs "AGENTS.md referencia $ref, pero no existe"
|
|
172
|
+
done
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
# Puentes de la raíz: solo enlaces e imports hacia harness/.
|
|
176
|
+
# Se acepta un enlace simbólico o, donde no hay symlinks (arness --copy), una copia idéntica.
|
|
177
|
+
check_link() { # enlace destino-relativo
|
|
178
|
+
local target_abs; target_abs="$(dirname "$1")/$2"
|
|
179
|
+
if [ -L "$1" ] && [ "$(readlink "$1")" = "$2" ] && [ -e "$1" ]; then ok "$1 → $2"
|
|
180
|
+
elif [ -e "$1" ] && [ ! -L "$1" ] && diff -rq "$1" "$target_abs" >/dev/null 2>&1; then ok "$1 = copia de $2"
|
|
181
|
+
else issue harness.bridges "$1 debe ser un enlace simbólico (o copia idéntica) a $2"; fi
|
|
182
|
+
}
|
|
183
|
+
check_link AGENTS.md "$H/AGENTS.md"
|
|
184
|
+
check_link .claude/agents "../$H/agents"
|
|
185
|
+
check_link .claude/skills "../$H/skills"
|
|
186
|
+
check_link .agents/skills "../$H/skills"
|
|
187
|
+
|
|
188
|
+
grep -q "^@$H/AGENTS.md" CLAUDE.md 2>/dev/null && ok "CLAUDE.md importa $H/AGENTS.md" || issue harness.bridges "CLAUDE.md no importa @$H/AGENTS.md"
|
|
189
|
+
grep -q "^@\./$H/AGENTS.md" GEMINI.md 2>/dev/null && ok "GEMINI.md importa $H/AGENTS.md" || issue harness.bridges "GEMINI.md no importa @./$H/AGENTS.md"
|
|
190
|
+
grep -q "$H/AGENTS.md" .cursor/rules/agents.mdc 2>/dev/null && ok "Regla de Cursor apunta a $H/AGENTS.md" || issue harness.bridges "La regla de Cursor no apunta a $H/AGENTS.md"
|
|
191
|
+
|
|
192
|
+
agents_before=$((FAILS + WARNS))
|
|
193
|
+
for ag in lider implementador revisor; do
|
|
194
|
+
f="$H/agents/$ag.md"
|
|
195
|
+
if [ ! -f "$f" ]; then issue harness.agents "Falta el agente $f"; continue; fi
|
|
196
|
+
for key in name description tools model; do
|
|
197
|
+
awk 'NR==1 && $0!="---"{exit 1} NR>1 && $0=="---"{exit} NR>1{print}' "$f" | grep -q "^$key:" \
|
|
198
|
+
|| issue harness.agents "$f: falta '$key' en el frontmatter"
|
|
199
|
+
done
|
|
200
|
+
done
|
|
201
|
+
[ "$((FAILS + WARNS))" -eq "$agents_before" ] && ok "Agentes lider, implementador y revisor definidos"
|
|
202
|
+
|
|
203
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
204
|
+
section "Decisiones"
|
|
205
|
+
|
|
206
|
+
decision_count=0
|
|
207
|
+
for d in $H/decisiones/[0-9][0-9][0-9][0-9]-*.md; do
|
|
208
|
+
[ -f "$d" ] || continue
|
|
209
|
+
decision_count=$((decision_count + 1))
|
|
210
|
+
name="$(basename "$d")"
|
|
211
|
+
estado="$(grep -m1 '^- \*\*Estado:\*\*' "$d" | sed 's/^- \*\*Estado:\*\* *//')"
|
|
212
|
+
if ! echo "$estado" | grep -Eq '^(Propuesta|Aceptada|Reemplazada por [0-9]{4}|Obsoleta)$'; then
|
|
213
|
+
issue decisions.state "$name: estado inválido o ausente ('$estado')"
|
|
214
|
+
elif [ "$estado" = "Propuesta" ]; then
|
|
215
|
+
warn "$name está en Propuesta: pendiente de aceptación humana" decisions.proposed
|
|
216
|
+
fi
|
|
217
|
+
grep -q "($name)" $H/decisiones/README.md 2>/dev/null || issue decisions.index "$name no está en el índice de $H/decisiones/README.md"
|
|
218
|
+
done
|
|
219
|
+
dupes="$(ls $H/decisiones 2>/dev/null | grep -E '^[0-9]{4}-' | cut -c1-4 | sort | uniq -d | tr '\n' ' ')"
|
|
220
|
+
[ -n "$dupes" ] && issue decisions.dupes "Números de decisión duplicados: $dupes"
|
|
221
|
+
ok "$decision_count decisión(es) revisada(s)"
|
|
222
|
+
|
|
223
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
224
|
+
section "Progress"
|
|
225
|
+
|
|
226
|
+
PROGRESS="$H/progress.md"
|
|
227
|
+
if [ -f "$PROGRESS" ]; then
|
|
228
|
+
p_estado="$(grep -m1 '^- \*\*Estado:\*\*' "$PROGRESS" | sed 's/^- \*\*Estado:\*\* *//')"
|
|
229
|
+
case "$p_estado" in
|
|
230
|
+
TERMINADO) ok "$PROGRESS: TERMINADO (el líder lo reinicia en la próxima tarea)" ;;
|
|
231
|
+
"EN CURSO") warn "$PROGRESS: hay trabajo EN CURSO; confirma con el humano si se retoma" progress.open ;;
|
|
232
|
+
BLOQUEADO) warn "$PROGRESS: BLOQUEADO; revisa 'Bloqueos y preguntas abiertas' antes de continuar" progress.open ;;
|
|
233
|
+
*) issue progress.state "$PROGRESS: estado inválido o ausente ('$p_estado'); usa EN CURSO | BLOQUEADO | TERMINADO" ;;
|
|
234
|
+
esac
|
|
235
|
+
p_lines="$(wc -l < "$PROGRESS" | tr -d ' ')"
|
|
236
|
+
[ "$p_lines" -gt 200 ] && warn "$PROGRESS tiene $p_lines líneas; el líder debe resumir las tareas cerradas" progress.size
|
|
237
|
+
else
|
|
238
|
+
info "Sin $PROGRESS: el líder lo crea desde la plantilla al iniciar una tarea"
|
|
239
|
+
fi
|
|
240
|
+
|
|
241
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
242
|
+
section "Git y secretos"
|
|
243
|
+
|
|
244
|
+
if git rev-parse --show-toplevel >/dev/null 2>&1; then
|
|
245
|
+
top="$(cd "$(git rev-parse --show-toplevel)" && pwd -P)"
|
|
246
|
+
if [ "$top" != "$(pwd -P)" ]; then
|
|
247
|
+
warn "El repo git raíz es $top, no este proyecto (ejecuta git init aquí)" git.root
|
|
248
|
+
else
|
|
249
|
+
ok "Repositorio git propio"
|
|
250
|
+
env_tracked=0
|
|
251
|
+
for secret in .env .env.local; do
|
|
252
|
+
if git ls-files --error-unmatch "$secret" >/dev/null 2>&1; then
|
|
253
|
+
crit git.env-tracked "$secret está versionado en git: sácalo del índice (git rm --cached $secret) y rota los secretos"
|
|
254
|
+
env_tracked=1
|
|
255
|
+
fi
|
|
256
|
+
done
|
|
257
|
+
[ "$env_tracked" -eq 0 ] && ok ".env y .env.local fuera de git"
|
|
258
|
+
git ls-files --error-unmatch "$H/progress.md" >/dev/null 2>&1 \
|
|
259
|
+
&& warn "$H/progress.md está versionado; es estado de trabajo y debería estar en .gitignore" git.progress
|
|
260
|
+
fi
|
|
261
|
+
else
|
|
262
|
+
warn "No es un repositorio git" git.none
|
|
263
|
+
fi
|
|
264
|
+
|
|
265
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
266
|
+
PKG_NAME=''
|
|
267
|
+
[ -f package.json ] && PKG_NAME="$(node -p "require('./package.json').name || ''" 2>/dev/null)"
|
|
268
|
+
HARNESS_PKG='@bi-analytics/arness-back'
|
|
269
|
+
|
|
270
|
+
if [ "$PKG_NAME" = "$HARNESS_PKG" ]; then
|
|
271
|
+
# Repo fuente del paquete: se verifica el CLI, no un proyecto NestJS.
|
|
272
|
+
section "Paquete $HARNESS_PKG"
|
|
273
|
+
info "Repo fuente del harness: se omiten las verificaciones de proyecto NestJS"
|
|
274
|
+
need_file package.files bin/arness.mjs
|
|
275
|
+
need_file package.files CHANGELOG.md
|
|
276
|
+
need_file package.files docs/adr/README.md
|
|
277
|
+
for d in $H/decisiones/[0-9][0-9][0-9][0-9]-*.md; do
|
|
278
|
+
[ -f "$d" ] && issue package.decisions "$d: el paquete no publica decisiones de proyecto; las del harness van en docs/adr/"
|
|
279
|
+
done
|
|
280
|
+
if [ "$QUICK" -eq 1 ]; then info "--quick: se omiten los tests del CLI"
|
|
281
|
+
else run_step package.tests "Tests del CLI" node --test; fi
|
|
282
|
+
else
|
|
283
|
+
# ── Instalación del harness (manifiesto e integridad) ─────────────────────
|
|
284
|
+
section "Instalación del harness"
|
|
285
|
+
if [ -f $H/.arness.json ]; then
|
|
286
|
+
inst_v="$(node -p "require('./$H/.arness.json').version" 2>/dev/null)"
|
|
287
|
+
ok "Instalado $HARNESS_PKG@$inst_v"
|
|
288
|
+
cli="node_modules/$HARNESS_PKG/bin/arness.mjs"
|
|
289
|
+
if [ -f "$cli" ]; then
|
|
290
|
+
dep_v="$(node -p "require('./node_modules/$HARNESS_PKG/package.json').version" 2>/dev/null)"
|
|
291
|
+
[ "$dep_v" != "$inst_v" ] && warn "El paquete en node_modules es $dep_v y harness/ está en $inst_v: ejecuta pnpm arness sync" install.version
|
|
292
|
+
if node "$cli" status --check >"$LOG_DIR/status.log" 2>&1; then ok "La base coincide con el manifiesto"
|
|
293
|
+
else issue install.integrity "La base del harness fue modificada o está incompleta (pnpm arness status)"; sed 's/^/ │ /' "$LOG_DIR/status.log"; fi
|
|
294
|
+
elif [ -f package.json ]; then
|
|
295
|
+
warn "$HARNESS_PKG no está en node_modules: no se pudo verificar la integridad de la base" install.integrity
|
|
296
|
+
fi
|
|
297
|
+
else
|
|
298
|
+
issue install.manifest "Falta $H/.arness.json: instala el harness (ver README de $HARNESS_PKG) y ejecuta arness init"
|
|
299
|
+
fi
|
|
300
|
+
fi
|
|
301
|
+
|
|
302
|
+
if [ "$PKG_NAME" = "$HARNESS_PKG" ]; then
|
|
303
|
+
:
|
|
304
|
+
elif [ ! -f package.json ]; then
|
|
305
|
+
section "Proyecto"
|
|
306
|
+
info "Sin package.json: modo solo harness. Para crear la base ejecuta la skill nest-base ($H/skills/nest-base)."
|
|
307
|
+
else
|
|
308
|
+
section "Toolchain"
|
|
309
|
+
|
|
310
|
+
if command -v node >/dev/null 2>&1; then
|
|
311
|
+
node_v="$(node -v)"
|
|
312
|
+
if [ -f .nvmrc ]; then
|
|
313
|
+
want="$(tr -d '[:space:]v' < .nvmrc)"; have="${node_v#v}"
|
|
314
|
+
if [ "${want%%.*}" = "${have%%.*}" ]; then ok "Node $node_v (esperado v$want por .nvmrc)"
|
|
315
|
+
else issue toolchain.node "Node $node_v no coincide con .nvmrc (v$want)"; fi
|
|
316
|
+
else
|
|
317
|
+
issue toolchain.nvmrc "Falta .nvmrc con la versión LTS de Node"
|
|
318
|
+
fi
|
|
319
|
+
else
|
|
320
|
+
issue toolchain.node "Node no está instalado"
|
|
321
|
+
fi
|
|
322
|
+
|
|
323
|
+
if command -v pnpm >/dev/null 2>&1; then ok "pnpm $(pnpm -v)"
|
|
324
|
+
else issue toolchain.pnpm "pnpm no está instalado"; fi
|
|
325
|
+
|
|
326
|
+
pm="$(node -p "require('./package.json').packageManager || ''" 2>/dev/null)"
|
|
327
|
+
case "$pm" in pnpm@*) ok "packageManager: $pm" ;; *) issue toolchain.package-manager "package.json debe declarar packageManager pnpm@<versión>" ;; esac
|
|
328
|
+
|
|
329
|
+
[ -f pnpm-lock.yaml ] && ok "pnpm-lock.yaml" || issue toolchain.lockfile "Falta pnpm-lock.yaml"
|
|
330
|
+
for lock in package-lock.json yarn.lock; do
|
|
331
|
+
[ -f "$lock" ] && issue toolchain.lockfile "$lock no está permitido: solo pnpm"
|
|
332
|
+
done
|
|
333
|
+
|
|
334
|
+
for s in lint:check format:check build test test:e2e; do
|
|
335
|
+
node -e "process.exit(require('./package.json').scripts?.['$s'] ? 0 : 1)" 2>/dev/null \
|
|
336
|
+
|| issue toolchain.scripts "package.json no define el script '$s'"
|
|
337
|
+
done
|
|
338
|
+
|
|
339
|
+
# ───────────────────────────────────────────────────────────────────────────
|
|
340
|
+
section "Estructura"
|
|
341
|
+
|
|
342
|
+
for f in src/main.ts src/app.module.ts eslint.config.mjs .prettierrc .env.example \
|
|
343
|
+
FRONT.md Dockerfile azure-pipelines.yml postman/arness-back.postman_collection.json; do
|
|
344
|
+
need_file structure.files "$f"
|
|
345
|
+
done
|
|
346
|
+
# Documentación del proyecto (AGENTS.md §12): FRONT.md en la raíz y docs/ con avance y pendientes.
|
|
347
|
+
for f in docs/TECNICO.md docs/EVOLUTIVO.md docs/PENDIENTES.md; do
|
|
348
|
+
need_file docs.files "$f"
|
|
349
|
+
done
|
|
350
|
+
for d in src/config src/common/auth src/common/database src/common/cache src/common/http \
|
|
351
|
+
src/common/logging src/common/health src/modules test; do
|
|
352
|
+
need_dir structure.dirs "$d"
|
|
353
|
+
done
|
|
354
|
+
|
|
355
|
+
if [ -d src/modules ]; then
|
|
356
|
+
for mod in $(find src/modules -mindepth 2 -maxdepth 2 -type d | sort); do
|
|
357
|
+
m="$(basename "$mod")"
|
|
358
|
+
[ -f "$mod/README.md" ] || issue structure.modules "$mod: falta README.md"
|
|
359
|
+
[ -f "$mod/$m.module.ts" ] || issue structure.modules "$mod: falta $m.module.ts"
|
|
360
|
+
done
|
|
361
|
+
fi
|
|
362
|
+
|
|
363
|
+
# El harness no debe entrar al build ni a la imagen.
|
|
364
|
+
for tsc in tsconfig.json tsconfig.build.json; do
|
|
365
|
+
if [ -f "$tsc" ]; then
|
|
366
|
+
node -e "const c=JSON.parse(require('fs').readFileSync('$tsc','utf8'));process.exit((c.exclude||[]).some(e=>/^\\.?\\/?harness/.test(e))?0:1)" 2>/dev/null \
|
|
367
|
+
|| issue structure.harness-isolation "$tsc no excluye harness/"
|
|
368
|
+
fi
|
|
369
|
+
done
|
|
370
|
+
if [ -f .dockerignore ]; then grep -qxE '/?harness/?' .dockerignore || issue structure.harness-isolation ".dockerignore no excluye harness/"
|
|
371
|
+
else issue structure.harness-isolation "Falta .dockerignore"; fi
|
|
372
|
+
|
|
373
|
+
if [ -f .gitignore ]; then
|
|
374
|
+
for p in .env .env.local "$H/progress.md"; do
|
|
375
|
+
grep -qxF "$p" .gitignore || issue structure.gitignore ".gitignore no excluye $p"
|
|
376
|
+
done
|
|
377
|
+
else
|
|
378
|
+
issue structure.gitignore "Falta .gitignore"
|
|
379
|
+
fi
|
|
380
|
+
|
|
381
|
+
if [ -f .env.example ]; then
|
|
382
|
+
for k in PORT DATABASES ACCESS_TOKEN JWT_ACCESS_SECRET JWT_ACCESS_EXPIRES_IN \
|
|
383
|
+
JWT_REFRESH_SECRET JWT_REFRESH_EXPIRES_IN CACHE_ENABLED REDIS_URL; do
|
|
384
|
+
grep -q "^$k=" .env.example || issue structure.env-example ".env.example no define $k"
|
|
385
|
+
done
|
|
386
|
+
grep -q '^CORS_ORIGINS=' .env.example || issue security.cors-env ".env.example no define CORS_ORIGINS (allowlist de orígenes, AGENTS.md §9)"
|
|
387
|
+
if [ -d src/common/monitor ]; then
|
|
388
|
+
for k in MONITOR_ENABLED MONITOR_SESSION_EXPIRES_IN; do
|
|
389
|
+
grep -q "^$k=" .env.example || issue security.monitor-env ".env.example no define $k (src/common/monitor existe)"
|
|
390
|
+
done
|
|
391
|
+
fi
|
|
392
|
+
dbs="$(grep -m1 '^DATABASES=' .env.example | cut -d= -f2 | tr ',' ' ')"
|
|
393
|
+
for db in $dbs; do
|
|
394
|
+
for part in HOST PORT NAME USER PASSWORD; do
|
|
395
|
+
grep -q "^DB_${db}_${part}=" .env.example || issue structure.env-example ".env.example no define DB_${db}_${part}"
|
|
396
|
+
done
|
|
397
|
+
done
|
|
398
|
+
fi
|
|
399
|
+
|
|
400
|
+
# ───────────────────────────────────────────────────────────────────────────
|
|
401
|
+
section "Reglas de SQL"
|
|
402
|
+
|
|
403
|
+
if [ -d src ]; then
|
|
404
|
+
# SQL al inicio de un literal o de una línea (consultas multilínea), también tras "(" y con escrituras.
|
|
405
|
+
# Se evitan falsos positivos de identificadores: SELECT = ..., WITH = ..., 'DELETE' (método HTTP).
|
|
406
|
+
sql_kw='SELECT([[:space:]]+[^=[:space:]]|[[:space:]]*$)|WITH[[:space:]]+(RECURSIVE[[:space:]]+)?[A-Za-z_][A-Za-z0-9_]*[[:space:]]*(\(|AS([[:space:]]|$))'
|
|
407
|
+
sql_kw="$sql_kw|INSERT[[:space:]]+INTO[[:space:]]|UPDATE[[:space:]]+[^[:space:]]+[[:space:]]+SET[[:space:]]|DELETE[[:space:]]+FROM[[:space:]]|MERGE[[:space:]]+INTO[[:space:]]"
|
|
408
|
+
sql_out="$(grep -rlE --include='*.ts' "(^|[\`'\"(])[[:space:]]*\(?[[:space:]]*($sql_kw)" src 2>/dev/null \
|
|
409
|
+
| grep -vE '\.sql(\.spec)?\.ts$')"
|
|
410
|
+
if [ -n "$sql_out" ]; then issue sql.outside "Texto SQL fuera de *.sql.ts: $(echo $sql_out)"
|
|
411
|
+
else ok "SQL solo en *.sql.ts"; fi
|
|
412
|
+
|
|
413
|
+
sql_write="$(grep -rliE --include='*.sql.ts' '(INSERT[[:space:]]+INTO|DELETE[[:space:]]+FROM|UPDATE[[:space:]]+[^[:space:]]+[[:space:]]+SET|TRUNCATE|(DROP|ALTER|CREATE)[[:space:]]+(TABLE|VIEW|INDEX|SCHEMA|DATABASE|FUNCTION|MATERIALIZED))' src 2>/dev/null)"
|
|
414
|
+
if [ -n "$sql_write" ]; then issue sql.write "Escritura o DDL en SQL (la BD es de solo lectura): $(echo $sql_write)"
|
|
415
|
+
else ok "Sin escrituras ni DDL"; fi
|
|
416
|
+
fi
|
|
417
|
+
|
|
418
|
+
# ───────────────────────────────────────────────────────────────────────────
|
|
419
|
+
section "Arquitectura"
|
|
420
|
+
|
|
421
|
+
if [ -f eslint.config.mjs ]; then
|
|
422
|
+
if [ -f eslint.boundaries.mjs ] && grep -q 'architectureBoundaries' eslint.config.mjs; then
|
|
423
|
+
ok "ESLint verifica las fronteras de arquitectura (eslint.boundaries.mjs)"
|
|
424
|
+
else
|
|
425
|
+
issue architecture.eslint-boundaries "eslint.config.mjs no aplica las fronteras: copia harness/skills/nest-base/templates/eslint.boundaries.mjs e impórtala (skill nest-base §4)"
|
|
426
|
+
fi
|
|
427
|
+
fi
|
|
428
|
+
|
|
429
|
+
if [ -d src ]; then
|
|
430
|
+
# Fronteras (AGENTS.md §4), por si el ESLint del proyecto aún no las tiene, y patrón por endpoint (§5).
|
|
431
|
+
arch="$(node -e '
|
|
432
|
+
const fs = require("fs"), path = require("path");
|
|
433
|
+
const walk = (dir) => fs.existsSync(dir) ? fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
|
|
434
|
+
const p = path.join(dir, e.name);
|
|
435
|
+
return e.isDirectory() ? [p + "/", ...walk(p)] : [p];
|
|
436
|
+
}) : [];
|
|
437
|
+
const out = (id, msg) => console.log(id + "\t" + msg);
|
|
438
|
+
const importsOf = (f) => [...fs.readFileSync(f, "utf8").matchAll(/(?:from|import|require\()\s*["\x27]([^"\x27]+)["\x27]/g)].map((m) => m[1]);
|
|
439
|
+
|
|
440
|
+
const common = walk("src/common").filter((f) => f.endsWith(".ts"));
|
|
441
|
+
const bad = common.filter((f) => importsOf(f).some((s) => /(^|\/)modules(\/|$)/.test(s)));
|
|
442
|
+
if (bad.length) out("architecture.common-imports-modules", "src/common importa de src/modules: " + bad.join(" "));
|
|
443
|
+
|
|
444
|
+
const TYPES = [
|
|
445
|
+
["controller", /\.controller\.ts$/, "controllers"], ["service", /\.service\.ts$/, "services"],
|
|
446
|
+
["repository", /\.repository\.ts$/, "repositories"], ["sql", /\.sql\.ts$/, "sql"],
|
|
447
|
+
["columns", /\.columns\.ts$/, "columns"], ["dto", /\.dto\.ts$/, "dto"], ["entity", /\.entity\.ts$/, "entities"],
|
|
448
|
+
];
|
|
449
|
+
const files = walk("src/modules").filter((f) => f.endsWith(".ts") && !f.endsWith(".spec.ts"));
|
|
450
|
+
const crossed = [];
|
|
451
|
+
for (const f of files) {
|
|
452
|
+
const [, client, mod] = f.split("/").slice(1);
|
|
453
|
+
for (const s of importsOf(f)) {
|
|
454
|
+
const target = s.startsWith(".") ? path.normalize(path.join(path.dirname(f), s)) : s;
|
|
455
|
+
const m = /^src\/modules\/([^/]+)(?:\/([^/]+))?/.exec(target);
|
|
456
|
+
if (m && (m[1] !== client || (m[2] && m[2] !== mod))) crossed.push(f + " → " + s);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (crossed.length) out("architecture.module-imports", "Imports entre módulos o clientes: " + crossed.join(", "));
|
|
460
|
+
|
|
461
|
+
const byDir = new Map();
|
|
462
|
+
for (const f of files) { const d = path.dirname(f); byDir.set(d, [...(byDir.get(d) || []), path.basename(f)]); }
|
|
463
|
+
for (const [dir, names] of byDir) {
|
|
464
|
+
if (dir.split("/").length < 4) continue;
|
|
465
|
+
for (const [type, re, folder] of TYPES) {
|
|
466
|
+
const n = names.filter((x) => re.test(x)).length;
|
|
467
|
+
if (n > 2 && path.basename(dir) !== folder)
|
|
468
|
+
out("architecture.grouping", dir + "/: " + n + " archivos " + type + "; agrúpalos en " + folder + "/ (AGENTS.md §5)");
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const SPEC = ["controller", "service", "repository", "sql", "query.dto"];
|
|
473
|
+
for (const ctrl of files.filter((f) => f.endsWith(".controller.ts"))) {
|
|
474
|
+
const parts = ctrl.split("/");
|
|
475
|
+
if (parts.length < 5) continue;
|
|
476
|
+
const root = parts.slice(0, 4).join("/");
|
|
477
|
+
const all = walk(root).filter((f) => f.endsWith(".ts"));
|
|
478
|
+
const e = path.basename(ctrl, ".controller.ts");
|
|
479
|
+
const want = [e + ".controller.ts", e + ".service.ts", e + ".repository.ts", e + ".sql.ts", e + ".columns.ts",
|
|
480
|
+
e + "-query.dto.ts", e + "-filters.dto.ts", e + ".entity.ts"];
|
|
481
|
+
const missing = [];
|
|
482
|
+
for (const name of want) {
|
|
483
|
+
const found = all.find((f) => path.basename(f) === name);
|
|
484
|
+
if (!found) { missing.push(name); continue; }
|
|
485
|
+
if (SPEC.some((t) => name.endsWith("." + t + ".ts") || name.endsWith("-" + t + ".ts")) &&
|
|
486
|
+
!fs.existsSync(found.replace(/\.ts$/, ".spec.ts"))) missing.push(name.replace(/\.ts$/, ".spec.ts"));
|
|
487
|
+
}
|
|
488
|
+
if (missing.length) out("architecture.endpoint-files", "Endpoint " + e + " (" + root + "): falta " + missing.join(", "));
|
|
489
|
+
}
|
|
490
|
+
' 2>&1)"
|
|
491
|
+
arch_before=$((FAILS + WARNS))
|
|
492
|
+
while IFS="$(printf '\t')" read -r id msg; do
|
|
493
|
+
[ -n "$id" ] && issue "$id" "$msg"
|
|
494
|
+
done <<EOF
|
|
495
|
+
$arch
|
|
496
|
+
EOF
|
|
497
|
+
[ "$((FAILS + WARNS))" -eq "$arch_before" ] && ok "Fronteras, agrupación y patrón por endpoint (AGENTS.md §4 y §5)"
|
|
498
|
+
fi
|
|
499
|
+
|
|
500
|
+
# ───────────────────────────────────────────────────────────────────────────
|
|
501
|
+
section "Variables de entorno"
|
|
502
|
+
|
|
503
|
+
if [ -d src ]; then
|
|
504
|
+
# Configuración centralizada (AGENTS.md §14): process.env solo en src/config/; los tests pueden usarlo.
|
|
505
|
+
if [ -f src/config/configuration.ts ]; then ok "src/config/configuration.ts"
|
|
506
|
+
else issue env.configuration-file "Falta src/config/configuration.ts: copia harness/skills/nest-base/templates/config/ (configuracion.md, Dónde se leen)"; fi
|
|
507
|
+
|
|
508
|
+
env_out="$(grep -rnE --include='*.ts' 'process\.env|\{[^}]*\benv\b[^}]*\}[[:space:]]*=[[:space:]]*process\b' src 2>/dev/null \
|
|
509
|
+
| grep -vE '^[^:]+:[0-9]+:[[:space:]]*(//|/?\*)' | cut -d: -f1 \
|
|
510
|
+
| grep -vE '^src/config/|\.spec\.ts$' | sort -u)"
|
|
511
|
+
if [ -n "$env_out" ]; then issue env.process-env "process.env fuera de src/config/ (usa ConfigService): $(echo $env_out)"
|
|
512
|
+
else ok "process.env solo en src/config/"; fi
|
|
513
|
+
fi
|
|
514
|
+
if [ -f eslint.config.mjs ]; then
|
|
515
|
+
if grep -q 'configBoundaries' eslint.config.mjs; then ok "ESLint restringe process.env a src/config/ (configBoundaries)"
|
|
516
|
+
else issue env.eslint "eslint.config.mjs no aplica configBoundaries de eslint.boundaries.mjs (process.env solo en src/config/)"; fi
|
|
517
|
+
fi
|
|
518
|
+
|
|
519
|
+
# ───────────────────────────────────────────────────────────────────────────
|
|
520
|
+
section "Seguridad"
|
|
521
|
+
|
|
522
|
+
if [ -f src/main.ts ]; then
|
|
523
|
+
if grep -qE 'enableCors[[:space:]]*\([[:space:]]*\)|cors:[[:space:]]*true|origin:[[:space:]]*(true|["'"'"']\*["'"'"'])' src/main.ts; then
|
|
524
|
+
issue security.cors-open "src/main.ts abre CORS a cualquier origen: usa la allowlist de CORS_ORIGINS (contrato-http.md, CORS)"
|
|
525
|
+
elif grep -q 'enableCors' src/main.ts; then ok "CORS configurado en src/main.ts"
|
|
526
|
+
else issue security.cors "src/main.ts no configura CORS con la allowlist de CORS_ORIGINS (contrato-http.md, CORS)"; fi
|
|
527
|
+
fi
|
|
528
|
+
if [ -f .env.example ] && grep -E '^CORS_ORIGINS=' .env.example | grep -q '\*'; then
|
|
529
|
+
issue security.cors-open ".env.example: CORS_ORIGINS no admite comodines"
|
|
530
|
+
fi
|
|
531
|
+
|
|
532
|
+
if [ -d src/common/monitor ]; then
|
|
533
|
+
mon_ctrl="$(grep -rl --include='*.controller.ts' '@Controller' src/common/monitor 2>/dev/null)"
|
|
534
|
+
if [ -z "$mon_ctrl" ] || grep -L 'MonitorSessionGuard' $mon_ctrl | grep -q .; then
|
|
535
|
+
issue security.monitor-guard "El controller de src/common/monitor debe usar MonitorSessionGuard (AGENTS.md §10)"
|
|
536
|
+
elif grep -rqE '^[[:space:]]*@Public\(' src/common/monitor; then
|
|
537
|
+
issue security.monitor-guard "src/common/monitor no usa @Public(): lo protege MonitorSessionGuard (AGENTS.md §10)"
|
|
538
|
+
else
|
|
539
|
+
ok "Monitor protegido por MonitorSessionGuard"
|
|
540
|
+
fi
|
|
541
|
+
fi
|
|
542
|
+
|
|
543
|
+
# ───────────────────────────────────────────────────────────────────────────
|
|
544
|
+
section "Logging"
|
|
545
|
+
|
|
546
|
+
# JSON en producción; formato legible solo en desarrollo (LOG_FORMAT=pretty, contrato-http.md "Logging").
|
|
547
|
+
if node -e "process.exit(require('./package.json').dependencies?.['pino-pretty'] ? 0 : 1)" 2>/dev/null; then
|
|
548
|
+
issue logging.pretty-production "pino-pretty está en dependencies: va solo en devDependencies (producción registra JSON)"
|
|
549
|
+
fi
|
|
550
|
+
if [ -f .env.example ]; then
|
|
551
|
+
if grep -q '^LOG_FORMAT=' .env.example; then
|
|
552
|
+
if grep -qE '^NODE_ENV=production' .env.example && grep -qE '^LOG_FORMAT=pretty' .env.example; then
|
|
553
|
+
issue logging.pretty-production ".env.example combina NODE_ENV=production con LOG_FORMAT=pretty (la app no arranca así)"
|
|
554
|
+
else
|
|
555
|
+
ok "LOG_FORMAT definido en .env.example"
|
|
556
|
+
fi
|
|
557
|
+
else
|
|
558
|
+
issue logging.format-env ".env.example no define LOG_FORMAT (json|pretty; configuracion.md, App)"
|
|
559
|
+
fi
|
|
560
|
+
fi
|
|
561
|
+
|
|
562
|
+
# ───────────────────────────────────────────────────────────────────────────
|
|
563
|
+
if [ "$QUICK" -eq 1 ]; then
|
|
564
|
+
section "Calidad"
|
|
565
|
+
info "--quick: se omiten dependencias, lint, formato, build, tests y health"
|
|
566
|
+
else
|
|
567
|
+
section "Dependencias"
|
|
568
|
+
if [ -d node_modules ]; then
|
|
569
|
+
ok "node_modules presente"
|
|
570
|
+
elif [ "$INSTALL" -eq 1 ]; then
|
|
571
|
+
run_step quality.deps "pnpm install --frozen-lockfile" pnpm install --frozen-lockfile
|
|
572
|
+
else
|
|
573
|
+
issue quality.deps "Faltan dependencias: ejecuta ./harness/init.sh --install (pnpm install --frozen-lockfile)"
|
|
574
|
+
fi
|
|
575
|
+
|
|
576
|
+
if [ -d node_modules ]; then
|
|
577
|
+
section "Calidad"
|
|
578
|
+
run_step quality.lint "Lint" pnpm run -s lint:check
|
|
579
|
+
run_step quality.format "Formato" pnpm run -s format:check
|
|
580
|
+
run_step quality.build "Build" pnpm run -s build
|
|
581
|
+
run_step quality.test "Tests unitarios" pnpm run -s test -- --ci
|
|
582
|
+
[ "$E2E" -eq 1 ] && run_step quality.e2e "Tests e2e" pnpm run -s test:e2e -- --ci
|
|
583
|
+
|
|
584
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
585
|
+
if [ "$HEALTH" -eq 1 ]; then
|
|
586
|
+
section "Health"
|
|
587
|
+
port="${PORT:-}"
|
|
588
|
+
if [ -z "$port" ]; then
|
|
589
|
+
for envf in .env.local .env; do
|
|
590
|
+
[ -f "$envf" ] && port="$(grep -m1 '^PORT=' "$envf" | cut -d= -f2 | tr -d '[:space:]"'"'")"
|
|
591
|
+
[ -n "$port" ] && break
|
|
592
|
+
done
|
|
593
|
+
fi
|
|
594
|
+
url="${HEALTH_URL:-http://localhost:${port:-3000}/health}"
|
|
595
|
+
body="$LOG_DIR/health.json"
|
|
596
|
+
|
|
597
|
+
probe() { curl -s -m 5 -o "$body" -w '%{http_code}' "$url" 2>/dev/null; }
|
|
598
|
+
code="$(probe)"
|
|
599
|
+
|
|
600
|
+
if [ "$code" = "000" ]; then
|
|
601
|
+
if [ -f dist/main.js ]; then
|
|
602
|
+
info "La app no está corriendo; se arranca temporalmente desde dist/main.js"
|
|
603
|
+
node dist/main.js >"$LOG_DIR/app.log" 2>&1 &
|
|
604
|
+
APP_PID=$!
|
|
605
|
+
waited=0; limit="${HEALTH_TIMEOUT_S:-30}"
|
|
606
|
+
while [ "$waited" -lt "$limit" ]; do
|
|
607
|
+
kill -0 "$APP_PID" 2>/dev/null || break
|
|
608
|
+
code="$(probe)"; [ "$code" != "000" ] && break
|
|
609
|
+
sleep 1; waited=$((waited + 1))
|
|
610
|
+
done
|
|
611
|
+
fi
|
|
612
|
+
fi
|
|
613
|
+
|
|
614
|
+
if [ "$code" = "000" ]; then
|
|
615
|
+
issue health.unreachable "No se pudo consultar $url (la app no responde o no arrancó)"
|
|
616
|
+
[ -f "$LOG_DIR/app.log" ] && tail -n 25 "$LOG_DIR/app.log" | sed 's/^/ │ /'
|
|
617
|
+
else
|
|
618
|
+
report="$(node -e '
|
|
619
|
+
let b; try { b = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")); }
|
|
620
|
+
catch { console.log("INVALID"); process.exit(0); }
|
|
621
|
+
const d = b.details || { ...(b.info || {}), ...(b.error || {}) };
|
|
622
|
+
for (const [k, v] of Object.entries(d)) {
|
|
623
|
+
console.log([k, (v && v.status) || "unknown", (v && v.message) || ""].join("\t"));
|
|
624
|
+
}' "$body")"
|
|
625
|
+
if [ "$report" = "INVALID" ]; then
|
|
626
|
+
issue health.invalid "/health respondió $code con un cuerpo que no es JSON"
|
|
627
|
+
else
|
|
628
|
+
db_down=0
|
|
629
|
+
while IFS="$(printf '\t')" read -r key status msg; do
|
|
630
|
+
[ -z "$key" ] && continue
|
|
631
|
+
case "$key" in
|
|
632
|
+
db_*)
|
|
633
|
+
name="$(echo "${key#db_}" | tr '[:lower:]' '[:upper:]')"
|
|
634
|
+
if [ "$status" = "up" ]; then ok "Base de datos $name: arriba"
|
|
635
|
+
else issue health.db "Base de datos $name CAÍDA${msg:+: $msg}"; db_down=1; fi ;;
|
|
636
|
+
cache)
|
|
637
|
+
case "$status" in
|
|
638
|
+
up) ok "Caché (Redis): arriba" ;;
|
|
639
|
+
disabled) info "Caché deshabilitada (CACHE_ENABLED=false)" ;;
|
|
640
|
+
*) warn "Caché (Redis) caída${msg:+: $msg}; la API sigue funcionando en BYPASS" health.cache ;;
|
|
641
|
+
esac ;;
|
|
642
|
+
*)
|
|
643
|
+
[ "$status" = "up" ] && ok "$key: arriba" || warn "$key: $status" health.other ;;
|
|
644
|
+
esac
|
|
645
|
+
done <<EOF
|
|
646
|
+
$report
|
|
647
|
+
EOF
|
|
648
|
+
if [ "$code" != "200" ] && [ "$db_down" -eq 0 ]; then
|
|
649
|
+
issue health.status "/health respondió HTTP $code"
|
|
650
|
+
fi
|
|
651
|
+
fi
|
|
652
|
+
fi
|
|
653
|
+
fi
|
|
654
|
+
fi
|
|
655
|
+
fi
|
|
656
|
+
fi
|
|
657
|
+
|
|
658
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
659
|
+
EXIT=0
|
|
660
|
+
{ [ "$CRITS" -gt 0 ] || [ "$FAILS" -gt 0 ]; } && EXIT=1
|
|
661
|
+
MODE=$([ "$STRICT" -eq 1 ] && echo strict || echo default)
|
|
662
|
+
|
|
663
|
+
printf '\n%s──────────────────────────────────────────────%s\n' "$BOLD" "$RESET"
|
|
664
|
+
if [ -s "$ISSUES" ]; then
|
|
665
|
+
# Resumen agrupado por categoría (sección de la revisión).
|
|
666
|
+
printf '%sResumen por categoría:%s\n' "$BOLD" "$RESET"
|
|
667
|
+
awk -F'\t' '$1!="ignored"{ if (!($3 in seen)) { seen[$3]=1; order[++n]=$3 } items[$3]=items[$3] " - [" $1 "] " $4 ($2!="-" ? " (" $2 ")" : "") "\n" }
|
|
668
|
+
END { for (i=1;i<=n;i++) printf " %s\n%s", order[i], items[order[i]] }' "$ISSUES"
|
|
669
|
+
[ "$IGNORED" -gt 0 ] && printf ' (%d chequeo(s) silenciado(s) en %s)\n' "$IGNORED" "$CONFIG"
|
|
670
|
+
printf '\n'
|
|
671
|
+
fi
|
|
672
|
+
if [ "$CRITS" -gt 0 ]; then
|
|
673
|
+
printf '%s%s✖ Fallo crítico (%d). Corrígelo antes de continuar.%s\n' "$BOLD" "$RED" "$CRITS" "$RESET"
|
|
674
|
+
printf '%sAgente: detente, informa al usuario y espera instrucciones (AGENTS.md §0).%s\n' "$YELLOW" "$RESET"
|
|
675
|
+
elif [ "$FAILS" -gt 0 ]; then
|
|
676
|
+
printf '%s%s✖ %d hallazgo(s) obligatorio(s) (modo %s), %d advertencia(s).%s\n' "$BOLD" "$RED" "$FAILS" "$MODE" "$WARNS" "$RESET"
|
|
677
|
+
printf '%sAgente: no continúes. Informa estos fallos al usuario y espera instrucciones (AGENTS.md §0).%s\n' "$YELLOW" "$RESET"
|
|
678
|
+
elif [ "$WARNS" -gt 0 ]; then
|
|
679
|
+
printf '%s%s✔ Proyecto operativo con %d advertencia(s).%s\n' "$BOLD" "$GREEN" "$WARNS" "$RESET"
|
|
680
|
+
printf '%sAgente: informa las advertencias al usuario y continúa; no las corrijas si no son parte de tu tarea (AGENTS.md §0).%s\n' "$YELLOW" "$RESET"
|
|
681
|
+
else
|
|
682
|
+
printf '%s%s✔ Proyecto en buen estado%s (0 advertencias).\n' "$BOLD" "$GREEN" "$RESET"
|
|
683
|
+
fi
|
|
684
|
+
|
|
685
|
+
if [ "$JSON" -eq 1 ]; then
|
|
686
|
+
node -e '
|
|
687
|
+
const [file, mode, exitCode, crit, fail, warn, ign] = process.argv.slice(1);
|
|
688
|
+
const issues = require("fs").readFileSync(file, "utf8").split("\n").filter(Boolean).map((l) => {
|
|
689
|
+
const [level, id, section, message] = l.split("\t");
|
|
690
|
+
return { level, id: id === "-" ? null : id, section, message };
|
|
691
|
+
});
|
|
692
|
+
const summary = { critical: +crit, failures: +fail, warnings: +warn, ignored: +ign };
|
|
693
|
+
process.stdout.write(JSON.stringify({ version: 1, mode, exitCode: +exitCode, summary, issues }, null, 2) + "\n");
|
|
694
|
+
' "$ISSUES" "$MODE" "$EXIT" "$CRITS" "$FAILS" "$WARNS" "$IGNORED" >&3
|
|
695
|
+
fi
|
|
696
|
+
exit "$EXIT"
|