@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.
Files changed (268) hide show
  1. package/CLAUDE.md +14 -2
  2. package/README.md +65 -18
  3. package/agentes/_intent-spec.md +73 -73
  4. package/agentes/_propose-step.md +90 -90
  5. package/bin/swl-ses.js +10 -0
  6. package/comandos/swl/brainstorm.md +1 -0
  7. package/comandos/swl/briefing.md +119 -119
  8. package/comandos/swl/contribuir.md +233 -233
  9. package/comandos/swl/deuda-codigo.md +97 -97
  10. package/comandos/swl/mcp-status.md +1 -0
  11. package/gateway/lib/event-channel.js +191 -191
  12. package/habilidades/agent-deep-links/SKILL.md +148 -148
  13. package/habilidades/backend-async-postgres-testing/SKILL.md +216 -216
  14. package/habilidades/backend-error-design/SKILL.md +221 -221
  15. package/habilidades/backend-production-resilience/SKILL.md +288 -288
  16. package/habilidades/calidad-anti-patrones-universales/SKILL.md +105 -1
  17. package/habilidades/calidad-contract-testing/SKILL.md +165 -165
  18. package/habilidades/calidad-mutation-testing/SKILL.md +25 -1
  19. package/habilidades/checklist-seguridad/recursos/stride-cobertura.md +60 -60
  20. package/habilidades/ci-cd-pipelines/SKILL.md +5 -1
  21. package/habilidades/css-moderno/SKILL.md +7 -1
  22. package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
  23. package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
  24. package/habilidades/estructura-proyecto-claude/recursos/mcp-json-template.json +57 -57
  25. package/habilidades/extractor-de-aprendizajes/SKILL.md +5 -1
  26. package/habilidades/feynman-auditor-swl/recursos/preguntas-language-agnostic.md +108 -108
  27. package/habilidades/harness-claude-code/SKILL.md +3 -2
  28. package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
  29. package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
  30. package/habilidades/perfil-usuario/SKILL.md +200 -200
  31. package/habilidades/prevencion-sobreingenieria/recursos/EXAMPLES.md +580 -580
  32. package/habilidades/prevencion-sobreingenieria/recursos/soluciones-nativas.md +166 -166
  33. package/habilidades/prevencion-sobreingenieria/recursos/variables-residuales-post-refactor.md +85 -85
  34. package/habilidades/proceso-ddia-streaming/SKILL.md +231 -231
  35. package/habilidades/proceso-discovery-machote/SKILL.md +157 -157
  36. package/habilidades/proceso-dynamic-workflows/SKILL.md +60 -0
  37. package/habilidades/proceso-dynamic-workflows/recursos/template-adversarial-verify.js +65 -65
  38. package/habilidades/proceso-dynamic-workflows/recursos/template-triage.js +65 -65
  39. package/habilidades/proceso-intent-engineering/SKILL.md +269 -269
  40. package/habilidades/proceso-modular-split/SKILL.md +256 -256
  41. package/habilidades/state-inconsistency-auditor-swl/recursos/coupled-state-patterns.md +147 -147
  42. package/habilidades/swl-claudemd/recursos/contrato-aprender.md +83 -83
  43. package/habilidades/swl-claudemd/recursos/duplicacion-reglas-globales.md +85 -85
  44. package/habilidades/swl-claudemd/recursos/plantillas-init.md +94 -94
  45. package/habilidades/tdd-workflow/recursos/gherkin-bdd.md +111 -111
  46. package/hooks/calidad-pre-commit.js +159 -10
  47. package/hooks/ciclo-evolucion-subagente.js +26 -26
  48. package/hooks/ciclo-evolucion.js +26 -26
  49. package/hooks/contexto-subagente.js +68 -68
  50. package/hooks/lib/auto-consolidator.js +335 -335
  51. package/hooks/lib/ciclo-evolucion.js +47 -47
  52. package/hooks/lib/deep-links.js +185 -185
  53. package/hooks/lib/error-classifier.js +308 -308
  54. package/hooks/lib/notificacion-formato.js +45 -11
  55. package/hooks/lib/provenance-tracker.js +191 -191
  56. package/hooks/lib/raiz-proyecto.js +35 -4
  57. package/hooks/lib/resource-quota.js +122 -122
  58. package/hooks/lib/retry-jitter.js +165 -165
  59. package/hooks/lib/security-net.js +201 -201
  60. package/hooks/lib/skill-auditor.js +588 -588
  61. package/hooks/lib/sync-status.js +228 -228
  62. package/hooks/lib/taint-tracker.js +107 -107
  63. package/hooks/lib/text-similarity.js +241 -241
  64. package/hooks/lib/toon-compressor.js +245 -245
  65. package/hooks/notificacion-telegram.js +5 -11
  66. package/hooks/session-briefing.js +12 -4
  67. package/instintos/autonomia.yaml +27 -27
  68. package/instintos/prompt-appendices.yaml +57 -57
  69. package/llms.txt +1 -1
  70. package/manifiestos/agent-output-schemas.json +57 -57
  71. package/manifiestos/canonical-hashes.json +662 -0
  72. package/manifiestos/harness-ir.json +47536 -0
  73. package/manifiestos/hooks-config.json +469 -469
  74. package/manifiestos/invariantes-criticos.json +30 -30
  75. package/manifiestos/policy-bundle.json +2065 -0
  76. package/manifiestos/policy-corpus-w2.json +3926 -0
  77. package/manifiestos/runtime-adapters-core3.json +208 -0
  78. package/manifiestos/runtime-conformance.json +139 -0
  79. package/manifiestos/skills-lock.json +43 -43
  80. package/package.json +2 -2
  81. package/plantillas/auditor-veto-template.md +105 -105
  82. package/plantillas/github-workflows/release-please.yml +44 -44
  83. package/plantillas/github-workflows/swl-ci.yml +107 -107
  84. package/plantillas/github-workflows/swl-security.yml +51 -51
  85. package/plugin.json +2 -2
  86. package/reglas/accesibilidad.md +10 -10
  87. package/reglas/auditorias-documentales-estructurales.md +7 -7
  88. package/reglas/cloud-infra.md +8 -8
  89. package/reglas/consultar-vault-primero.md +195 -195
  90. package/reglas/git-workflow.md +1 -0
  91. package/reglas/hooks.md +6 -6
  92. package/reglas/intent-engineering.md +218 -218
  93. package/reglas/markitdown.md +8 -8
  94. package/reglas/monitor-ci.md +12 -0
  95. package/reglas/patrones.md +6 -6
  96. package/reglas/testing.md +7 -7
  97. package/reglas/tests-cleanup.md +224 -224
  98. package/schemas/agent-message.schema.json +73 -73
  99. package/schemas/agent-output-implementacion.schema.json +114 -114
  100. package/schemas/agent-output-planificacion.schema.json +150 -150
  101. package/schemas/agent-output-review.schema.json +98 -98
  102. package/schemas/diary-entry.schema.json +112 -112
  103. package/schemas/gate-state.schema.json +76 -0
  104. package/schemas/harness-ir.schema.json +369 -0
  105. package/schemas/hook-profiles.schema.json +54 -54
  106. package/schemas/hooks-config.schema.json +89 -89
  107. package/schemas/legacy-gates.schema.json +45 -0
  108. package/schemas/modulos.schema.json +38 -38
  109. package/schemas/perfiles.schema.json +36 -36
  110. package/schemas/plugin.schema.json +77 -77
  111. package/schemas/policy-bundle.schema.json +140 -0
  112. package/schemas/policy-enforcement.schema.json +117 -0
  113. package/schemas/policy-operation.schema.json +261 -0
  114. package/schemas/runtime-adapter.schema.json +176 -0
  115. package/schemas/runtime-build-attestation.schema.json +100 -0
  116. package/schemas/runtime-conformance.schema.json +239 -0
  117. package/schemas/runtime-diagnostic.schema.json +395 -0
  118. package/schemas/skill-evals.schema.json +119 -119
  119. package/schemas/skill-frontmatter.schema.json +245 -245
  120. package/schemas/w4-certification-request.schema.json +72 -0
  121. package/schemas/w4-certification-verdict.schema.json +224 -0
  122. package/schemas/w4-corpus.schema.json +172 -0
  123. package/schemas/w4-mutation-report.schema.json +116 -0
  124. package/schemas/w4-replay-result.schema.json +164 -0
  125. package/schemas/w4-scoring-report.schema.json +89 -0
  126. package/scripts/audit-tools/audit-history.js +330 -330
  127. package/scripts/audit-tools/bundle-tracker.js +290 -290
  128. package/scripts/audit-tools/canary-monitor.js +352 -352
  129. package/scripts/audit-tools/code-profiler.js +605 -605
  130. package/scripts/audit-tools/dep-doctor.js +320 -320
  131. package/scripts/audit-tools/env-validator.js +206 -206
  132. package/scripts/audit-tools/lib/fs-walk.js +48 -48
  133. package/scripts/audit-tools/lib/output.js +23 -23
  134. package/scripts/audit-tools/migration-checker.js +392 -392
  135. package/scripts/audit-tools/pentest-scanner.js +1436 -1436
  136. package/scripts/bootstrap-instintos.js +3 -0
  137. package/scripts/cli/aprobar-plan.js +73 -73
  138. package/scripts/cli/briefing.js +23 -23
  139. package/scripts/cli/ciclo-evolucion.js +26 -26
  140. package/scripts/cli/derivar-feature-list.js +25 -25
  141. package/scripts/cli/detectar-host.js +27 -27
  142. package/scripts/cli/diary-entry.js +69 -69
  143. package/scripts/cli/execution-state.js +18 -18
  144. package/scripts/cli/gateway-notify.js +41 -41
  145. package/scripts/cli/liberar-fase.js +42 -42
  146. package/scripts/cli/mark-evolved.js +56 -56
  147. package/scripts/cli/metricas-dora.js +26 -26
  148. package/scripts/cli/near-duplicate.js +55 -55
  149. package/scripts/cli/notificaciones.js +123 -123
  150. package/scripts/cli/propose-step.js +29 -29
  151. package/scripts/cli/schedule-parse.js +19 -19
  152. package/scripts/cli/sugerir-modelo.js +20 -20
  153. package/scripts/cli/verificar-plan.js +36 -36
  154. package/scripts/cli/verificar-trazabilidad.js +35 -35
  155. package/scripts/comandos/install-asistido.js +8 -7
  156. package/scripts/configurar-branch-protection.js +418 -418
  157. package/scripts/detectar-aprendizajes-duplicados.js +151 -151
  158. package/scripts/doctor.js +61 -36
  159. package/scripts/generar-checklists-consolidados.js +273 -273
  160. package/scripts/generar-claims-runtime.js +1342 -0
  161. package/scripts/generar-harness-ir.js +257 -0
  162. package/scripts/generar-inventario.js +52 -54
  163. package/scripts/generar-policy-bundle.js +202 -0
  164. package/scripts/instalador.js +26 -7
  165. package/scripts/lib/approval-receipts.js +190 -0
  166. package/scripts/lib/artefactos-python.js +43 -43
  167. package/scripts/lib/benchmark-metrics.js +160 -160
  168. package/scripts/lib/budget-enforcer.js +252 -252
  169. package/scripts/lib/certificacion-loop-state.js +421 -0
  170. package/scripts/lib/ci-reader.js +193 -193
  171. package/scripts/lib/ciclo-autonomo/yaml-instintos.js +56 -0
  172. package/scripts/lib/clasificar-directorio.js +92 -0
  173. package/scripts/lib/contadores-inventario.js +217 -217
  174. package/scripts/lib/detectar-host-swl.js +175 -175
  175. package/scripts/lib/detectar-runtime.js +29 -20
  176. package/scripts/lib/detectar-stack-detallado.js +307 -307
  177. package/scripts/lib/detector-autoduplicacion-intra-archivo.js +234 -234
  178. package/scripts/lib/detector-reglas-duplicadas.js +220 -220
  179. package/scripts/lib/eval-metrics-store.js +218 -218
  180. package/scripts/lib/eval-quality.js +171 -171
  181. package/scripts/lib/eval-schemas.js +144 -144
  182. package/scripts/lib/eval-self-correct.js +106 -106
  183. package/scripts/lib/eval-validator.js +185 -185
  184. package/scripts/lib/evidence-verifier.js +192 -0
  185. package/scripts/lib/evidencia-release.js +322 -322
  186. package/scripts/lib/frontmatter-canonico.js +509 -0
  187. package/scripts/lib/gate-engine.js +871 -0
  188. package/scripts/lib/gate-hooks-requires.js +249 -249
  189. package/scripts/lib/gate-licencias.js +212 -212
  190. package/scripts/lib/git-config-preflight.js +48 -0
  191. package/scripts/lib/git-metricas.js +257 -257
  192. package/scripts/lib/harness-ir.js +778 -0
  193. package/scripts/lib/harness-source-snapshot.js +309 -0
  194. package/scripts/lib/integrity-ledger.js +1147 -0
  195. package/scripts/lib/jaccard-similarity.js +98 -98
  196. package/scripts/lib/legacy-gate-migration.js +324 -0
  197. package/scripts/lib/limpiar-basura-global.js +45 -2
  198. package/scripts/lib/longmemeval-runner.js +125 -125
  199. package/scripts/lib/metricas-dora.js +204 -204
  200. package/scripts/lib/notificaciones-telegram.js +1 -0
  201. package/scripts/lib/npm-version.js +1 -0
  202. package/scripts/lib/paquetes-conocidos.js +50 -50
  203. package/scripts/lib/plan-lock.js +61 -13
  204. package/scripts/lib/policy-broker.js +338 -0
  205. package/scripts/lib/policy-bundle.js +342 -0
  206. package/scripts/lib/policy-context-provider.js +310 -0
  207. package/scripts/lib/policy-contract.js +479 -0
  208. package/scripts/lib/policy-verifier-utils.js +65 -0
  209. package/scripts/lib/pr-analyzer.js +399 -399
  210. package/scripts/lib/principal-verifier.js +178 -0
  211. package/scripts/lib/prompt-builder.js +264 -264
  212. package/scripts/lib/resolver-plan-fase.js +37 -37
  213. package/scripts/lib/rrf-fusion.js +175 -175
  214. package/scripts/lib/runtime-adapter-contract.js +267 -0
  215. package/scripts/lib/runtime-artifact-verifier.js +426 -0
  216. package/scripts/lib/runtime-build-attestation.js +127 -0
  217. package/scripts/lib/runtime-bundle-installer.js +586 -0
  218. package/scripts/lib/runtime-compiler.js +327 -0
  219. package/scripts/lib/runtime-conformance.js +202 -0
  220. package/scripts/lib/runtime-doctor-core3.js +567 -0
  221. package/scripts/lib/runtime-doctor-input.js +59 -0
  222. package/scripts/lib/runtime-operation-adapter.js +267 -0
  223. package/scripts/lib/schema-version.js +164 -164
  224. package/scripts/lib/semantic-search.js +252 -252
  225. package/scripts/lib/signed-envelope.js +545 -0
  226. package/scripts/lib/single-use-store.js +359 -0
  227. package/scripts/lib/skills-externas.js +31 -0
  228. package/scripts/lib/transformadores/codex.js +15 -8
  229. package/scripts/lib/transformadores/gemini.js +79 -5
  230. package/scripts/lib/w4-attestation-adapter.js +158 -0
  231. package/scripts/lib/w4-canario.js +337 -0
  232. package/scripts/lib/w4-claims.js +182 -0
  233. package/scripts/lib/w4-corpus-generador.js +542 -0
  234. package/scripts/lib/w4-gate-c5.js +115 -0
  235. package/scripts/lib/w4-harness-bajo-prueba.js +155 -0
  236. package/scripts/lib/w4-matriz-combos.js +55 -0
  237. package/scripts/lib/w4-motor-mutacion.js +1348 -0
  238. package/scripts/lib/w4-motor-replay.js +735 -0
  239. package/scripts/lib/w4-pin-origen.js +54 -0
  240. package/scripts/lib/w4-publicar-request.js +132 -0
  241. package/scripts/lib/w4-revocacion.js +62 -0
  242. package/scripts/lib/w4-runtimes-core3.js +38 -0
  243. package/scripts/lib/w4-scorer-certificacion.js +692 -0
  244. package/scripts/lib/w4-superficie-candidato.js +49 -0
  245. package/scripts/lib/w4-veredicto.js +452 -0
  246. package/scripts/lib/w4-verificar-veredicto.js +302 -0
  247. package/scripts/limpiar-artefactos-python.js +131 -131
  248. package/scripts/migrar-csv-a-array.js +168 -168
  249. package/scripts/migrar-fase-dominio.js +200 -200
  250. package/scripts/migrar-gates-legacy.js +108 -0
  251. package/scripts/publicar-certification-request.js +115 -0
  252. package/scripts/runtime-doctor.js +107 -0
  253. package/scripts/tui/componentes/selector-multi.js +189 -189
  254. package/scripts/tui/componentes/selector-unico.js +158 -158
  255. package/scripts/tui/ejecutores.js +375 -375
  256. package/scripts/tui/lib/colores.js +129 -129
  257. package/scripts/tui/lib/render.js +264 -264
  258. package/scripts/tui/lib/teclas.js +113 -113
  259. package/scripts/tui/pantallas/install-wizard.js +12 -7
  260. package/scripts/tui/pantallas/menu-principal.js +52 -52
  261. package/scripts/tui/pantallas/progreso.js +274 -274
  262. package/scripts/tui/pantallas/resumen.js +132 -132
  263. package/scripts/validar-userland-vacio.js +110 -110
  264. package/scripts/verificar-aislamiento-swl-eval.js +87 -0
  265. package/scripts/verificar-empaquetado-downstream.js +375 -0
  266. package/scripts/verificar-loop-constructor.js +215 -0
  267. package/scripts/verificar-trazabilidad.js +13 -6
  268. package/scripts/verificar-veredicto-real.js +84 -0
