fix(nakui-core): schema_bundle_hash usa bytes reales del schema, no del bundle

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:<name>\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) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-05-10 13:10:48 +00:00
parent be3a0e78fc
commit 7fb2ad3b1e
3 changed files with 60 additions and 2 deletions
+28 -1
View File
@@ -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:<name>\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<Vec<u8>> {
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],