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:
@@ -6,6 +6,37 @@ ratio/diff ver `git show <sha>`.
|
|||||||
|
|
||||||
## 2026-05-10
|
## 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:<name>\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
|
### feat(yahweh-widget-app-header): promover el header standard de explorers
|
||||||
Iter 16. Patrón con 4 consumers idénticos: `nakui-explorer`,
|
Iter 16. Patrón con 4 consumers idénticos: `nakui-explorer`,
|
||||||
`nouser-explorer`, `minga-explorer`, `brahman-broker-explorer`
|
`nouser-explorer`, `minga-explorer`, `brahman-broker-explorer`
|
||||||
|
|||||||
@@ -136,7 +136,12 @@ impl Executor {
|
|||||||
let graph = ManifestGraph::build(&manifest)?;
|
let graph = ManifestGraph::build(&manifest)?;
|
||||||
let schema_path = build_schema_bundle(&module_dir, &manifest.effective_schemas())?;
|
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 schema_bundle_hash = compute_schema_bundle_hash(&schema_bundle_bytes);
|
||||||
let mut schema_hashes = HashMap::with_capacity(manifest.morphisms.len());
|
let mut schema_hashes = HashMap::with_capacity(manifest.morphisms.len());
|
||||||
for spec in &manifest.morphisms {
|
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
|
/// El path en el `import "..."` queda absoluto (resuelto desde
|
||||||
/// `module_dir`) para que el evaluator lo encuentre desde el
|
/// `module_dir`) para que el evaluator lo encuentre desde el
|
||||||
/// tempdir.
|
/// 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(
|
fn build_schema_bundle(
|
||||||
module_dir: &std::path::Path,
|
module_dir: &std::path::Path,
|
||||||
schemas: &[String],
|
schemas: &[String],
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ fn seed_and_log_writes_bundle_hash_into_seed_entries() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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 temp = TempModule::from(&treasury_module());
|
||||||
let log_path = fresh_log_path();
|
let log_path = fresh_log_path();
|
||||||
let id = Uuid::new_v4();
|
let id = Uuid::new_v4();
|
||||||
|
|||||||
Reference in New Issue
Block a user