@@ -0,0 +1,871 @@
1
+ 'use strict';
2
+
3
+ /** Máquina de estados G0-G4 compuesta sobre el writer privado del kernel. */
4
+
5
+ const crypto = require('node:crypto');
6
+ const fs = require('node:fs');
7
+ const path = require('node:path');
8
+
9
+ const { assertEvidenceCurrent, isVerifiedEvidence } = require('./evidence-verifier');
10
+ const { assertPrincipalCurrent, isVerifiedPrincipal } = require('./principal-verifier');
11
+ const {
12
+ exactKeys, fail, freezeDeep, getContractValidators, plainObject,
13
+ } = require('./policy-verifier-utils');
14
+ const { canonicalizarIJson, validarTimestampIso } = require('./signed-envelope');
15
+
16
+ const STATES = new WeakMap();
17
+ const RECEIPTS = new WeakMap();
18
+ const SHA256 = /^[a-f0-9]{64}$/u;
19
+ const GIT_HASH = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u;
20
+ const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
21
+ const TASK = /^T-[0-9]{2}$/u;
22
+ const REQUIREMENT = /^REQ-[0-9]{2}-[0-9]{2}$/u;
23
+ const TASK_EVENTS = Object.freeze([
24
+ 'red_recorded', 'tdd_exempted', 'implementation_started', 'tests_passed',
25
+ ]);
26
+ const G4_EVENTS = Object.freeze(['trace_verified', 'phase_closed']);
27
+ const PHASE_STATES = Object.freeze([
28
+ 'sin_spec', 'spec_validada', 'plan_aprobado', 'red_evidenciado', 'tdd_exento',
29
+ 'implementacion_activa', 'tests_verdes', 'trazabilidad_verificada', 'cerrada',
30
+ ]);
31
+ const ROLES = Object.freeze([
32
+ 'evidence_verifier', 'executor', 'human_approver',
33
+ 'independent_verifier', 'spec_validator',
34
+ ]);
35
+ const CONFIG_KEYS = [
36
+ 'phaseId', 'planDigest', 'planVersion', 'planPath', 'contextDigest',
37
+ 'contextPath', 'coverageThreshold', 'taskIds', 'requirementsByTask', 'actorBindings',
38
+ ];
39
+ const TRANSITION_KEYS = [
40
+ 'event', 'expectedVersion', 'taskId', 'principal', 'evidence', 'grantClaimReceipt',
41
+ ];
42
+ const PAYLOAD_KEYS = [
43
+ 'schemaVersion', 'configurationDigest', 'event', 'expectedVersion', 'phaseId',
44
+ 'taskId', 'actorRole', 'principalDigest', 'evidenceDigests', 'grantDigest',
45
+ 'evidenceBaseHead', 'fromState', 'toState', 'planDigest', 'observedPlanDigest',
46
+ 'planVersion', 'approvalDigest', 'operationDigest',
47
+ 'g1Approval',
48
+ ];
49
+ const G1_KEYS = [
50
+ 'kind', 'approvalDigest', 'evidenceDigest', 'approverDigest', 'phaseId',
51
+ 'planDigest', 'planBindingDigest', 'issuedAt', 'expiresAt',
52
+ ];
53
+ const EVENT_ACTORS = Object.freeze({
54
+ spec_validated: 'spec_validator',
55
+ plan_approved: 'human_approver',
56
+ red_recorded: 'evidence_verifier',
57
+ tdd_exempted: 'human_approver',
58
+ implementation_started: 'executor',
59
+ tests_passed: 'evidence_verifier',
60
+ trace_verified: 'evidence_verifier',
61
+ phase_closed: 'independent_verifier',
62
+ plan_invalidated: 'gate_engine',
63
+ });
64
+
65
+ function cloneConfig(config) {
66
+ return freezeDeep(structuredClone(config));
67
+ }
68
+
69
+ function validateSortedTasks(taskIds) {
70
+ if (!Array.isArray(taskIds) || taskIds.length === 0 || taskIds.length > 99
71
+ || taskIds.some((taskId) => !TASK.test(taskId))
72
+ || taskIds.some((taskId, index) => index > 0 && taskIds[index - 1] >= taskId)) {
73
+ fail('GATE_CONFIG_INVALID', 'taskIds inválidos o no canónicos');
74
+ }
75
+ }
76
+
77
+ function validateActorBindings(bindings) {
78
+ plainObject(bindings, 'GATE_CONFIG_INVALID', 'actorBindings');
79
+ exactKeys(bindings, ROLES, 'GATE_CONFIG_INVALID', 'actorBindings');
80
+ if (Object.values(bindings).some((digest) => !SHA256.test(digest))) {
81
+ fail('GATE_CONFIG_INVALID', 'actorBindings inválidos');
82
+ }
83
+ }
84
+
85
+ function validateRequirementsByTask(requirementsByTask, taskIds) {
86
+ plainObject(requirementsByTask, 'GATE_CONFIG_INVALID', 'requirementsByTask');
87
+ exactKeys(requirementsByTask, taskIds, 'GATE_CONFIG_INVALID', 'requirementsByTask');
88
+ for (const taskId of taskIds) {
89
+ const requirements = requirementsByTask[taskId];
90
+ if (!Array.isArray(requirements) || requirements.length === 0
91
+ || requirements.some((requirement) => !REQUIREMENT.test(requirement))
92
+ || requirements.some((requirement, index) => (
93
+ index > 0 && requirements[index - 1] >= requirement
94
+ ))) {
95
+ fail('GATE_CONFIG_INVALID', `requisitos inválidos para ${taskId}`);
96
+ }
97
+ }
98
+ }
99
+
100
+ function resolveTrackedPath(workspaceRoot, relativePath, label) {
101
+ if (typeof relativePath !== 'string' || path.isAbsolute(relativePath)
102
+ || relativePath.includes('\0')) fail('GATE_CONFIG_INVALID', `${label} inválido`);
103
+ const root = fs.realpathSync(workspaceRoot);
104
+ const target = path.resolve(root, relativePath);
105
+ const relative = path.relative(root, target);
106
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
107
+ fail('GATE_CONFIG_INVALID', `${label} fuera del workspace`);
108
+ }
109
+ const realTarget = fs.realpathSync(target);
110
+ if (realTarget !== target || !fs.statSync(realTarget).isFile()) {
111
+ fail('GATE_CONFIG_INVALID', `${label} ambiguo`);
112
+ }
113
+ return realTarget;
114
+ }
115
+
116
+ function hashStableFile(filePath) {
117
+ const before = fs.statSync(filePath);
118
+ const bytes = fs.readFileSync(filePath);
119
+ const after = fs.statSync(filePath);
120
+ if (before.dev !== after.dev || before.ino !== after.ino
121
+ || before.size !== after.size || before.mtimeMs !== after.mtimeMs) {
122
+ fail('GATE_ARTIFACT_CHANGED', 'artefacto cambió durante lectura');
123
+ }
124
+ return crypto.createHash('sha256').update(bytes).digest('hex');
125
+ }
126
+
127
+ function validateConfig(rawConfig, workspaceRoot) {
128
+ plainObject(rawConfig, 'GATE_CONFIG_INVALID', 'configuración gate');
129
+ exactKeys(rawConfig, CONFIG_KEYS, 'GATE_CONFIG_INVALID', 'configuración gate');
130
+ if (!IDENTIFIER.test(rawConfig.phaseId) || !IDENTIFIER.test(rawConfig.planVersion)
131
+ || !SHA256.test(rawConfig.planDigest) || !SHA256.test(rawConfig.contextDigest)
132
+ || !Number.isInteger(rawConfig.coverageThreshold)
133
+ || rawConfig.coverageThreshold < 0 || rawConfig.coverageThreshold > 100) {
134
+ fail('GATE_CONFIG_INVALID', 'bindings de configuración inválidos');
135
+ }
136
+ validateSortedTasks(rawConfig.taskIds);
137
+ validateRequirementsByTask(rawConfig.requirementsByTask, rawConfig.taskIds);
138
+ validateActorBindings(rawConfig.actorBindings);
139
+ const config = cloneConfig(rawConfig);
140
+ return {
141
+ config,
142
+ contextPath: resolveTrackedPath(workspaceRoot, config.contextPath, 'contextPath'),
143
+ planPath: resolveTrackedPath(workspaceRoot, config.planPath, 'planPath'),
144
+ };
145
+ }
146
+
147
+ function calculateConfigurationDigest(config) {
148
+ const invariant = { ...config };
149
+ delete invariant.planDigest;
150
+ delete invariant.planVersion;
151
+ return crypto.createHash('sha256')
152
+ .update(canonicalizarIJson(invariant), 'utf8').digest('hex');
153
+ }
154
+
155
+ function calculateApprovalDigest(configurationDigest, phaseId, planDigest, planVersion) {
156
+ return crypto.createHash('sha256').update(canonicalizarIJson({
157
+ configurationDigest, phaseId, planDigest, planVersion,
158
+ }), 'utf8').digest('hex');
159
+ }
160
+
161
+ function createTaskStates(taskIds) {
162
+ return Object.fromEntries(taskIds.map((taskId) => [taskId, {
163
+ state: 'pendiente', g2EvidenceDigest: null, g3EvidenceDigest: null, baseHead: null,
164
+ }]));
165
+ }
166
+
167
+ function initialState(config, configurationDigest, revision = config) {
168
+ return {
169
+ kind: 'verified-gate-state', schemaVersion: '1.0.0',
170
+ phaseId: config.phaseId, configurationDigest,
171
+ planDigest: revision.planDigest, planVersion: revision.planVersion,
172
+ activeApprovalDigest: revision.approvalDigest || calculateApprovalDigest(
173
+ configurationDigest, config.phaseId, revision.planDigest, revision.planVersion
174
+ ),
175
+ activeG1: null,
176
+ phaseState: 'sin_spec', version: 0, taskStates: createTaskStates(config.taskIds),
177
+ planInvalidated: false, observedPlanDigest: null, lastEvent: null,
178
+ };
179
+ }
180
+
181
+ function requireTask(state, taskId, expectedState) {
182
+ const task = state.taskStates[taskId];
183
+ if (!task || (expectedState && task.state !== expectedState)) {
184
+ fail('GATE_TRANSITION_INVALID', 'tarea ausente o en estado inválido');
185
+ }
186
+ return task;
187
+ }
188
+
189
+ function applyTaskStart(state, taskId, target) {
190
+ if (!['plan_aprobado', 'implementacion_activa'].includes(state.phaseState)) {
191
+ fail('GATE_TRANSITION_INVALID', 'origen de G2 inválido');
192
+ }
193
+ requireTask(state, taskId, 'pendiente').state = target;
194
+ state.phaseState = target;
195
+ }
196
+
197
+ function applyImplementation(state, taskId) {
198
+ const task = requireTask(state, taskId);
199
+ if (!['red_evidenciado', 'tdd_exento'].includes(state.phaseState)
200
+ || task.state !== state.phaseState) {
201
+ fail('GATE_TRANSITION_INVALID', 'G2 no precede implementación');
202
+ }
203
+ task.state = 'implementacion_activa';
204
+ state.phaseState = 'implementacion_activa';
205
+ }
206
+
207
+ function applyTests(state, taskId) {
208
+ const task = requireTask(state, taskId, 'implementacion_activa');
209
+ if (state.phaseState !== 'implementacion_activa') {
210
+ fail('GATE_TRANSITION_INVALID', 'implementación no activa');
211
+ }
212
+ task.state = 'tests_verdes';
213
+ const allGreen = Object.values(state.taskStates).every(({ state: value }) => (
214
+ value === 'tests_verdes'
215
+ ));
216
+ state.phaseState = allGreen ? 'tests_verdes' : 'implementacion_activa';
217
+ }
218
+
219
+ function applyPhaseTransition(state, payload) {
220
+ const { event } = payload;
221
+ const matrix = {
222
+ spec_validated: ['sin_spec', 'spec_validada'],
223
+ plan_approved: ['spec_validada', 'plan_aprobado'],
224
+ trace_verified: ['tests_verdes', 'trazabilidad_verificada'],
225
+ phase_closed: ['trazabilidad_verificada', 'cerrada'],
226
+ };
227
+ const transition = matrix[event];
228
+ if (!transition || state.phaseState !== transition[0]) {
229
+ fail('GATE_TRANSITION_INVALID', 'transición de fase inválida');
230
+ }
231
+ state.phaseState = transition[1];
232
+ if (event === 'plan_approved') {
233
+ state.planDigest = payload.planDigest;
234
+ state.planVersion = payload.planVersion;
235
+ state.activeApprovalDigest = payload.approvalDigest;
236
+ state.activeG1 = structuredClone(payload.g1Approval);
237
+ state.planInvalidated = false;
238
+ state.observedPlanDigest = null;
239
+ }
240
+ }
241
+
242
+ function applyInvalidation(state, observedPlanDigest) {
243
+ if (['sin_spec', 'spec_validada'].includes(state.phaseState)) {
244
+ fail('GATE_TRANSITION_INVALID', 'plan no aprobado para invalidar');
245
+ }
246
+ state.phaseState = 'spec_validada';
247
+ state.taskStates = createTaskStates(Object.keys(state.taskStates));
248
+ state.planInvalidated = true;
249
+ state.observedPlanDigest = observedPlanDigest;
250
+ state.activeG1 = null;
251
+ }
252
+
253
+ function canPersistInvalidation(state) {
254
+ return !['sin_spec', 'spec_validada'].includes(state.phaseState);
255
+ }
256
+
257
+ function applyEvent(previous, payload) {
258
+ const state = structuredClone(previous);
259
+ if (payload.event === 'red_recorded') applyTaskStart(state, payload.taskId, 'red_evidenciado');
260
+ else if (payload.event === 'tdd_exempted') applyTaskStart(state, payload.taskId, 'tdd_exento');
261
+ else if (payload.event === 'implementation_started') applyImplementation(state, payload.taskId);
262
+ else if (payload.event === 'tests_passed') applyTests(state, payload.taskId);
263
+ else if (payload.event === 'plan_invalidated') {
264
+ applyInvalidation(state, payload.observedPlanDigest);
265
+ } else applyPhaseTransition(state, payload);
266
+ state.version += 1;
267
+ state.lastEvent = payload.event;
268
+ return state;
269
+ }
270
+
271
+ function validatePayloadShape(payload) {
272
+ plainObject(payload, 'GATE_STATE_INVALID', 'evento gate');
273
+ exactKeys(payload, PAYLOAD_KEYS, 'GATE_STATE_INVALID', 'evento gate');
274
+ if (payload.schemaVersion !== '1.0.0' || !Object.hasOwn(EVENT_ACTORS, payload.event)
275
+ || !Number.isInteger(payload.expectedVersion) || payload.expectedVersion < 0
276
+ || !IDENTIFIER.test(payload.phaseId) || payload.actorRole !== EVENT_ACTORS[payload.event]
277
+ || !PHASE_STATES.includes(payload.fromState) || !PHASE_STATES.includes(payload.toState)
278
+ || (payload.taskId !== null && !TASK.test(payload.taskId))
279
+ || !SHA256.test(payload.configurationDigest) || !SHA256.test(payload.planDigest)
280
+ || !IDENTIFIER.test(payload.planVersion) || !SHA256.test(payload.approvalDigest)
281
+ || (payload.operationDigest !== null && !SHA256.test(payload.operationDigest))
282
+ || !Array.isArray(payload.evidenceDigests)
283
+ || payload.evidenceDigests.some((digest) => !SHA256.test(digest))
284
+ || (payload.evidenceBaseHead !== null && !GIT_HASH.test(payload.evidenceBaseHead))) {
285
+ fail('GATE_STATE_INVALID', 'evento gate futuro o inválido');
286
+ }
287
+ validateG1ApprovalShape(payload.g1Approval);
288
+ }
289
+
290
+ function calculateG1ApprovalDigest(approval) {
291
+ const unsigned = { ...approval };
292
+ delete unsigned.approvalDigest;
293
+ return crypto.createHash('sha256')
294
+ .update(canonicalizarIJson(unsigned), 'utf8').digest('hex');
295
+ }
296
+
297
+ function validateG1ApprovalShape(approval) {
298
+ if (approval === null) return;
299
+ plainObject(approval, 'GATE_STATE_INVALID', 'approval G1');
300
+ exactKeys(approval, G1_KEYS, 'GATE_STATE_INVALID', 'approval G1');
301
+ validarTimestampIso(approval.issuedAt, 'G1 issuedAt');
302
+ validarTimestampIso(approval.expiresAt, 'G1 expiresAt');
303
+ if (approval.kind !== 'g1-approval-receipt'
304
+ || !SHA256.test(approval.approvalDigest)
305
+ || approval.approvalDigest !== calculateG1ApprovalDigest(approval)
306
+ || !SHA256.test(approval.evidenceDigest) || !SHA256.test(approval.approverDigest)
307
+ || !IDENTIFIER.test(approval.phaseId) || !SHA256.test(approval.planDigest)
308
+ || !SHA256.test(approval.planBindingDigest)
309
+ || approval.issuedAt >= approval.expiresAt) {
310
+ fail('GATE_STATE_INVALID', 'approval G1 durable inválida');
311
+ }
312
+ }
313
+
314
+ function validateDurableCardinality(payload, config) {
315
+ const expectedEvidence = payload.event === 'plan_invalidated'
316
+ ? 0 : (G4_EVENTS.includes(payload.event) ? config.taskIds.length : 1);
317
+ const taskExpected = TASK_EVENTS.includes(payload.event);
318
+ if (payload.evidenceDigests.length !== expectedEvidence
319
+ || taskExpected !== (payload.taskId !== null)
320
+ || (payload.taskId !== null && !config.taskIds.includes(payload.taskId))) {
321
+ fail('GATE_STATE_INVALID', 'cardinalidad durable de gate inválida');
322
+ }
323
+ }
324
+
325
+ function validateDurableAuthority(payload, config) {
326
+ const isInvalidation = payload.event === 'plan_invalidated';
327
+ const expectedPrincipal = isInvalidation ? null : config.actorBindings[payload.actorRole];
328
+ const expectedGrant = payload.event === 'implementation_started';
329
+ if (payload.principalDigest !== expectedPrincipal
330
+ || expectedGrant !== (payload.grantDigest !== null)
331
+ || expectedGrant !== (payload.operationDigest !== null)
332
+ || (payload.grantDigest !== null && !SHA256.test(payload.grantDigest))
333
+ || isInvalidation !== (payload.observedPlanDigest !== null)
334
+ || (payload.observedPlanDigest !== null && !SHA256.test(payload.observedPlanDigest))) {
335
+ fail('GATE_STATE_INVALID', 'autoridad durable de gate inválida');
336
+ }
337
+ const expectsG1 = payload.event === 'plan_approved';
338
+ if (expectsG1 !== (payload.g1Approval !== null)) {
339
+ fail('GATE_STATE_INVALID', 'receipt G1 durable ausente o inesperado');
340
+ }
341
+ if (expectsG1 && (payload.g1Approval.evidenceDigest !== payload.evidenceDigests[0]
342
+ || payload.g1Approval.approverDigest !== payload.principalDigest
343
+ || payload.g1Approval.phaseId !== payload.phaseId
344
+ || payload.g1Approval.planDigest !== payload.planDigest
345
+ || payload.g1Approval.planBindingDigest !== payload.approvalDigest)) {
346
+ fail('GATE_STATE_INVALID', 'receipt G1 durable no coincide con transición');
347
+ }
348
+ }
349
+
350
+ function validateDurableTaskEvidence(payload, state) {
351
+ const task = payload.taskId === null ? null : state.taskStates[payload.taskId];
352
+ const expectsBase = TASK_EVENTS.includes(payload.event);
353
+ if (expectsBase !== (payload.evidenceBaseHead !== null)) {
354
+ fail('GATE_STATE_INVALID', 'base durable de gate inválida');
355
+ }
356
+ if (['implementation_started', 'tests_passed'].includes(payload.event)
357
+ && (!task || payload.evidenceBaseHead !== task.baseHead)) {
358
+ fail('GATE_STATE_INVALID', 'base durable no conserva G2');
359
+ }
360
+ if (payload.event === 'implementation_started'
361
+ && payload.evidenceDigests[0] !== task.g2EvidenceDigest) {
362
+ fail('GATE_STATE_INVALID', 'evidencia durable no conserva G2');
363
+ }
364
+ }
365
+
366
+ function validateDurableSemantics(payload, state, config) {
367
+ validateDurableCardinality(payload, config);
368
+ validateDurableAuthority(payload, config);
369
+ validateDurableTaskEvidence(payload, state);
370
+ }
371
+
372
+ function validateDurableGrant(payload, ledgerState, state) {
373
+ if (payload.event !== 'implementation_started') return;
374
+ const claims = ledgerState.events.filter(({ eventType, payload: claim }) => (
375
+ eventType === 'grant.claimed' && claim.grantDigest === payload.grantDigest
376
+ && claim.principalDigest === payload.principalDigest
377
+ && claim.operationDigest === payload.operationDigest
378
+ && claim.planDigest === payload.planDigest
379
+ && claim.g1ApprovalDigest === state.activeG1?.approvalDigest
380
+ && claim.phaseId === payload.phaseId && claim.taskId === payload.taskId
381
+ ));
382
+ const consumers = ledgerState.events.filter(({ eventType, payload: event }) => (
383
+ eventType === 'gate.transition' && event.event === 'implementation_started'
384
+ && event.grantDigest === payload.grantDigest
385
+ ));
386
+ if (claims.length !== 1 || consumers.length !== 1) {
387
+ fail('GATE_STATE_INVALID', 'grant durable ausente o reutilizada');
388
+ }
389
+ }
390
+
391
+ function applyDurableEvidence(state, payload) {
392
+ if (!payload.taskId) return;
393
+ const task = state.taskStates[payload.taskId];
394
+ if (['red_recorded', 'tdd_exempted'].includes(payload.event)) {
395
+ task.g2EvidenceDigest = payload.evidenceDigests[0];
396
+ task.baseHead = payload.evidenceBaseHead;
397
+ }
398
+ if (payload.event === 'tests_passed') task.g3EvidenceDigest = payload.evidenceDigests[0];
399
+ }
400
+
401
+ function validateReplayBindings(payload, state, config, configurationDigest) {
402
+ const revisionMayChange = payload.event === 'plan_approved' && state.planInvalidated;
403
+ const expectedApprovalDigest = calculateApprovalDigest(
404
+ configurationDigest, config.phaseId, payload.planDigest, payload.planVersion
405
+ );
406
+ if (payload.configurationDigest !== configurationDigest || payload.phaseId !== config.phaseId
407
+ || (!revisionMayChange && (payload.planDigest !== state.planDigest
408
+ || payload.planVersion !== state.planVersion))
409
+ || payload.approvalDigest !== expectedApprovalDigest
410
+ || payload.expectedVersion !== state.version
411
+ || payload.actorRole !== EVENT_ACTORS[payload.event]
412
+ || payload.fromState !== state.phaseState) {
413
+ fail('GATE_STATE_INVALID', 'binding durable de gate inválido');
414
+ }
415
+ validateDurableSemantics(payload, state, config);
416
+ }
417
+
418
+ function replayState(ledgerState, config, configurationDigest) {
419
+ const events = ledgerState.events.filter(({ eventType }) => eventType === 'gate.transition');
420
+ const phaseEvents = [];
421
+ for (const { payload } of events) {
422
+ validatePayloadShape(payload);
423
+ if (payload.phaseId === config.phaseId) phaseEvents.push(payload);
424
+ }
425
+ const initialRevision = phaseEvents[0] || config;
426
+ let state = initialState(config, configurationDigest, initialRevision);
427
+ for (const payload of phaseEvents) {
428
+ validateReplayBindings(payload, state, config, configurationDigest);
429
+ validateDurableGrant(payload, ledgerState, state);
430
+ const next = applyEvent(state, payload);
431
+ applyDurableEvidence(next, payload);
432
+ if (payload.toState !== next.phaseState) {
433
+ fail('GATE_STATE_INVALID', 'destino durable de gate inválido');
434
+ }
435
+ state = next;
436
+ }
437
+ const matchesCurrent = state.planDigest === config.planDigest
438
+ && state.planVersion === config.planVersion;
439
+ const matchesObservedRevision = state.planInvalidated
440
+ && state.observedPlanDigest === config.planDigest;
441
+ if (!matchesCurrent && !matchesObservedRevision) {
442
+ fail('GATE_STATE_INVALID', 'revisión de PLAN incompatible con proyección');
443
+ }
444
+ return state;
445
+ }
446
+
447
+ function asEvidenceArray(evidence) {
448
+ return Array.isArray(evidence) ? evidence : [evidence];
449
+ }
450
+
451
+ function assertEvidenceBindings(evidence, event, taskId, principal, config, authority) {
452
+ if (!isVerifiedEvidence(evidence)) fail('GATE_EVIDENCE_INVALID', 'evidence no verificada');
453
+ assertEvidenceCurrent(evidence, authority);
454
+ const expectedKind = {
455
+ spec_validated: 'g0', plan_approved: 'g1', red_recorded: 'g2',
456
+ tdd_exempted: 'g2', implementation_started: 'g2', tests_passed: 'g3',
457
+ trace_verified: 'g4', phase_closed: 'g4',
458
+ }[event];
459
+ const expectedTask = taskId || config.phaseId;
460
+ const principalMismatch = event !== 'implementation_started'
461
+ && evidence.principalDigest !== principal.principalDigest;
462
+ if (evidence.evidenceKind !== expectedKind || principalMismatch
463
+ || evidence.phaseId !== config.phaseId || evidence.planDigest !== config.planDigest
464
+ || evidence.taskId !== expectedTask) {
465
+ fail('GATE_EVIDENCE_INVALID', 'evidence ligada a otra transición');
466
+ }
467
+ }
468
+
469
+ function assertEventEvidence(evidence, event, config, configurationDigest, approvalDigest) {
470
+ const expectedG0 = [config.contextDigest, configurationDigest].sort();
471
+ const expectedG1 = [config.planDigest, approvalDigest].sort();
472
+ if (event === 'spec_validated'
473
+ && JSON.stringify(evidence.sourceHashes) !== JSON.stringify(expectedG0)) {
474
+ fail('GATE_EVIDENCE_INVALID', 'G0 no liga contexto y configuración');
475
+ }
476
+ if (event === 'plan_approved'
477
+ && JSON.stringify(evidence.sourceHashes) !== JSON.stringify(expectedG1)) {
478
+ fail('GATE_EVIDENCE_INVALID', 'G1 no liga PLAN, versión y alcance');
479
+ }
480
+ if (event === 'red_recorded' && evidence.exitCode === 0) {
481
+ fail('GATE_EVIDENCE_INVALID', 'G2 RED requiere fallo causal');
482
+ }
483
+ const exemptionClaims = [
484
+ 'tdd-exemption', `scope:${evidence.taskId}`,
485
+ ];
486
+ const hasReason = evidence.testIds.filter((value) => /^reason:[A-Za-z0-9._:-]+$/u.test(value));
487
+ const hasRisk = evidence.testIds.filter((value) => /^risk:(?:low|medium|high)$/u.test(value));
488
+ const exemptionValid = exemptionClaims.every((claim) => evidence.testIds.includes(claim))
489
+ && hasReason.length === 1 && hasRisk.length === 1 && evidence.testIds.length === 4;
490
+ if (event === 'tdd_exempted' && (evidence.exitCode !== 0 || !exemptionValid)) {
491
+ fail('GATE_EVIDENCE_INVALID', 'exención TDD inválida');
492
+ }
493
+ }
494
+
495
+ function assertGreenEvidence(evidence, state, taskId, config) {
496
+ const task = requireTask(state, taskId, 'implementacion_activa');
497
+ if (evidence.exitCode !== 0 || evidence.baseHead !== task.baseHead
498
+ || Object.values(evidence.coverage).some((value) => value < config.coverageThreshold)) {
499
+ fail('GATE_EVIDENCE_INVALID', 'G3 no demuestra tests y cobertura');
500
+ }
501
+ }
502
+
503
+ function assertG4Evidence(evidences, config) {
504
+ const actualTasks = evidences.map(({ taskId }) => taskId).sort();
505
+ if (JSON.stringify(actualTasks) !== JSON.stringify(config.taskIds)) {
506
+ fail('GATE_EVIDENCE_INVALID', 'G4 no cubre tareas exactas');
507
+ }
508
+ for (const evidence of evidences) {
509
+ const actualRequirements = evidence.traceability
510
+ .filter(({ taskId }) => taskId === evidence.taskId)
511
+ .map(({ requirementId }) => requirementId).sort();
512
+ const expectedRequirements = config.requirementsByTask[evidence.taskId];
513
+ if (evidence.exitCode !== 0 || evidence.traceability.length !== expectedRequirements.length
514
+ || JSON.stringify(actualRequirements) !== JSON.stringify(expectedRequirements)) {
515
+ fail('GATE_EVIDENCE_INVALID', 'G4 sin trazabilidad exacta REQ×T');
516
+ }
517
+ }
518
+ }
519
+
520
+ function validateEvidenceSet(options, state, config, configurationDigest, authority) {
521
+ const evidences = asEvidenceArray(options.evidence);
522
+ const expectedCount = G4_EVENTS.includes(options.event) ? config.taskIds.length : 1;
523
+ if (evidences.length !== expectedCount) {
524
+ fail('GATE_EVIDENCE_INVALID', 'cardinalidad de evidence inválida');
525
+ }
526
+ for (const evidence of evidences) {
527
+ assertEvidenceBindings(
528
+ evidence, options.event,
529
+ G4_EVENTS.includes(options.event) ? evidence.taskId : options.taskId,
530
+ options.principal, config, authority
531
+ );
532
+ }
533
+ if (G4_EVENTS.includes(options.event)) assertG4Evidence(evidences, config);
534
+ else assertEventEvidence(
535
+ evidences[0], options.event, config, configurationDigest,
536
+ calculateApprovalDigest(
537
+ configurationDigest, config.phaseId, config.planDigest, config.planVersion
538
+ )
539
+ );
540
+ if (options.event === 'tests_passed') {
541
+ assertGreenEvidence(evidences[0], state, options.taskId, config);
542
+ }
543
+ return evidences;
544
+ }
545
+
546
+ function validateTransitionOptions(options) {
547
+ plainObject(options, 'GATE_OPTIONS_INVALID', 'opciones transición');
548
+ exactKeys(options, TRANSITION_KEYS, 'GATE_OPTIONS_INVALID', 'opciones transición');
549
+ if (!Object.hasOwn(EVENT_ACTORS, options.event) || options.event === 'plan_invalidated'
550
+ || !Number.isInteger(options.expectedVersion) || options.expectedVersion < 0
551
+ || (options.taskId !== null && !TASK.test(options.taskId))) {
552
+ fail('GATE_OPTIONS_INVALID', 'transición desconocida');
553
+ }
554
+ if (!isVerifiedPrincipal(options.principal)) {
555
+ fail('GATE_PRINCIPAL_INVALID', 'actor no verificado');
556
+ }
557
+ }
558
+
559
+ function validateActor(options, config, authority) {
560
+ assertPrincipalCurrent(options.principal, authority);
561
+ const role = EVENT_ACTORS[options.event];
562
+ if (config.actorBindings[role] !== options.principal.principalDigest) {
563
+ fail('GATE_ACTOR_INVALID', 'actor no autorizado para transición');
564
+ }
565
+ return role;
566
+ }
567
+
568
+ function validateActiveG1(state, authority, context, event) {
569
+ if (['spec_validated', 'plan_approved'].includes(event)) return;
570
+ const approval = state.activeG1;
571
+ if (!approval || approval.phaseId !== context.config.phaseId
572
+ || approval.planDigest !== state.planDigest
573
+ || approval.planBindingDigest !== state.activeApprovalDigest
574
+ || approval.approverDigest !== context.config.actorBindings.human_approver
575
+ || approval.issuedAt > authority.observedAt
576
+ || approval.expiresAt < authority.observedAt) {
577
+ fail('G1_EXPIRED', 'G1 ausente, fuera de alcance o expirado');
578
+ }
579
+ }
580
+
581
+ function validateTaskPresence(event, taskId) {
582
+ if (TASK_EVENTS.includes(event) !== (taskId !== null)) {
583
+ fail('GATE_OPTIONS_INVALID', 'taskId incompatible con transición');
584
+ }
585
+ }
586
+
587
+ function validateGrant(ledgerState, state, options, singleUseStore, context) {
588
+ if (options.event !== 'implementation_started') {
589
+ if (options.grantClaimReceipt !== null) fail('GATE_OPTIONS_INVALID', 'grant inesperada');
590
+ return null;
591
+ }
592
+ const receipt = options.grantClaimReceipt;
593
+ if (!singleUseStore.isGrantClaimReceipt(receipt)) {
594
+ fail('GATE_GRANT_INVALID', 'grant claim no verificada');
595
+ }
596
+ if (receipt.phaseId !== context.config.phaseId || receipt.taskId !== options.taskId) {
597
+ fail('GATE_GRANT_INVALID', 'grant claim ligada a otra fase o tarea');
598
+ }
599
+ const priorConsumers = ledgerState.events.filter(({ eventType, payload }) => (
600
+ eventType === 'gate.transition' && payload.event === 'implementation_started'
601
+ && payload.grantDigest === receipt.grantDigest
602
+ ));
603
+ if (priorConsumers.length !== 0) {
604
+ fail('GATE_GRANT_INVALID', 'grant ya consumida por otra transición');
605
+ }
606
+ if (receipt.g1ApprovalDigest !== state.activeG1?.approvalDigest) {
607
+ fail('GATE_GRANT_INVALID', 'grant claim ligada a otra época G1');
608
+ }
609
+ const matches = ledgerState.events.filter(({ eventType, sequence, payload }) => (
610
+ eventType === 'grant.claimed' && sequence === receipt.sequence
611
+ && payload.grantDigest === receipt.grantDigest
612
+ && payload.principalDigest === options.principal.principalDigest
613
+ && payload.operationDigest === options.principal.operationDigest
614
+ && payload.planDigest === context.config.planDigest
615
+ && payload.g1ApprovalDigest === state.activeG1.approvalDigest
616
+ && payload.phaseId === context.config.phaseId && payload.taskId === options.taskId
617
+ ));
618
+ if (matches.length !== 1) fail('GATE_GRANT_INVALID', 'grant claim no durable');
619
+ return receipt.grantDigest;
620
+ }
621
+
622
+ function enrichTaskEvidence(next, options, evidences) {
623
+ if (!options.taskId) return;
624
+ const task = next.taskStates[options.taskId];
625
+ if (['red_recorded', 'tdd_exempted'].includes(options.event)) {
626
+ task.g2EvidenceDigest = evidences[0].evidenceDigest;
627
+ task.baseHead = evidences[0].baseHead;
628
+ }
629
+ if (options.event === 'implementation_started'
630
+ && task.g2EvidenceDigest !== evidences[0].evidenceDigest) {
631
+ fail('GATE_EVIDENCE_INVALID', 'implementación no reutiliza G2 vigente');
632
+ }
633
+ if (options.event === 'tests_passed') task.g3EvidenceDigest = evidences[0].evidenceDigest;
634
+ }
635
+
636
+ function createG1Approval(evidence, context) {
637
+ const base = {
638
+ kind: 'g1-approval-receipt', evidenceDigest: evidence.evidenceDigest,
639
+ approverDigest: evidence.principalDigest, phaseId: context.config.phaseId,
640
+ planDigest: context.config.planDigest, planBindingDigest: context.approvalDigest,
641
+ issuedAt: evidence.timestamp, expiresAt: evidence.expiresAt,
642
+ };
643
+ return {
644
+ ...base, approvalDigest: crypto.createHash('sha256')
645
+ .update(canonicalizarIJson(base), 'utf8').digest('hex'),
646
+ };
647
+ }
648
+
649
+ function createPayload(state, next, options, role, evidences, grantDigest, context) {
650
+ return {
651
+ schemaVersion: '1.0.0', configurationDigest: context.configurationDigest,
652
+ event: options.event, expectedVersion: state.version, phaseId: context.config.phaseId,
653
+ taskId: options.taskId, actorRole: role,
654
+ principalDigest: options.principal.principalDigest,
655
+ evidenceDigests: evidences.map(({ evidenceDigest }) => evidenceDigest),
656
+ grantDigest, evidenceBaseHead: options.taskId ? evidences[0].baseHead : null,
657
+ fromState: state.phaseState, toState: next.phaseState,
658
+ planDigest: context.config.planDigest, observedPlanDigest: null,
659
+ planVersion: context.config.planVersion, approvalDigest: context.approvalDigest,
660
+ operationDigest: options.event === 'implementation_started'
661
+ ? options.principal.operationDigest : null,
662
+ g1Approval: options.event === 'plan_approved'
663
+ ? createG1Approval(evidences[0], context) : null,
664
+ };
665
+ }
666
+
667
+ function createInvalidationPayload(state, context, observedPlanDigest) {
668
+ const options = { event: 'plan_invalidated', taskId: null };
669
+ const next = applyEvent(state, { ...options, observedPlanDigest });
670
+ return {
671
+ schemaVersion: '1.0.0', configurationDigest: context.configurationDigest,
672
+ event: 'plan_invalidated', expectedVersion: state.version,
673
+ phaseId: context.config.phaseId, taskId: null, actorRole: 'gate_engine',
674
+ principalDigest: null, evidenceDigests: [], grantDigest: null,
675
+ evidenceBaseHead: null,
676
+ fromState: state.phaseState, toState: next.phaseState,
677
+ planDigest: state.planDigest, observedPlanDigest,
678
+ planVersion: state.planVersion, approvalDigest: state.activeApprovalDigest,
679
+ operationDigest: null,
680
+ g1Approval: null,
681
+ };
682
+ }
683
+
684
+ function appendEntry(ledgerState, payload) {
685
+ return {
686
+ expectedSequence: ledgerState.sequence, expectedHeadHash: ledgerState.headHash,
687
+ eventType: 'gate.transition', payload,
688
+ };
689
+ }
690
+
691
+ function createBrandedState(state, owner) {
692
+ const branded = freezeDeep(structuredClone(state));
693
+ STATES.set(branded, owner);
694
+ return branded;
695
+ }
696
+
697
+ function createReceipt(payload, appendReceipt, owner) {
698
+ const receipt = freezeDeep({
699
+ kind: 'gate-transition-receipt', phaseId: payload.phaseId, event: payload.event,
700
+ taskId: payload.taskId, version: payload.expectedVersion + 1,
701
+ phaseState: payload.toState, evidenceDigests: payload.evidenceDigests,
702
+ sequence: appendReceipt.sequence, headHash: appendReceipt.headHash,
703
+ });
704
+ RECEIPTS.set(receipt, owner);
705
+ return receipt;
706
+ }
707
+
708
+ function buildTransitionAction(options, context, singleUseStore) {
709
+ let payload;
710
+ let invalidated = false;
711
+ const action = (ledgerState, authority) => {
712
+ const state = replayState(ledgerState, context.config, context.configurationDigest);
713
+ const observedPlanDigest = hashStableFile(context.planPath);
714
+ if (observedPlanDigest !== context.config.planDigest) {
715
+ if (state.planInvalidated || !canPersistInvalidation(state)) {
716
+ fail('PLAN_DRIFT', 'PLAN cambió sin aprobación vigente');
717
+ }
718
+ payload = createInvalidationPayload(state, context, observedPlanDigest);
719
+ invalidated = true;
720
+ return appendEntry(ledgerState, payload);
721
+ }
722
+ if (options.expectedVersion !== state.version) fail('GATE_CAS_CONFLICT', 'versión cambió');
723
+ const role = validateActor(options, context.config, authority);
724
+ validateActiveG1(state, authority, context, options.event);
725
+ const evidences = validateEvidenceSet(
726
+ options, state, context.config, context.configurationDigest, authority
727
+ );
728
+ const grantDigest = validateGrant(
729
+ ledgerState, state, options, singleUseStore, context
730
+ );
731
+ const next = applyEvent(state, {
732
+ event: options.event, taskId: options.taskId,
733
+ planDigest: context.config.planDigest, planVersion: context.config.planVersion,
734
+ approvalDigest: context.approvalDigest,
735
+ });
736
+ enrichTaskEvidence(next, options, evidences);
737
+ payload = createPayload(state, next, options, role, evidences, grantDigest, context);
738
+ return appendEntry(ledgerState, payload);
739
+ };
740
+ return { action, getPayload: () => payload, wasInvalidated: () => invalidated };
741
+ }
742
+
743
+ function transition(writer, singleUseStore, owner, context, options) {
744
+ validateTransitionOptions(options);
745
+ validateTaskPresence(options.event, options.taskId);
746
+ const built = buildTransitionAction(options, context, singleUseStore);
747
+ const appendReceipt = writer.transact(built.action);
748
+ if (built.wasInvalidated()) fail('PLAN_DRIFT', 'PLAN cambió; estado invalidado');
749
+ return createReceipt(built.getPayload(), appendReceipt, owner);
750
+ }
751
+
752
+ function persistInvalidation(writer, context) {
753
+ writer.transact((ledgerState) => {
754
+ const state = replayState(ledgerState, context.config, context.configurationDigest);
755
+ const currentDigest = hashStableFile(context.planPath);
756
+ if (currentDigest === context.config.planDigest) {
757
+ fail('GATE_ARTIFACT_CHANGED', 'PLAN cambió durante invalidación');
758
+ }
759
+ const payload = createInvalidationPayload(state, context, currentDigest);
760
+ return appendEntry(ledgerState, payload);
761
+ });
762
+ }
763
+
764
+ function readVerifiedState(writer, owner, context) {
765
+ const inspected = writer.inspect((ledgerState) => ({
766
+ state: replayState(ledgerState, context.config, context.configurationDigest),
767
+ observedPlanDigest: hashStableFile(context.planPath),
768
+ }));
769
+ if (inspected.observedPlanDigest === context.config.planDigest) {
770
+ return createBrandedState(inspected.state, owner);
771
+ }
772
+ if (inspected.state.planInvalidated) {
773
+ if (inspected.observedPlanDigest !== inspected.state.observedPlanDigest) {
774
+ fail('PLAN_DRIFT', 'PLAN volvió a cambiar después de invalidarse');
775
+ }
776
+ return createBrandedState(inspected.state, owner);
777
+ }
778
+ if (!canPersistInvalidation(inspected.state)) {
779
+ fail('PLAN_DRIFT', 'PLAN cambió antes de su aprobación');
780
+ }
781
+ persistInvalidation(writer, context);
782
+ return writer.inspect((ledgerState) => createBrandedState(
783
+ replayState(ledgerState, context.config, context.configurationDigest), owner
784
+ ));
785
+ }
786
+
787
+ function createPhaseEngine(writer, singleUseStore, owner, rawConfig, workspaceRoot) {
788
+ const paths = validateConfig(rawConfig, workspaceRoot);
789
+ if (hashStableFile(paths.planPath) !== paths.config.planDigest
790
+ || hashStableFile(paths.contextPath) !== paths.config.contextDigest) {
791
+ fail('GATE_CONFIG_INVALID', 'artefactos no coinciden con configuración');
792
+ }
793
+ const context = Object.freeze({
794
+ ...paths, configurationDigest: calculateConfigurationDigest(paths.config),
795
+ });
796
+ const approvalDigest = calculateApprovalDigest(
797
+ context.configurationDigest, context.config.phaseId,
798
+ context.config.planDigest, context.config.planVersion
799
+ );
800
+ const boundContext = Object.freeze({ ...context, approvalDigest });
801
+ const publicPhase = Object.freeze({
802
+ approvalDigest, configurationDigest: context.configurationDigest,
803
+ readVerifiedState: () => readVerifiedState(writer, owner, boundContext),
804
+ transition: (options) => transition(writer, singleUseStore, owner, boundContext, options),
805
+ });
806
+ return Object.freeze({ boundContext, publicPhase });
807
+ }
808
+
809
+ function readAuthorizationGate(phases, ledgerState, authority, operation) {
810
+ if (!getContractValidators().isVerifiedOperation(operation)
811
+ || operation.schemaVersion !== '1.1.0') {
812
+ fail('GATE_OPERATION_INVALID', 'operación de gate inválida');
813
+ }
814
+ const context = phases.get(operation.phaseId);
815
+ if (!context) fail('GATE_PHASE_UNKNOWN', 'fase no registrada');
816
+ if (hashStableFile(context.planPath) !== context.config.planDigest) {
817
+ fail('PLAN_DRIFT', 'PLAN cambió durante autorización');
818
+ }
819
+ const state = replayState(ledgerState, context.config, context.configurationDigest);
820
+ if (state.planInvalidated || state.version !== operation.fsmVersion) {
821
+ fail('GATE_VERSION_MISMATCH', 'estado gate stale o invalidado');
822
+ }
823
+ const task = state.taskStates[operation.taskId];
824
+ if (!task) fail('GATE_TASK_UNKNOWN', 'tarea no registrada');
825
+ const esLectura = ['filesystem.read', 'memory.read'].includes(operation.capability);
826
+ if (!esLectura && (task.state !== 'implementacion_activa' || !state.activeG1)) {
827
+ fail('GATE_NOT_SATISFIED', 'G1/G2 no habilita la operación');
828
+ }
829
+ if (state.activeG1 && state.activeG1.expiresAt < authority.observedAt) {
830
+ fail('GATE_G1_EXPIRED', 'G1 expiró');
831
+ }
832
+ const evidence = {
833
+ configurationDigest: context.configurationDigest,
834
+ gateVersion: state.version, g1ApprovalDigest: state.activeG1?.approvalDigest || null,
835
+ g1ExpiresAt: state.activeG1?.expiresAt || null,
836
+ phaseId: operation.phaseId, planDigest: state.planDigest,
837
+ planVersion: state.planVersion, taskId: operation.taskId, taskState: task.state,
838
+ };
839
+ return freezeDeep({
840
+ ...evidence,
841
+ gateAuthorizationDigest: crypto.createHash('sha256')
842
+ .update(canonicalizarIJson(evidence), 'utf8').digest('hex'),
843
+ });
844
+ }
845
+
846
+ function createGateEngine(writer, singleUseStore, workspaceRoot) {
847
+ const owner = Object.freeze({});
848
+ const phases = new Map();
849
+ const publicGateEngine = Object.freeze({
850
+ isGateReceipt: (value) => RECEIPTS.get(value) === owner,
851
+ isVerifiedGateState: (value) => STATES.get(value) === owner,
852
+ openPhase: (config) => {
853
+ const created = createPhaseEngine(writer, singleUseStore, owner, config, workspaceRoot);
854
+ const previous = phases.get(created.boundContext.config.phaseId);
855
+ if (previous
856
+ && previous.configurationDigest !== created.boundContext.configurationDigest) {
857
+ fail('GATE_CONFIG_CONFLICT', 'fase registrada con otra configuración');
858
+ }
859
+ phases.set(created.boundContext.config.phaseId, created.boundContext);
860
+ return created.publicPhase;
861
+ },
862
+ });
863
+ const authorizationGateReader = Object.freeze({
864
+ read: (ledgerState, authority, operation) => (
865
+ readAuthorizationGate(phases, ledgerState, authority, operation)
866
+ ),
867
+ });
868
+ return Object.freeze({ authorizationGateReader, publicGateEngine });
869
+ }
870
+
871
+ module.exports = { createGateEngine };