From 7fb2ad3b1ebba21ac0dd997f8e282f67c847888b Mon Sep 17 00:00:00 2001 From: Sergio Date: Sun, 10 May 2026 13:10:48 +0000 Subject: [PATCH] fix(nakui-core): schema_bundle_hash usa bytes reales del schema, no del bundle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Iter 17. Regresión surfaceada por verify_log_rejects_seed_after_schema_kcl_changes. Bug: compute_schema_bundle_hash operaba sobre los bytes del bundle compilado, que es `(import "/abs/path") & ...`. Esos bytes no cambian cuando se edita el archivo apuntado; sólo cambian si se mueve el módulo o se agregan/quitan schemas. El hash quedaba pegado y un seed firmado con schema vN se verificaba ok contra schema vN+1. Fix: nueva fn read_schema_files_concat que lee cada schema declarado y los concatena con framing `\0NCL:\0`. Esos bytes alimentan los dos hashers (schema_bundle_hash y morphism_schema_hash). El bundle compilado sigue siendo imports-style (Nickel necesita los paths para resolver), sólo la fuente del hash cambia. Impacto: logs versados con el binario anterior fallan SchemaMismatch al verificarse — comportamiento correcto (re-seed). Test renombrado: verify_log_rejects_seed_after_schema_kcl_changes → _after_schema_changes (residuo de la migración KCL→Nickel). 10/10 schema_versioning verde. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 31 +++++++++++++++++++ crates/modules/nakui/core/src/executor.rs | 29 ++++++++++++++++- .../nakui/core/tests/schema_versioning.rs | 2 +- 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99b166e..e399af2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,37 @@ ratio/diff ver `git show `. ## 2026-05-10 +### fix(nakui-core): schema_bundle_hash debe reflejar el contenido real del schema +Iter 17. Regresión surfaceada por el workspace test +`verify_log_rejects_seed_after_schema_kcl_changes` (rebautizado a +`verify_log_rejects_seed_after_schema_changes`). + +**Bug**: `compute_schema_bundle_hash` operaba sobre los bytes del +bundle compilado, que `build_schema_bundle` arma como +`(import "/abs/path/schema.ncl") & ...`. Los bytes del bundle nunca +cambian cuando se edita el archivo apuntado — sólo cambian si se +agregan/quitan schemas o se mueve el módulo de path. Resultado: el +hash quedaba pegado y los seeds firmados bajo schema vN se aceptaban +silenciosamente como válidos bajo schema vN+1, aunque las invariantes +hubieran cambiado. + +**Fix**: nueva fn `read_schema_files_concat(module_dir, schemas)` +que lee los bytes de cada schema declarado y los concatena con +framing `\0NCL:\0` (separador no ambiguo + nombre relativo, no +absoluto, para que el hash sea estable entre máquinas). Esos bytes +alimentan `compute_schema_bundle_hash` y `compute_morphism_schema_hash` +en lugar de los bytes del bundle. El bundle compilado sigue siendo +imports-style (necesario para que Nickel resuelva paths relativos); +sólo la fuente del hash cambió. + +**Impacto en logs existentes**: como cualquier cambio al insumo del +hash, los seeds y morphisms versados bajo el bundle hash anterior +fallarán `SchemaMismatch` al verificarse contra un binario nuevo. No +hay migración — esto es exactamente el comportamiento que el hash +busca: re-seed. + +Tests: 10/10 en `schema_versioning` (era 9/10 con 1 FAILED). + ### feat(yahweh-widget-app-header): promover el header standard de explorers Iter 16. Patrón con 4 consumers idénticos: `nakui-explorer`, `nouser-explorer`, `minga-explorer`, `brahman-broker-explorer` diff --git a/crates/modules/nakui/core/src/executor.rs b/crates/modules/nakui/core/src/executor.rs index 1707832..23ea7d9 100644 --- a/crates/modules/nakui/core/src/executor.rs +++ b/crates/modules/nakui/core/src/executor.rs @@ -136,7 +136,12 @@ impl Executor { let graph = ManifestGraph::build(&manifest)?; let schema_path = build_schema_bundle(&module_dir, &manifest.effective_schemas())?; - let schema_bundle_bytes = std::fs::read(&schema_path)?; + // Hash insumos = bytes reales de cada schema file, NO el bundle + // (que sólo contiene `import "/abs/path"` y no cambia cuando el + // archivo apuntado se mueve). Sin esto, el bundle hash quedaba + // pegado y la versión del seed nunca detectaba ediciones de + // schema. Ver `verify_log_rejects_seed_after_schema_changes`. + let schema_bundle_bytes = read_schema_files_concat(&module_dir, &manifest.effective_schemas())?; let schema_bundle_hash = compute_schema_bundle_hash(&schema_bundle_bytes); let mut schema_hashes = HashMap::with_capacity(manifest.morphisms.len()); for spec in &manifest.morphisms { @@ -519,6 +524,28 @@ pub fn normalize_rhai_source(src: &str) -> String { /// El path en el `import "..."` queda absoluto (resuelto desde /// `module_dir`) para que el evaluator lo encuentre desde el /// tempdir. +/// Concatena los bytes de cada schema file declarado, en orden y con +/// framing `\0NCL:\0`, para alimentar el bundle hash con el +/// contenido real de los schemas. El bundle compilado por +/// `build_schema_bundle` sólo contiene imports de paths absolutos — +/// inestables entre runs y, peor, invariantes a ediciones del archivo +/// apuntado. Esta función entrega bytes que sí mueven el hash cuando +/// cambia el schema en disco. +fn read_schema_files_concat( + module_dir: &std::path::Path, + schemas: &[String], +) -> std::io::Result> { + let mut out = Vec::new(); + for s in schemas { + out.extend_from_slice(b"\0NCL:"); + out.extend_from_slice(s.as_bytes()); + out.push(0); + let bytes = std::fs::read(module_dir.join(s))?; + out.extend_from_slice(&bytes); + } + Ok(out) +} + fn build_schema_bundle( module_dir: &std::path::Path, schemas: &[String], diff --git a/crates/modules/nakui/core/tests/schema_versioning.rs b/crates/modules/nakui/core/tests/schema_versioning.rs index d8f478b..abf15a1 100644 --- a/crates/modules/nakui/core/tests/schema_versioning.rs +++ b/crates/modules/nakui/core/tests/schema_versioning.rs @@ -335,7 +335,7 @@ fn seed_and_log_writes_bundle_hash_into_seed_entries() { } #[test] -fn verify_log_rejects_seed_after_schema_kcl_changes() { +fn verify_log_rejects_seed_after_schema_changes() { let temp = TempModule::from(&treasury_module()); let log_path = fresh_log_path(); let id = Uuid::new_v4();