@saulwade/swl-ses 2.6.1 → 2.8.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/CLAUDE.md +14 -2
- package/README.md +65 -18
- package/agentes/_intent-spec.md +73 -73
- package/agentes/_propose-step.md +90 -90
- package/bin/swl-ses.js +10 -0
- package/comandos/swl/brainstorm.md +1 -0
- package/comandos/swl/briefing.md +119 -119
- package/comandos/swl/contribuir.md +233 -233
- package/comandos/swl/deuda-codigo.md +97 -97
- package/comandos/swl/mcp-status.md +1 -0
- package/gateway/lib/event-channel.js +191 -191
- package/habilidades/agent-deep-links/SKILL.md +148 -148
- package/habilidades/backend-async-postgres-testing/SKILL.md +216 -216
- package/habilidades/backend-error-design/SKILL.md +221 -221
- package/habilidades/backend-production-resilience/SKILL.md +288 -288
- package/habilidades/calidad-anti-patrones-universales/SKILL.md +105 -1
- package/habilidades/calidad-contract-testing/SKILL.md +165 -165
- package/habilidades/calidad-mutation-testing/SKILL.md +25 -1
- package/habilidades/checklist-seguridad/recursos/stride-cobertura.md +60 -60
- package/habilidades/ci-cd-pipelines/SKILL.md +5 -1
- package/habilidades/css-moderno/SKILL.md +7 -1
- package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
- package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
- package/habilidades/estructura-proyecto-claude/recursos/mcp-json-template.json +57 -57
- package/habilidades/extractor-de-aprendizajes/SKILL.md +5 -1
- package/habilidades/feynman-auditor-swl/recursos/preguntas-language-agnostic.md +108 -108
- package/habilidades/harness-claude-code/SKILL.md +3 -2
- package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
- package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
- package/habilidades/perfil-usuario/SKILL.md +200 -200
- package/habilidades/prevencion-sobreingenieria/recursos/EXAMPLES.md +580 -580
- package/habilidades/prevencion-sobreingenieria/recursos/soluciones-nativas.md +166 -166
- package/habilidades/prevencion-sobreingenieria/recursos/variables-residuales-post-refactor.md +85 -85
- package/habilidades/proceso-ddia-streaming/SKILL.md +231 -231
- package/habilidades/proceso-discovery-machote/SKILL.md +157 -157
- package/habilidades/proceso-dynamic-workflows/SKILL.md +60 -0
- package/habilidades/proceso-dynamic-workflows/recursos/template-adversarial-verify.js +65 -65
- package/habilidades/proceso-dynamic-workflows/recursos/template-triage.js +65 -65
- package/habilidades/proceso-intent-engineering/SKILL.md +269 -269
- package/habilidades/proceso-modular-split/SKILL.md +256 -256
- package/habilidades/state-inconsistency-auditor-swl/recursos/coupled-state-patterns.md +147 -147
- package/habilidades/swl-claudemd/recursos/contrato-aprender.md +83 -83
- package/habilidades/swl-claudemd/recursos/duplicacion-reglas-globales.md +85 -85
- package/habilidades/swl-claudemd/recursos/plantillas-init.md +94 -94
- package/habilidades/tdd-workflow/recursos/gherkin-bdd.md +111 -111
- package/hooks/calidad-pre-commit.js +159 -10
- package/hooks/ciclo-evolucion-subagente.js +26 -26
- package/hooks/ciclo-evolucion.js +26 -26
- package/hooks/contexto-subagente.js +68 -68
- package/hooks/lib/auto-consolidator.js +335 -335
- package/hooks/lib/ciclo-evolucion.js +47 -47
- package/hooks/lib/deep-links.js +185 -185
- package/hooks/lib/error-classifier.js +308 -308
- package/hooks/lib/notificacion-formato.js +45 -11
- package/hooks/lib/provenance-tracker.js +191 -191
- package/hooks/lib/raiz-proyecto.js +35 -4
- package/hooks/lib/resource-quota.js +122 -122
- package/hooks/lib/retry-jitter.js +165 -165
- package/hooks/lib/security-net.js +201 -201
- package/hooks/lib/skill-auditor.js +588 -588
- package/hooks/lib/sync-status.js +228 -228
- package/hooks/lib/taint-tracker.js +107 -107
- package/hooks/lib/text-similarity.js +241 -241
- package/hooks/lib/toon-compressor.js +245 -245
- package/hooks/notificacion-telegram.js +5 -11
- package/hooks/session-briefing.js +12 -4
- package/instintos/autonomia.yaml +27 -27
- package/instintos/prompt-appendices.yaml +57 -57
- package/llms.txt +1 -1
- package/manifiestos/agent-output-schemas.json +57 -57
- package/manifiestos/canonical-hashes.json +662 -0
- package/manifiestos/harness-ir.json +47536 -0
- package/manifiestos/hooks-config.json +469 -469
- package/manifiestos/invariantes-criticos.json +30 -30
- package/manifiestos/policy-bundle.json +2065 -0
- package/manifiestos/policy-corpus-w2.json +3926 -0
- package/manifiestos/runtime-adapters-core3.json +208 -0
- package/manifiestos/runtime-conformance.json +139 -0
- package/manifiestos/skills-lock.json +43 -43
- package/package.json +2 -2
- package/plantillas/auditor-veto-template.md +105 -105
- package/plantillas/github-workflows/release-please.yml +44 -44
- package/plantillas/github-workflows/swl-ci.yml +107 -107
- package/plantillas/github-workflows/swl-security.yml +51 -51
- package/plugin.json +2 -2
- package/reglas/accesibilidad.md +10 -10
- package/reglas/auditorias-documentales-estructurales.md +7 -7
- package/reglas/cloud-infra.md +8 -8
- package/reglas/consultar-vault-primero.md +195 -195
- package/reglas/git-workflow.md +1 -0
- package/reglas/hooks.md +6 -6
- package/reglas/intent-engineering.md +218 -218
- package/reglas/markitdown.md +8 -8
- package/reglas/monitor-ci.md +12 -0
- package/reglas/patrones.md +6 -6
- package/reglas/testing.md +7 -7
- package/reglas/tests-cleanup.md +224 -224
- package/schemas/agent-message.schema.json +73 -73
- package/schemas/agent-output-implementacion.schema.json +114 -114
- package/schemas/agent-output-planificacion.schema.json +150 -150
- package/schemas/agent-output-review.schema.json +98 -98
- package/schemas/diary-entry.schema.json +112 -112
- package/schemas/gate-state.schema.json +76 -0
- package/schemas/harness-ir.schema.json +369 -0
- package/schemas/hook-profiles.schema.json +54 -54
- package/schemas/hooks-config.schema.json +89 -89
- package/schemas/legacy-gates.schema.json +45 -0
- package/schemas/modulos.schema.json +38 -38
- package/schemas/perfiles.schema.json +36 -36
- package/schemas/plugin.schema.json +77 -77
- package/schemas/policy-bundle.schema.json +140 -0
- package/schemas/policy-enforcement.schema.json +117 -0
- package/schemas/policy-operation.schema.json +261 -0
- package/schemas/runtime-adapter.schema.json +176 -0
- package/schemas/runtime-build-attestation.schema.json +100 -0
- package/schemas/runtime-conformance.schema.json +239 -0
- package/schemas/runtime-diagnostic.schema.json +395 -0
- package/schemas/skill-evals.schema.json +119 -119
- package/schemas/skill-frontmatter.schema.json +245 -245
- package/schemas/w4-certification-request.schema.json +72 -0
- package/schemas/w4-certification-verdict.schema.json +224 -0
- package/schemas/w4-corpus.schema.json +172 -0
- package/schemas/w4-mutation-report.schema.json +116 -0
- package/schemas/w4-replay-result.schema.json +164 -0
- package/schemas/w4-scoring-report.schema.json +89 -0
- package/scripts/audit-tools/audit-history.js +330 -330
- package/scripts/audit-tools/bundle-tracker.js +290 -290
- package/scripts/audit-tools/canary-monitor.js +352 -352
- package/scripts/audit-tools/code-profiler.js +605 -605
- package/scripts/audit-tools/dep-doctor.js +320 -320
- package/scripts/audit-tools/env-validator.js +206 -206
- package/scripts/audit-tools/lib/fs-walk.js +48 -48
- package/scripts/audit-tools/lib/output.js +23 -23
- package/scripts/audit-tools/migration-checker.js +392 -392
- package/scripts/audit-tools/pentest-scanner.js +1436 -1436
- package/scripts/bootstrap-instintos.js +3 -0
- package/scripts/cli/aprobar-plan.js +73 -73
- package/scripts/cli/briefing.js +23 -23
- package/scripts/cli/ciclo-evolucion.js +26 -26
- package/scripts/cli/derivar-feature-list.js +25 -25
- package/scripts/cli/detectar-host.js +27 -27
- package/scripts/cli/diary-entry.js +69 -69
- package/scripts/cli/execution-state.js +18 -18
- package/scripts/cli/gateway-notify.js +41 -41
- package/scripts/cli/liberar-fase.js +42 -42
- package/scripts/cli/mark-evolved.js +56 -56
- package/scripts/cli/metricas-dora.js +26 -26
- package/scripts/cli/near-duplicate.js +55 -55
- package/scripts/cli/notificaciones.js +123 -123
- package/scripts/cli/propose-step.js +29 -29
- package/scripts/cli/schedule-parse.js +19 -19
- package/scripts/cli/sugerir-modelo.js +20 -20
- package/scripts/cli/verificar-plan.js +36 -36
- package/scripts/cli/verificar-trazabilidad.js +35 -35
- package/scripts/comandos/install-asistido.js +8 -7
- package/scripts/configurar-branch-protection.js +418 -418
- package/scripts/detectar-aprendizajes-duplicados.js +151 -151
- package/scripts/doctor.js +61 -36
- package/scripts/generar-checklists-consolidados.js +273 -273
- package/scripts/generar-claims-runtime.js +1342 -0
- package/scripts/generar-harness-ir.js +257 -0
- package/scripts/generar-inventario.js +52 -54
- package/scripts/generar-policy-bundle.js +202 -0
- package/scripts/instalador.js +26 -7
- package/scripts/lib/approval-receipts.js +190 -0
- package/scripts/lib/artefactos-python.js +43 -43
- package/scripts/lib/benchmark-metrics.js +160 -160
- package/scripts/lib/budget-enforcer.js +252 -252
- package/scripts/lib/certificacion-loop-state.js +421 -0
- package/scripts/lib/ci-reader.js +193 -193
- package/scripts/lib/ciclo-autonomo/yaml-instintos.js +56 -0
- package/scripts/lib/clasificar-directorio.js +92 -0
- package/scripts/lib/contadores-inventario.js +217 -217
- package/scripts/lib/detectar-host-swl.js +175 -175
- package/scripts/lib/detectar-runtime.js +29 -20
- package/scripts/lib/detectar-stack-detallado.js +307 -307
- package/scripts/lib/detector-autoduplicacion-intra-archivo.js +234 -234
- package/scripts/lib/detector-reglas-duplicadas.js +220 -220
- package/scripts/lib/eval-metrics-store.js +218 -218
- package/scripts/lib/eval-quality.js +171 -171
- package/scripts/lib/eval-schemas.js +144 -144
- package/scripts/lib/eval-self-correct.js +106 -106
- package/scripts/lib/eval-validator.js +185 -185
- package/scripts/lib/evidence-verifier.js +192 -0
- package/scripts/lib/evidencia-release.js +322 -322
- package/scripts/lib/frontmatter-canonico.js +509 -0
- package/scripts/lib/gate-engine.js +871 -0
- package/scripts/lib/gate-hooks-requires.js +249 -249
- package/scripts/lib/gate-licencias.js +212 -212
- package/scripts/lib/git-config-preflight.js +48 -0
- package/scripts/lib/git-metricas.js +257 -257
- package/scripts/lib/harness-ir.js +778 -0
- package/scripts/lib/harness-source-snapshot.js +309 -0
- package/scripts/lib/integrity-ledger.js +1147 -0
- package/scripts/lib/jaccard-similarity.js +98 -98
- package/scripts/lib/legacy-gate-migration.js +324 -0
- package/scripts/lib/limpiar-basura-global.js +45 -2
- package/scripts/lib/longmemeval-runner.js +125 -125
- package/scripts/lib/metricas-dora.js +204 -204
- package/scripts/lib/notificaciones-telegram.js +1 -0
- package/scripts/lib/npm-version.js +1 -0
- package/scripts/lib/paquetes-conocidos.js +50 -50
- package/scripts/lib/plan-lock.js +61 -13
- package/scripts/lib/policy-broker.js +338 -0
- package/scripts/lib/policy-bundle.js +342 -0
- package/scripts/lib/policy-context-provider.js +310 -0
- package/scripts/lib/policy-contract.js +479 -0
- package/scripts/lib/policy-verifier-utils.js +65 -0
- package/scripts/lib/pr-analyzer.js +399 -399
- package/scripts/lib/principal-verifier.js +178 -0
- package/scripts/lib/prompt-builder.js +264 -264
- package/scripts/lib/resolver-plan-fase.js +37 -37
- package/scripts/lib/rrf-fusion.js +175 -175
- package/scripts/lib/runtime-adapter-contract.js +267 -0
- package/scripts/lib/runtime-artifact-verifier.js +426 -0
- package/scripts/lib/runtime-build-attestation.js +127 -0
- package/scripts/lib/runtime-bundle-installer.js +586 -0
- package/scripts/lib/runtime-compiler.js +327 -0
- package/scripts/lib/runtime-conformance.js +202 -0
- package/scripts/lib/runtime-doctor-core3.js +567 -0
- package/scripts/lib/runtime-doctor-input.js +59 -0
- package/scripts/lib/runtime-operation-adapter.js +267 -0
- package/scripts/lib/schema-version.js +164 -164
- package/scripts/lib/semantic-search.js +252 -252
- package/scripts/lib/signed-envelope.js +545 -0
- package/scripts/lib/single-use-store.js +359 -0
- package/scripts/lib/skills-externas.js +31 -0
- package/scripts/lib/transformadores/codex.js +15 -8
- package/scripts/lib/transformadores/gemini.js +79 -5
- package/scripts/lib/w4-attestation-adapter.js +158 -0
- package/scripts/lib/w4-canario.js +337 -0
- package/scripts/lib/w4-claims.js +182 -0
- package/scripts/lib/w4-corpus-generador.js +542 -0
- package/scripts/lib/w4-gate-c5.js +115 -0
- package/scripts/lib/w4-harness-bajo-prueba.js +155 -0
- package/scripts/lib/w4-matriz-combos.js +55 -0
- package/scripts/lib/w4-motor-mutacion.js +1348 -0
- package/scripts/lib/w4-motor-replay.js +735 -0
- package/scripts/lib/w4-pin-origen.js +54 -0
- package/scripts/lib/w4-publicar-request.js +132 -0
- package/scripts/lib/w4-revocacion.js +62 -0
- package/scripts/lib/w4-runtimes-core3.js +38 -0
- package/scripts/lib/w4-scorer-certificacion.js +692 -0
- package/scripts/lib/w4-superficie-candidato.js +49 -0
- package/scripts/lib/w4-veredicto.js +452 -0
- package/scripts/lib/w4-verificar-veredicto.js +302 -0
- package/scripts/limpiar-artefactos-python.js +131 -131
- package/scripts/migrar-csv-a-array.js +168 -168
- package/scripts/migrar-fase-dominio.js +200 -200
- package/scripts/migrar-gates-legacy.js +108 -0
- package/scripts/publicar-certification-request.js +115 -0
- package/scripts/runtime-doctor.js +107 -0
- package/scripts/tui/componentes/selector-multi.js +189 -189
- package/scripts/tui/componentes/selector-unico.js +158 -158
- package/scripts/tui/ejecutores.js +375 -375
- package/scripts/tui/lib/colores.js +129 -129
- package/scripts/tui/lib/render.js +264 -264
- package/scripts/tui/lib/teclas.js +113 -113
- package/scripts/tui/pantallas/install-wizard.js +12 -7
- package/scripts/tui/pantallas/menu-principal.js +52 -52
- package/scripts/tui/pantallas/progreso.js +274 -274
- package/scripts/tui/pantallas/resumen.js +132 -132
- package/scripts/validar-userland-vacio.js +110 -110
- package/scripts/verificar-aislamiento-swl-eval.js +87 -0
- package/scripts/verificar-empaquetado-downstream.js +375 -0
- package/scripts/verificar-loop-constructor.js +215 -0
- package/scripts/verificar-trazabilidad.js +13 -6
- package/scripts/verificar-veredicto-real.js +84 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Estado verificable del programa de certificación C10.
|
|
5
|
+
*
|
|
6
|
+
* `eventos.jsonl` es la autoridad. `LOOP-STATE.md` es una vista derivada. La
|
|
7
|
+
* confianza en la cadena parte de un hash génesis que el caller protege fuera
|
|
8
|
+
* del directorio de estado.
|
|
9
|
+
*
|
|
10
|
+
* @module scripts/lib/certificacion-loop-state
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const crypto = require('node:crypto');
|
|
14
|
+
const fs = require('node:fs');
|
|
15
|
+
const path = require('node:path');
|
|
16
|
+
|
|
17
|
+
const { atomicWriteSync } = require('../../hooks/lib/atomic-write');
|
|
18
|
+
|
|
19
|
+
const ARCHIVO_EVENTOS = 'eventos.jsonl';
|
|
20
|
+
const ARCHIVO_SNAPSHOT = 'LOOP-STATE.md';
|
|
21
|
+
const ESTADO_GENESIS = 'bootstrap';
|
|
22
|
+
const HASH_GENESIS = '0'.repeat(64);
|
|
23
|
+
const HASH_PATTERN = /^[a-f0-9]{64}$/i;
|
|
24
|
+
const CAMPOS_PAYLOAD = [
|
|
25
|
+
'tipo',
|
|
26
|
+
'schemaVersion',
|
|
27
|
+
'secuencia',
|
|
28
|
+
'prevHash',
|
|
29
|
+
'timestamp',
|
|
30
|
+
'actor',
|
|
31
|
+
'nodo',
|
|
32
|
+
'estado',
|
|
33
|
+
'evidencia',
|
|
34
|
+
];
|
|
35
|
+
const CAMPOS_EVENTO = [...CAMPOS_PAYLOAD, 'hash'];
|
|
36
|
+
const CAMPOS_GENESIS = [
|
|
37
|
+
'schemaVersion',
|
|
38
|
+
'timestamp',
|
|
39
|
+
'actor',
|
|
40
|
+
'nodo',
|
|
41
|
+
'estado',
|
|
42
|
+
'evidencia',
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
function esObjetoPlano(valor) {
|
|
46
|
+
if (valor === null || typeof valor !== 'object') return false;
|
|
47
|
+
const prototipo = Object.getPrototypeOf(valor);
|
|
48
|
+
return prototipo === Object.prototype || prototipo === null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function validarValorJSON(valor, activos) {
|
|
52
|
+
if (valor === null || typeof valor === 'string' || typeof valor === 'boolean') return;
|
|
53
|
+
if (typeof valor === 'number' && Number.isFinite(valor)) return;
|
|
54
|
+
if (typeof valor !== 'object') throw new TypeError('tipo no serializable');
|
|
55
|
+
if (!Array.isArray(valor) && !esObjetoPlano(valor)) throw new TypeError('objeto no plano');
|
|
56
|
+
if (activos.has(valor)) throw new TypeError('referencia cíclica');
|
|
57
|
+
if (Object.getOwnPropertySymbols(valor).length > 0) throw new TypeError('clave symbol');
|
|
58
|
+
activos.add(valor);
|
|
59
|
+
validarContenedorJSON(valor, activos);
|
|
60
|
+
activos.delete(valor);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function validarContenedorJSON(valor, activos) {
|
|
64
|
+
if (Array.isArray(valor)) {
|
|
65
|
+
for (let indice = 0; indice < valor.length; indice += 1) {
|
|
66
|
+
if (!Object.hasOwn(valor, indice)) throw new TypeError('array disperso');
|
|
67
|
+
validarValorJSON(valor[indice], activos);
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
for (const clave of Object.keys(valor)) validarValorJSON(valor[clave], activos);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function validarEvidencia(evidencia) {
|
|
75
|
+
if (!esObjetoPlano(evidencia)) {
|
|
76
|
+
throw new TypeError('evidencia no es JSON serializable: debe ser objeto plano');
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
validarValorJSON(evidencia, new WeakSet());
|
|
80
|
+
} catch (error) {
|
|
81
|
+
throw new TypeError(`evidencia no es JSON serializable: ${error.message}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function ordenarValorCanonico(valor) {
|
|
86
|
+
if (Array.isArray(valor)) return valor.map(ordenarValorCanonico);
|
|
87
|
+
if (!esObjetoPlano(valor)) return valor;
|
|
88
|
+
const ordenado = Object.create(null);
|
|
89
|
+
for (const clave of Object.keys(valor).sort()) {
|
|
90
|
+
ordenado[clave] = ordenarValorCanonico(valor[clave]);
|
|
91
|
+
}
|
|
92
|
+
return ordenado;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function construirPayloadCanonico(evento) {
|
|
96
|
+
return {
|
|
97
|
+
tipo: evento.tipo,
|
|
98
|
+
schemaVersion: evento.schemaVersion,
|
|
99
|
+
secuencia: evento.secuencia,
|
|
100
|
+
prevHash: evento.prevHash,
|
|
101
|
+
timestamp: evento.timestamp,
|
|
102
|
+
actor: evento.actor,
|
|
103
|
+
nodo: evento.nodo,
|
|
104
|
+
estado: evento.estado,
|
|
105
|
+
evidencia: ordenarValorCanonico(evento.evidencia),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function calcularHashEvento(evento) {
|
|
110
|
+
validarEvidencia(evento.evidencia);
|
|
111
|
+
const payload = JSON.stringify(construirPayloadCanonico(evento));
|
|
112
|
+
return crypto.createHash('sha256').update(payload, 'utf8').digest('hex');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function validarTextoRequerido(objeto, campo) {
|
|
116
|
+
if (typeof objeto[campo] !== 'string' || objeto[campo].trim() === '') {
|
|
117
|
+
throw new TypeError(`campo requerido: ${campo}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function validarCamposBase(objeto) {
|
|
122
|
+
for (const campo of CAMPOS_GENESIS) {
|
|
123
|
+
if (!Object.hasOwn(objeto, campo) || objeto[campo] === null) {
|
|
124
|
+
throw new TypeError(`campo requerido: ${campo}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
for (const campo of CAMPOS_GENESIS.filter((campo) => campo !== 'evidencia')) {
|
|
128
|
+
validarTextoRequerido(objeto, campo);
|
|
129
|
+
}
|
|
130
|
+
if (Number.isNaN(Date.parse(objeto.timestamp))) {
|
|
131
|
+
throw new TypeError('campo requerido: timestamp');
|
|
132
|
+
}
|
|
133
|
+
validarEvidencia(objeto.evidencia);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function validarGenesis(genesis) {
|
|
137
|
+
if (!esObjetoPlano(genesis)) throw new TypeError('genesis debe ser un objeto');
|
|
138
|
+
validarCamposBase(genesis);
|
|
139
|
+
if (genesis.estado !== ESTADO_GENESIS) {
|
|
140
|
+
throw new TypeError(`estado génesis debe ser ${ESTADO_GENESIS}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function crearEventoGenesis(genesis) {
|
|
145
|
+
const payload = construirPayloadCanonico({
|
|
146
|
+
tipo: 'genesis',
|
|
147
|
+
schemaVersion: genesis.schemaVersion,
|
|
148
|
+
secuencia: 0,
|
|
149
|
+
prevHash: HASH_GENESIS,
|
|
150
|
+
timestamp: genesis.timestamp,
|
|
151
|
+
actor: genesis.actor,
|
|
152
|
+
nodo: genesis.nodo,
|
|
153
|
+
estado: genesis.estado,
|
|
154
|
+
evidencia: genesis.evidencia,
|
|
155
|
+
});
|
|
156
|
+
return { ...payload, hash: calcularHashEvento(payload) };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function resolverRutas(dir) {
|
|
160
|
+
if (typeof dir !== 'string' || dir.trim() === '') {
|
|
161
|
+
throw new TypeError('dir debe ser un string no vacío');
|
|
162
|
+
}
|
|
163
|
+
const directorio = path.resolve(dir);
|
|
164
|
+
return {
|
|
165
|
+
eventos: path.join(directorio, ARCHIVO_EVENTOS),
|
|
166
|
+
snapshot: path.join(directorio, ARCHIVO_SNAPSHOT),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function crearSnapshot(eventos) {
|
|
171
|
+
const ultimo = eventos[eventos.length - 1];
|
|
172
|
+
return [
|
|
173
|
+
'# Estado del loop C10',
|
|
174
|
+
'',
|
|
175
|
+
'> Vista derivada. La autoridad es `eventos.jsonl`.',
|
|
176
|
+
'',
|
|
177
|
+
`- Secuencia: \`${ultimo.secuencia}\``,
|
|
178
|
+
`- Último hash: \`${ultimo.hash}\``,
|
|
179
|
+
`- Eventos: \`${eventos.length}\``,
|
|
180
|
+
`- Tipo: \`${ultimo.tipo}\``,
|
|
181
|
+
`- Nodo: \`${ultimo.nodo}\``,
|
|
182
|
+
`- Estado: \`${ultimo.estado}\``,
|
|
183
|
+
`- Timestamp: \`${ultimo.timestamp}\``,
|
|
184
|
+
'',
|
|
185
|
+
].join('\n');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function escribirSnapshotGenesis(rutas, evento) {
|
|
189
|
+
try {
|
|
190
|
+
atomicWriteSync(rutas.snapshot, crearSnapshot([evento]), 'utf8');
|
|
191
|
+
} catch (causa) {
|
|
192
|
+
const error = new Error('snapshot no persistido; usar genesisHash para regenerarlo', {
|
|
193
|
+
cause: causa,
|
|
194
|
+
});
|
|
195
|
+
error.code = 'C10_SNAPSHOT_WRITE_FAILED';
|
|
196
|
+
error.estadoPersistido = true;
|
|
197
|
+
error.genesisHash = evento.hash;
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function inicializarEstado({ dir, genesis } = {}) {
|
|
203
|
+
const rutas = resolverRutas(dir);
|
|
204
|
+
// Génesis única por contrato: incluso un replay idéntico debe rechazarse.
|
|
205
|
+
if (fs.existsSync(rutas.eventos)) {
|
|
206
|
+
throw new Error('eventos.jsonl ya existe; la génesis es única');
|
|
207
|
+
}
|
|
208
|
+
validarGenesis(genesis);
|
|
209
|
+
const evento = crearEventoGenesis(genesis);
|
|
210
|
+
const errorEvento = validarEvento(evento, 0, HASH_GENESIS);
|
|
211
|
+
if (errorEvento) throw new Error(`evento génesis inválido: ${errorEvento}`);
|
|
212
|
+
atomicWriteSync(rutas.eventos, `${JSON.stringify(evento)}\n`, 'utf8');
|
|
213
|
+
escribirSnapshotGenesis(rutas, evento);
|
|
214
|
+
return evento.hash;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function resultadoInvalido(motivo, eventos, ultimoHash = HASH_GENESIS) {
|
|
218
|
+
return { ok: false, motivo, eventos, ultimoHash };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function normalizarAncla(opciones) {
|
|
222
|
+
const ancla = opciones && opciones.genesisHashEsperado;
|
|
223
|
+
if (typeof ancla !== 'string' || !HASH_PATTERN.test(ancla)) return null;
|
|
224
|
+
return ancla.toLowerCase();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function validarCamposEvento(evento, indice) {
|
|
228
|
+
if (!esObjetoPlano(evento)) return `evento ${indice} no es un objeto`;
|
|
229
|
+
const campos = Object.keys(evento).sort();
|
|
230
|
+
const esperados = [...CAMPOS_EVENTO].sort();
|
|
231
|
+
if (JSON.stringify(campos) !== JSON.stringify(esperados)) {
|
|
232
|
+
return `campos inválidos en evento ${indice}`;
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
validarCamposBase(evento);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
return `${error.message} en evento ${indice}`;
|
|
238
|
+
}
|
|
239
|
+
if (typeof evento.tipo !== 'string' || evento.tipo.trim() === '') {
|
|
240
|
+
return `tipo inválido en evento ${indice}`;
|
|
241
|
+
}
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function validarEvento(evento, indice, hashAnterior) {
|
|
246
|
+
const errorCampos = validarCamposEvento(evento, indice);
|
|
247
|
+
if (errorCampos) return errorCampos;
|
|
248
|
+
if (evento.secuencia !== indice) return `secuencia inválida en evento ${indice}`;
|
|
249
|
+
if (evento.prevHash !== hashAnterior) return `prevHash inválido en evento ${indice}`;
|
|
250
|
+
if (indice === 0 && evento.tipo !== 'genesis') return 'el evento 0 no es genesis';
|
|
251
|
+
if (indice === 0 && evento.estado !== ESTADO_GENESIS) return 'estado génesis inválido';
|
|
252
|
+
if (indice > 0 && evento.tipo === 'genesis') return `genesis duplicada en evento ${indice}`;
|
|
253
|
+
if (!HASH_PATTERN.test(evento.hash)) return `hash inválido en evento ${indice}`;
|
|
254
|
+
if (calcularHashEvento(evento) !== evento.hash) return `hash inválido en evento ${indice}`;
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function separarLineas(contenido) {
|
|
259
|
+
const lineas = contenido.split(/\r?\n/);
|
|
260
|
+
if (lineas.at(-1) === '') lineas.pop();
|
|
261
|
+
return lineas;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function verificarCadenaEventos(dir, genesisHashEsperado) {
|
|
265
|
+
const rutas = resolverRutas(dir);
|
|
266
|
+
if (!fs.existsSync(rutas.eventos)) {
|
|
267
|
+
return resultadoInvalido('eventos.jsonl no existe', 0);
|
|
268
|
+
}
|
|
269
|
+
const lineas = separarLineas(fs.readFileSync(rutas.eventos, 'utf8'));
|
|
270
|
+
if (lineas.length === 0) return resultadoInvalido('eventos.jsonl está vacío', 0);
|
|
271
|
+
return procesarLineas(lineas, genesisHashEsperado);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function procesarLineas(lineas, genesisHashEsperado) {
|
|
275
|
+
const eventos = [];
|
|
276
|
+
let hashAnterior = HASH_GENESIS;
|
|
277
|
+
for (let indice = 0; indice < lineas.length; indice += 1) {
|
|
278
|
+
const parseado = procesarLinea(lineas, indice, hashAnterior);
|
|
279
|
+
if (!parseado.ok) return parseado;
|
|
280
|
+
if (indice === 0 && parseado.evento.hash !== genesisHashEsperado) {
|
|
281
|
+
return resultadoInvalido('ancla genesis no coincide', lineas.length, hashAnterior);
|
|
282
|
+
}
|
|
283
|
+
eventos.push(parseado.evento);
|
|
284
|
+
hashAnterior = parseado.evento.hash;
|
|
285
|
+
}
|
|
286
|
+
return crearResultadoCadena(eventos, hashAnterior);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function procesarLinea(lineas, indice, hashAnterior) {
|
|
290
|
+
if (lineas[indice].trim() === '') {
|
|
291
|
+
return resultadoInvalido(`línea vacía en evento ${indice}`, lineas.length, hashAnterior);
|
|
292
|
+
}
|
|
293
|
+
return parsearLinea(lineas[indice], indice, lineas.length, hashAnterior);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function crearResultadoCadena(eventos, ultimoHash) {
|
|
297
|
+
return {
|
|
298
|
+
ok: true,
|
|
299
|
+
motivo: 'ok',
|
|
300
|
+
eventos: eventos.length,
|
|
301
|
+
ultimoHash,
|
|
302
|
+
lista: eventos,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function parsearLinea(linea, indice, total, hashAnterior) {
|
|
307
|
+
let evento;
|
|
308
|
+
try {
|
|
309
|
+
evento = JSON.parse(linea);
|
|
310
|
+
} catch {
|
|
311
|
+
return resultadoInvalido(`JSON inválido en evento ${indice}`, total, hashAnterior);
|
|
312
|
+
}
|
|
313
|
+
const motivo = validarEvento(evento, indice, hashAnterior);
|
|
314
|
+
if (motivo) return resultadoInvalido(motivo, total, hashAnterior);
|
|
315
|
+
return { ok: true, evento };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function verificarEstado(dir, opciones = {}) {
|
|
319
|
+
const ancla = normalizarAncla(opciones);
|
|
320
|
+
if (!ancla) return resultadoInvalido('genesisHashEsperado inválido', 0);
|
|
321
|
+
const cadena = verificarCadenaEventos(dir, ancla);
|
|
322
|
+
if (!cadena.ok) return cadena;
|
|
323
|
+
return verificarSnapshot(dir, cadena);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function verificarSnapshot(dir, cadena) {
|
|
327
|
+
const rutas = resolverRutas(dir);
|
|
328
|
+
if (!fs.existsSync(rutas.snapshot)) {
|
|
329
|
+
return resultadoInvalido('LOOP-STATE.md no existe', cadena.eventos, cadena.ultimoHash);
|
|
330
|
+
}
|
|
331
|
+
const esperado = crearSnapshot(cadena.lista);
|
|
332
|
+
const actual = fs.readFileSync(rutas.snapshot, 'utf8');
|
|
333
|
+
if (actual !== esperado) {
|
|
334
|
+
return resultadoInvalido('snapshot divergente', cadena.eventos, cadena.ultimoHash);
|
|
335
|
+
}
|
|
336
|
+
return { ok: true, motivo: 'ok', eventos: cadena.eventos, ultimoHash: cadena.ultimoHash };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function regenerarSnapshot(dir, opciones = {}) {
|
|
340
|
+
const ancla = normalizarAncla(opciones);
|
|
341
|
+
if (!ancla) throw new Error('genesisHashEsperado inválido');
|
|
342
|
+
const cadena = verificarCadenaEventos(dir, ancla);
|
|
343
|
+
if (!cadena.ok) throw new Error(`cadena inválida: ${cadena.motivo}`);
|
|
344
|
+
const contenido = crearSnapshot(cadena.lista);
|
|
345
|
+
atomicWriteSync(resolverRutas(dir).snapshot, contenido, 'utf8');
|
|
346
|
+
return contenido;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const CAMPOS_EVENTO_NUEVO = ['tipo', ...CAMPOS_GENESIS];
|
|
350
|
+
|
|
351
|
+
function normalizarPrevHash(opciones) {
|
|
352
|
+
const prevHash = opciones && opciones.prevHashEsperado;
|
|
353
|
+
if (typeof prevHash !== 'string' || !HASH_PATTERN.test(prevHash)) return null;
|
|
354
|
+
return prevHash.toLowerCase();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function validarCamposEventoNuevo(evento) {
|
|
358
|
+
if (!esObjetoPlano(evento)) throw new TypeError('evento debe ser un objeto');
|
|
359
|
+
const claves = Object.keys(evento);
|
|
360
|
+
const claveInesperada = claves.find((clave) => !CAMPOS_EVENTO_NUEVO.includes(clave));
|
|
361
|
+
if (claveInesperada) throw new TypeError(`clave inesperada: ${claveInesperada}`);
|
|
362
|
+
validarCamposBase(evento);
|
|
363
|
+
validarTextoRequerido(evento, 'tipo');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function verificarCadenaYSnapshotVigentes(dir, ancla, prevHashEsperado) {
|
|
367
|
+
const cadena = verificarCadenaEventos(dir, ancla);
|
|
368
|
+
if (!cadena.ok) throw new Error(`cadena inválida: ${cadena.motivo}`);
|
|
369
|
+
if (cadena.ultimoHash !== prevHashEsperado) {
|
|
370
|
+
throw new Error('prevHashEsperado no coincide con el tip actual; releer antes de reintentar');
|
|
371
|
+
}
|
|
372
|
+
const snapshot = verificarSnapshot(dir, cadena);
|
|
373
|
+
if (!snapshot.ok) throw new Error(`snapshot inválido: ${snapshot.motivo}`);
|
|
374
|
+
return cadena;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function construirEventoEncadenado(evento, cadena) {
|
|
378
|
+
const payload = construirPayloadCanonico({
|
|
379
|
+
tipo: evento.tipo,
|
|
380
|
+
schemaVersion: evento.schemaVersion,
|
|
381
|
+
secuencia: cadena.eventos,
|
|
382
|
+
prevHash: cadena.ultimoHash,
|
|
383
|
+
timestamp: evento.timestamp,
|
|
384
|
+
actor: evento.actor,
|
|
385
|
+
nodo: evento.nodo,
|
|
386
|
+
estado: evento.estado,
|
|
387
|
+
evidencia: evento.evidencia,
|
|
388
|
+
});
|
|
389
|
+
const eventoFinal = { ...payload, hash: calcularHashEvento(payload) };
|
|
390
|
+
const errorEvento = validarEvento(eventoFinal, cadena.eventos, cadena.ultimoHash);
|
|
391
|
+
if (errorEvento) throw new Error(`evento inválido: ${errorEvento}`);
|
|
392
|
+
return eventoFinal;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function persistirEventoAnexado(dir, eventoFinal, eventosPrevios) {
|
|
396
|
+
const rutas = resolverRutas(dir);
|
|
397
|
+
const contenidoActual = fs.readFileSync(rutas.eventos, 'utf8');
|
|
398
|
+
const nuevoContenido = `${contenidoActual}${JSON.stringify(eventoFinal)}\n`;
|
|
399
|
+
atomicWriteSync(rutas.eventos, nuevoContenido, 'utf8');
|
|
400
|
+
atomicWriteSync(rutas.snapshot, crearSnapshot([...eventosPrevios, eventoFinal]), 'utf8');
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function agregarEvento(dir, evento, opciones = {}) {
|
|
404
|
+
const ancla = normalizarAncla(opciones);
|
|
405
|
+
if (!ancla) throw new Error('genesisHashEsperado inválido');
|
|
406
|
+
const prevHashEsperado = normalizarPrevHash(opciones);
|
|
407
|
+
if (!prevHashEsperado) throw new Error('prevHashEsperado inválido');
|
|
408
|
+
validarCamposEventoNuevo(evento);
|
|
409
|
+
const cadena = verificarCadenaYSnapshotVigentes(dir, ancla, prevHashEsperado);
|
|
410
|
+
const eventoFinal = construirEventoEncadenado(evento, cadena);
|
|
411
|
+
persistirEventoAnexado(dir, eventoFinal, cadena.lista);
|
|
412
|
+
return eventoFinal.hash;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
module.exports = {
|
|
416
|
+
agregarEvento,
|
|
417
|
+
calcularHashEvento,
|
|
418
|
+
inicializarEstado,
|
|
419
|
+
regenerarSnapshot,
|
|
420
|
+
verificarEstado,
|
|
421
|
+
};
|