@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,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 };
|