H4b: gate de compatibilidad de configs en la receta real + hammer install (SDD 15 §H4)
Sube el modelo de slots de wawa-memo (prototipo host) a hammer:
- Recipe + source_patch del .swm llevan bloque `slots` {claims, requires} (slot->b3:…),
FUERA de hash_inputs (topología ≠ identidad, no mueve el artifact_hash). Viaja intacto
por los dos sentidos del puente (Recipe->.swm->Recipe): test de round-trip.
- InstalledDb registra claims por paquete + system_state() -> slot->hash (el Estado del gate).
- hammer-core::compat::evaluar(estado, slots) -> Veredicto {Compatible, Colision, Incompatible}
(el álgebra probada en wawa-memo, sobre tipos de hammer).
- Gate en `hammer install`: antes de tocar nada evalúa el paquete contra el estado instalado.
Incompatible (requisito sin resolver, caso wayland) -> aborta; Colisión (caso logo) ->
aborta pidiendo elección salvo --force-slots; Compatible -> procede y registra los claims.
Plumbing propagado por los 5 sitios de Mutation::SourcePatch (from_recipe, swm_bridge,
export, bus, orchestrator). Tests: compat (4) + slots-no-en-hash/round-trip (2) +
system_state (1) + puente receta<->swm (1). Workspace compila y verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -171,6 +171,7 @@ impl<T: IntentTranslator> Orchestrator<T> {
|
|||||||
expected_hash,
|
expected_hash,
|
||||||
deps: _,
|
deps: _,
|
||||||
evidence, // [H1c] viaja al lab; el veredicto vuelve en BuildReady
|
evidence, // [H1c] viaja al lab; el veredicto vuelve en BuildReady
|
||||||
|
slots: _, // [H4b] topología del sistema; la consume el gate de install, no el build
|
||||||
} => {
|
} => {
|
||||||
let CompileMode::ViaBus { store, timeout, .. } = &self.compile else {
|
let CompileMode::ViaBus { store, timeout, .. } = &self.compile else {
|
||||||
skipped_source_patches += 1;
|
skipped_source_patches += 1;
|
||||||
@@ -637,6 +638,7 @@ mod tests {
|
|||||||
expected_hash: None,
|
expected_hash: None,
|
||||||
deps: Default::default(),
|
deps: Default::default(),
|
||||||
evidence: Default::default(),
|
evidence: Default::default(),
|
||||||
|
slots: Default::default(),
|
||||||
}],
|
}],
|
||||||
signature: None,
|
signature: None,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ fn swm_with_evidence() -> Swm {
|
|||||||
expected_output: None,
|
expected_output: None,
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
|
slots: Default::default(),
|
||||||
}],
|
}],
|
||||||
signature: None,
|
signature: None,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,14 +73,14 @@ pub fn recipe_from_source_patch(
|
|||||||
name: Option<&str>,
|
name: Option<&str>,
|
||||||
catalog_dir: &Path,
|
catalog_dir: &Path,
|
||||||
) -> hammer_core::Result<Recipe> {
|
) -> hammer_core::Result<Recipe> {
|
||||||
let (repo, commit, tarball, sha256, strip_components, patch, patch_url, build_cfg, target_bin, deps, evidence) =
|
let (repo, commit, tarball, sha256, strip_components, patch, patch_url, build_cfg, target_bin, deps, evidence, slots) =
|
||||||
match mutation {
|
match mutation {
|
||||||
Mutation::SourcePatch {
|
Mutation::SourcePatch {
|
||||||
repo, commit, tarball, sha256, strip_components, patch, patch_url, build,
|
repo, commit, tarball, sha256, strip_components, patch, patch_url, build,
|
||||||
target_bin, deps, evidence, ..
|
target_bin, deps, evidence, slots, ..
|
||||||
} => (
|
} => (
|
||||||
repo, commit, tarball, sha256, strip_components, patch, patch_url, build,
|
repo, commit, tarball, sha256, strip_components, patch, patch_url, build,
|
||||||
target_bin, deps, evidence,
|
target_bin, deps, evidence, slots,
|
||||||
),
|
),
|
||||||
_ => {
|
_ => {
|
||||||
return Err(hammer_core::Error::Recipe(
|
return Err(hammer_core::Error::Recipe(
|
||||||
@@ -134,6 +134,7 @@ pub fn recipe_from_source_patch(
|
|||||||
*strip_components,
|
*strip_components,
|
||||||
deps,
|
deps,
|
||||||
evidence,
|
evidence,
|
||||||
|
slots,
|
||||||
patches,
|
patches,
|
||||||
catalog_dir,
|
catalog_dir,
|
||||||
)
|
)
|
||||||
@@ -158,6 +159,7 @@ fn synthesize_recipe(
|
|||||||
strip_components: Option<usize>,
|
strip_components: Option<usize>,
|
||||||
deps: &hammer_core::Deps,
|
deps: &hammer_core::Deps,
|
||||||
evidence: &hammer_core::Evidence,
|
evidence: &hammer_core::Evidence,
|
||||||
|
slots: &hammer_core::Slots,
|
||||||
patches: Vec<String>,
|
patches: Vec<String>,
|
||||||
base_dir: &Path,
|
base_dir: &Path,
|
||||||
) -> hammer_core::Result<Recipe> {
|
) -> hammer_core::Result<Recipe> {
|
||||||
@@ -218,6 +220,9 @@ flags = []
|
|||||||
// Evidencia de comportamiento (H1): viaja en el .swm y se reinyecta en la receta efímera para
|
// Evidencia de comportamiento (H1): viaja en el .swm y se reinyecta en la receta efímera para
|
||||||
// que el checker la vea al reproducir. No afecta el hash del artefacto (comportamiento ≠ identidad).
|
// que el checker la vea al reproducir. No afecta el hash del artefacto (comportamiento ≠ identidad).
|
||||||
recipe.evidence = evidence.clone();
|
recipe.evidence = evidence.clone();
|
||||||
|
// Superficies del sistema (H4b): viajan en el .swm y se reinyectan en la receta efímera para
|
||||||
|
// que el gate de compatibilidad de `install` las evalúe. Como la evidencia, no afectan el hash.
|
||||||
|
recipe.slots = slots.clone();
|
||||||
recipe.base_dir = base_dir.to_path_buf();
|
recipe.base_dir = base_dir.to_path_buf();
|
||||||
Ok(recipe)
|
Ok(recipe)
|
||||||
}
|
}
|
||||||
@@ -247,6 +252,44 @@ fn parse_link(s: &str) -> hammer_core::Result<LinkMode> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn slots_sobreviven_receta_a_swm_a_receta() {
|
||||||
|
// H4b: el gate de compatibilidad depende de que las superficies declaradas en la receta
|
||||||
|
// viajen intactas por el `.swm` y se recuperen al reproducir. Ejercita AMBAS direcciones
|
||||||
|
// del puente (Swm::from_recipe → recipe_from_source_patch).
|
||||||
|
let toml = r#"
|
||||||
|
name = "tema-oscuro"
|
||||||
|
version = "1.0"
|
||||||
|
[source]
|
||||||
|
repo = "git://x/tema.git"
|
||||||
|
commit = "abc123"
|
||||||
|
[build]
|
||||||
|
compiler = "zig-cc"
|
||||||
|
[slots.claims]
|
||||||
|
logo = "b3:azul"
|
||||||
|
[slots.requires]
|
||||||
|
"wayland-protocol" = "b3:stock"
|
||||||
|
"#;
|
||||||
|
let recipe = hammer_core::Recipe::from_toml(toml).unwrap();
|
||||||
|
let swm =
|
||||||
|
hammer_core::Swm::from_recipe(&recipe, "/usr/bin/tema", None, None, "2026-06-06").unwrap();
|
||||||
|
// El .swm YAML debe llevar el bloque slots.
|
||||||
|
let yaml = swm.to_yaml().unwrap();
|
||||||
|
assert!(yaml.contains("slots"), "el .swm debe transportar slots: {yaml}");
|
||||||
|
|
||||||
|
// Vuelta: source_patch → Recipe recupera exactamente los slots.
|
||||||
|
let d = tempfile::tempdir().unwrap();
|
||||||
|
let mutation = swm
|
||||||
|
.mutations
|
||||||
|
.iter()
|
||||||
|
.find(|m| matches!(m, Mutation::SourcePatch { .. }))
|
||||||
|
.unwrap();
|
||||||
|
let back = recipe_from_source_patch(mutation, Some("tema-oscuro"), d.path()).unwrap();
|
||||||
|
assert_eq!(back.slots, recipe.slots);
|
||||||
|
assert_eq!(back.slots.claims.get("logo").unwrap(), "b3:azul");
|
||||||
|
assert_eq!(back.slots.requires.get("wayland-protocol").unwrap(), "b3:stock");
|
||||||
|
}
|
||||||
|
|
||||||
fn fake_swm_build() -> SwmBuild {
|
fn fake_swm_build() -> SwmBuild {
|
||||||
SwmBuild {
|
SwmBuild {
|
||||||
compiler: "zig-cc".into(),
|
compiler: "zig-cc".into(),
|
||||||
@@ -278,6 +321,7 @@ mod tests {
|
|||||||
None,
|
None,
|
||||||
&hammer_core::Deps::default(),
|
&hammer_core::Deps::default(),
|
||||||
&hammer_core::Evidence::default(),
|
&hammer_core::Evidence::default(),
|
||||||
|
&hammer_core::Slots::default(),
|
||||||
vec![],
|
vec![],
|
||||||
d.path(),
|
d.path(),
|
||||||
)
|
)
|
||||||
@@ -307,6 +351,7 @@ mod tests {
|
|||||||
None,
|
None,
|
||||||
&hammer_core::Deps::default(),
|
&hammer_core::Deps::default(),
|
||||||
&hammer_core::Evidence::default(),
|
&hammer_core::Evidence::default(),
|
||||||
|
&hammer_core::Slots::default(),
|
||||||
vec![],
|
vec![],
|
||||||
d.path(),
|
d.path(),
|
||||||
)
|
)
|
||||||
@@ -342,6 +387,7 @@ mod tests {
|
|||||||
expected_hash: None,
|
expected_hash: None,
|
||||||
deps: Default::default(),
|
deps: Default::default(),
|
||||||
evidence: Default::default(),
|
evidence: Default::default(),
|
||||||
|
slots: Default::default(),
|
||||||
};
|
};
|
||||||
let recipe = recipe_from_source_patch(&m, None, &catalog).unwrap();
|
let recipe = recipe_from_source_patch(&m, None, &catalog).unwrap();
|
||||||
assert_eq!(recipe.source.patches.len(), 1);
|
assert_eq!(recipe.source.patches.len(), 1);
|
||||||
@@ -397,6 +443,7 @@ mod tests {
|
|||||||
expected_hash: None,
|
expected_hash: None,
|
||||||
deps: Default::default(),
|
deps: Default::default(),
|
||||||
evidence: Default::default(),
|
evidence: Default::default(),
|
||||||
|
slots: Default::default(),
|
||||||
};
|
};
|
||||||
let store = Store::open(d.path().join("store")).unwrap();
|
let store = Store::open(d.path().join("store")).unwrap();
|
||||||
let cfg = BuildConfig::defaults_for_store(store.root());
|
let cfg = BuildConfig::defaults_for_store(store.root());
|
||||||
|
|||||||
@@ -241,6 +241,11 @@ enum Cmd {
|
|||||||
/// (no basta con que el `.swm` reproduzca — exige autoría del catálogo verificada).
|
/// (no basta con que el `.swm` reproduzca — exige autoría del catálogo verificada).
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
require_signed: bool,
|
require_signed: bool,
|
||||||
|
/// [H4b] Ante una COLISIÓN de superficie (el paquete reclama un slot que ya ocupa otra
|
||||||
|
/// config, p. ej. el logo), adopta la nueva en vez de abortar. No afecta a un requisito
|
||||||
|
/// insatisfecho (ese rechazo es duro: el paquete no encaja con lo que tenés).
|
||||||
|
#[arg(long)]
|
||||||
|
force_slots: bool,
|
||||||
},
|
},
|
||||||
/// [Etapa F] Desinstala un paquete: borra los ficheros que registró (refcount: respeta los
|
/// [Etapa F] Desinstala un paquete: borra los ficheros que registró (refcount: respeta los
|
||||||
/// que otro paquete instalado también aporta) y lo quita de la DB de instalados.
|
/// que otro paquete instalado también aporta) y lo quita de la DB de instalados.
|
||||||
@@ -852,6 +857,7 @@ fn main() -> anyhow::Result<()> {
|
|||||||
state_root,
|
state_root,
|
||||||
db,
|
db,
|
||||||
require_signed,
|
require_signed,
|
||||||
|
force_slots,
|
||||||
} => {
|
} => {
|
||||||
run_install(
|
run_install(
|
||||||
&cli.store,
|
&cli.store,
|
||||||
@@ -864,6 +870,7 @@ fn main() -> anyhow::Result<()> {
|
|||||||
state_root.as_deref(),
|
state_root.as_deref(),
|
||||||
&db,
|
&db,
|
||||||
require_signed,
|
require_signed,
|
||||||
|
force_slots,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
Cmd::Uninstall { name, db } => run_uninstall(&name, &db)?,
|
Cmd::Uninstall { name, db } => run_uninstall(&name, &db)?,
|
||||||
@@ -1590,6 +1597,7 @@ fn run_install(
|
|||||||
state_root: Option<&std::path::Path>,
|
state_root: Option<&std::path::Path>,
|
||||||
db_path: &std::path::Path,
|
db_path: &std::path::Path,
|
||||||
require_signed: bool,
|
require_signed: bool,
|
||||||
|
force_slots: bool,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let source = RepoSource::parse(repo);
|
let source = RepoSource::parse(repo);
|
||||||
let index = source.read_index()?;
|
let index = source.read_index()?;
|
||||||
@@ -1658,6 +1666,48 @@ fn run_install(
|
|||||||
eprintln!("firma: no verificada (pasá --trust <dir> para validar la autoría)");
|
eprintln!("firma: no verificada (pasá --trust <dir> para validar la autoría)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gate de compatibilidad (H4b — configs compatibles, SDD 15 §H4). Antes de reconstruir o tocar
|
||||||
|
// nada, evaluamos las superficies que el paquete declara (`slots`) contra el estado INSTALADO.
|
||||||
|
// Es el chequeo local y reproducible de "cuando instalo, ¿es compatible con lo que ya tengo?".
|
||||||
|
let entrant_slots = source_patch_slots(&load_swm(swm_path_str)?);
|
||||||
|
if !entrant_slots.is_empty() {
|
||||||
|
entrant_slots
|
||||||
|
.validate()
|
||||||
|
.map_err(|e| anyhow::anyhow!("slots del paquete inválidos: {e}"))?;
|
||||||
|
let idb = hammer_core::InstalledDb::load(db_path)?;
|
||||||
|
let estado = idb.system_state();
|
||||||
|
match hammer_core::compat::evaluar(&estado, &entrant_slots) {
|
||||||
|
hammer_core::CompatVeredicto::Compatible => {
|
||||||
|
eprintln!(
|
||||||
|
"compat: compatible ({} reclamo(s), {} requisito(s) resuelto(s))",
|
||||||
|
entrant_slots.claims.len(),
|
||||||
|
entrant_slots.requires.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Caso wayland: una dependencia sobre una superficie que el usuario divergió. Rechazo duro.
|
||||||
|
hammer_core::CompatVeredicto::Incompatible { slot, requerido, presente } => {
|
||||||
|
anyhow::bail!(
|
||||||
|
"compat: INCOMPATIBLE — '{name}' requiere la superficie '{slot}' @ {requerido}, \
|
||||||
|
pero el sistema tiene {}. Divergiste esa superficie: no se instala sobre ella.",
|
||||||
|
presente.as_deref().unwrap_or("(ausente)")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Caso logo: dos configs escriben la misma superficie. Es una ELECCIÓN, no un error.
|
||||||
|
hammer_core::CompatVeredicto::Colision { slot, instalado, entrante } => {
|
||||||
|
if force_slots {
|
||||||
|
eprintln!(
|
||||||
|
"compat: colisión en '{slot}' ({instalado} → {entrante}); --force-slots ⇒ me quedo con la nueva"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
anyhow::bail!(
|
||||||
|
"compat: COLISIÓN en la superficie '{slot}' — ya la ocupa {instalado} y '{name}' \
|
||||||
|
quiere {entrante}. Es una elección: pasá --force-slots para adoptar la nueva."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Dependencias: resolvemos el cierre transitivo de build-deps desde el índice (deps antes
|
// Dependencias: resolvemos el cierre transitivo de build-deps desde el índice (deps antes
|
||||||
// que el dependiente) y poblamos el catálogo de recetas que el lab consultará al reproducir
|
// que el dependiente) y poblamos el catálogo de recetas que el lab consultará al reproducir
|
||||||
// el `source_patch`. Sin esto, un paquete con build-deps (openssh→zlib,openssl) no podría
|
// el `source_patch`. Sin esto, un paquete con build-deps (openssh→zlib,openssl) no podría
|
||||||
@@ -1729,6 +1779,9 @@ fn run_install(
|
|||||||
version: pkg_version,
|
version: pkg_version,
|
||||||
hash: pkg_hash,
|
hash: pkg_hash,
|
||||||
files,
|
files,
|
||||||
|
// Las superficies reclamadas quedan en la DB ⇒ pasan a formar el `system_state` que el
|
||||||
|
// gate de la PRÓXIMA instalación consultará (H4b). Así una colisión se detecta después.
|
||||||
|
claims: entrant_slots.claims.clone(),
|
||||||
});
|
});
|
||||||
idb.save(db_path)?;
|
idb.save(db_path)?;
|
||||||
eprintln!("registrado {pkg_name} ({nfiles} fichero(s)) en {}", db_path.display());
|
eprintln!("registrado {pkg_name} ({nfiles} fichero(s)) en {}", db_path.display());
|
||||||
@@ -1949,6 +2002,15 @@ fn source_patch_of(swm: &hammer_core::Swm) -> Option<&hammer_core::Mutation> {
|
|||||||
.find(|m| matches!(m, hammer_core::Mutation::SourcePatch { .. }))
|
.find(|m| matches!(m, hammer_core::Mutation::SourcePatch { .. }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Las superficies del sistema (H4b) que declara el `source_patch` de un `.swm`, o `Slots` vacío
|
||||||
|
/// si el paquete no declara topología. Lo consume el gate de compatibilidad de `install`.
|
||||||
|
fn source_patch_slots(swm: &hammer_core::Swm) -> hammer_core::Slots {
|
||||||
|
match source_patch_of(swm) {
|
||||||
|
Some(hammer_core::Mutation::SourcePatch { slots, .. }) => slots.clone(),
|
||||||
|
_ => hammer_core::Slots::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Firma el índice de un repo (release). Re-firmá tras publicar.
|
/// Firma el índice de un repo (release). Re-firmá tras publicar.
|
||||||
fn run_repo_sign(
|
fn run_repo_sign(
|
||||||
repo_dir: &std::path::Path,
|
repo_dir: &std::path::Path,
|
||||||
@@ -2432,6 +2494,7 @@ fn build_export_mutations(
|
|||||||
expected_hash: Some(art_hash),
|
expected_hash: Some(art_hash),
|
||||||
deps: recipe.deps.clone(),
|
deps: recipe.deps.clone(),
|
||||||
evidence: recipe.evidence.clone(),
|
evidence: recipe.evidence.clone(),
|
||||||
|
slots: recipe.slots.clone(),
|
||||||
});
|
});
|
||||||
stats.source_patches += 1;
|
stats.source_patches += 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
//! H4b — el gate de **compatibilidad de configs** sobre la receta real (SDD 15 §H4).
|
||||||
|
//!
|
||||||
|
//! Sube a hammer el modelo de slots que `wawa-memo/src/compat.rs` demostró como
|
||||||
|
//! prototipo (host/std, 52 tests verdes): una config/paquete declara qué
|
||||||
|
//! superficies del sistema **reclama** (`Slots::claims`) y **requiere**
|
||||||
|
//! (`Slots::requires`), ambas por hash. Antes de tocar el sistema, `install`
|
||||||
|
//! evalúa la config entrante contra el **estado instalado**
|
||||||
|
//! ([`crate::InstalledDb::system_state`]) y decide:
|
||||||
|
//!
|
||||||
|
//! - **Compatible** — requisitos resueltos y reclamos disjuntos (o coincidentes):
|
||||||
|
//! se puede instalar sin fricción.
|
||||||
|
//! - **Colisión** — reclama una superficie ya ocupada por OTRO contenido: es una
|
||||||
|
//! **elección** del usuario (el caso "ya cambié el logo y bajo otra config que
|
||||||
|
//! también lo cambia"), no un error. `install` la respeta salvo `--force-slots`.
|
||||||
|
//! - **Incompatible** — un requisito no resuelve contra el estado: **rechazo
|
||||||
|
//! duro** (el caso "modifiqué el protocolo de Wayland y bajo algo que depende
|
||||||
|
//! del Wayland stock"). Es el análogo de sistema a una referencia colgante.
|
||||||
|
//!
|
||||||
|
//! El chequeo es **local y reproducible** — el mismo "verificar, no confiar" de
|
||||||
|
//! SDD 09, ahora sobre la topología de superficies. "Compatible" (este módulo) es
|
||||||
|
//! el eje NUEVO; "completa" y "segura" ya viven en el stack (cierre transitivo /
|
||||||
|
//! reproducir el resultado) — ver el prototipo `wawa-memo` para la composición.
|
||||||
|
|
||||||
|
use crate::recipe::Slots;
|
||||||
|
|
||||||
|
/// El estado instalado como `slot → hash`. Alias de lo que produce
|
||||||
|
/// [`crate::InstalledDb::system_state`].
|
||||||
|
pub type Estado = std::collections::BTreeMap<String, String>;
|
||||||
|
|
||||||
|
/// El veredicto de evaluar unos [`Slots`] contra un [`Estado`]. Tres resultados
|
||||||
|
/// **mecánicamente distintos**: la incompatibilidad domina a la colisión.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum Veredicto {
|
||||||
|
/// Requisitos resueltos y reclamos disjuntos (o coincidentes en hash): instalable.
|
||||||
|
Compatible,
|
||||||
|
/// Un slot que la config reclama ya está ocupado por **otro** contenido. NO es
|
||||||
|
/// error: es una **elección** (caso *logo*).
|
||||||
|
Colision {
|
||||||
|
slot: String,
|
||||||
|
/// El hash que el slot tiene ahora en el estado.
|
||||||
|
instalado: String,
|
||||||
|
/// El hash que la config quiere poner.
|
||||||
|
entrante: String,
|
||||||
|
},
|
||||||
|
/// Un requisito **no resuelve** contra el estado (caso *wayland*): rechazo duro.
|
||||||
|
/// `presente = None` si el estado ni ofrece el slot; `Some(otro)` si otra versión.
|
||||||
|
Incompatible {
|
||||||
|
slot: String,
|
||||||
|
/// El hash que la config exige para ese slot.
|
||||||
|
requerido: String,
|
||||||
|
/// Lo que el estado ofrece para ese slot (o `None`).
|
||||||
|
presente: Option<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Veredicto {
|
||||||
|
/// `true` sólo si es [`Veredicto::Compatible`].
|
||||||
|
pub fn es_compatible(&self) -> bool {
|
||||||
|
matches!(self, Veredicto::Compatible)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evalúa unos `slots` contra el `estado` instalado. Primero **requisitos**
|
||||||
|
/// (incompatibilidad dura), luego **reclamos** (colisión = elección). Devuelve la
|
||||||
|
/// **primera** objeción en orden determinista (`BTreeMap`), o
|
||||||
|
/// [`Veredicto::Compatible`]. Un reclamo que coincide en hash es idempotente.
|
||||||
|
pub fn evaluar(estado: &Estado, slots: &Slots) -> Veredicto {
|
||||||
|
// 1. Requisitos: cada superficie requerida debe estar presente al hash exacto
|
||||||
|
// contra el que la config fue construida. Es la referencia colgante de sistema.
|
||||||
|
for (slot, requerido) in &slots.requires {
|
||||||
|
match estado.get(slot) {
|
||||||
|
Some(h) if h == requerido => {}
|
||||||
|
otro => {
|
||||||
|
return Veredicto::Incompatible {
|
||||||
|
slot: slot.clone(),
|
||||||
|
requerido: requerido.clone(),
|
||||||
|
presente: otro.cloned(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 2. Reclamos: un slot ya ocupado por otro contenido es colisión (elección).
|
||||||
|
for (slot, entrante) in &slots.claims {
|
||||||
|
if let Some(instalado) = estado.get(slot) {
|
||||||
|
if instalado != entrante {
|
||||||
|
return Veredicto::Colision {
|
||||||
|
slot: slot.clone(),
|
||||||
|
instalado: instalado.clone(),
|
||||||
|
entrante: entrante.clone(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Veredicto::Compatible
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn estado(pares: &[(&str, &str)]) -> Estado {
|
||||||
|
pares.iter().map(|(s, h)| (s.to_string(), h.to_string())).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn slots(claims: &[(&str, &str)], requires: &[(&str, &str)]) -> Slots {
|
||||||
|
Slots {
|
||||||
|
claims: claims.iter().map(|(s, h)| (s.to_string(), h.to_string())).collect(),
|
||||||
|
requires: requires.iter().map(|(s, h)| (s.to_string(), h.to_string())).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn logo_ya_cambiado_es_eleccion() {
|
||||||
|
let mio = estado(&[("logo", "b3:rojo")]);
|
||||||
|
let entrante = slots(&[("logo", "b3:azul")], &[]);
|
||||||
|
match evaluar(&mio, &entrante) {
|
||||||
|
Veredicto::Colision { slot, instalado, entrante } => {
|
||||||
|
assert_eq!(slot, "logo");
|
||||||
|
assert_eq!(instalado, "b3:rojo");
|
||||||
|
assert_eq!(entrante, "b3:azul");
|
||||||
|
}
|
||||||
|
otro => panic!("esperaba Colision, fue {otro:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reclamar_lo_mismo_es_idempotente() {
|
||||||
|
let mio = estado(&[("logo", "b3:rojo")]);
|
||||||
|
assert!(evaluar(&mio, &slots(&[("logo", "b3:rojo")], &[])).es_compatible());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wayland_divergido_rechaza_dependiente() {
|
||||||
|
let mio = estado(&[("wayland-protocol", "b3:mio")]);
|
||||||
|
let descargado = slots(&[("app-foo", "b3:v1")], &[("wayland-protocol", "b3:stock")]);
|
||||||
|
match evaluar(&mio, &descargado) {
|
||||||
|
Veredicto::Incompatible { slot, requerido, presente } => {
|
||||||
|
assert_eq!(slot, "wayland-protocol");
|
||||||
|
assert_eq!(requerido, "b3:stock");
|
||||||
|
assert_eq!(presente, Some("b3:mio".to_string()));
|
||||||
|
}
|
||||||
|
otro => panic!("esperaba Incompatible, fue {otro:?}"),
|
||||||
|
}
|
||||||
|
// Con el wayland stock, la misma descarga es compatible.
|
||||||
|
let stock = estado(&[("wayland-protocol", "b3:stock")]);
|
||||||
|
assert!(evaluar(&stock, &descargado).es_compatible());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn requisito_ausente_incompatible() {
|
||||||
|
let vacio = estado(&[]);
|
||||||
|
match evaluar(&vacio, &slots(&[], &[("wayland-protocol", "b3:stock")])) {
|
||||||
|
Veredicto::Incompatible { presente, .. } => assert_eq!(presente, None),
|
||||||
|
otro => panic!("esperaba Incompatible/None, fue {otro:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,6 +32,11 @@ pub struct InstalledPackage {
|
|||||||
pub hash: Option<String>,
|
pub hash: Option<String>,
|
||||||
/// Rutas ABSOLUTAS que este paquete creó (orden de instalación). Lo que `uninstall` borra.
|
/// Rutas ABSOLUTAS que este paquete creó (orden de instalación). Lo que `uninstall` borra.
|
||||||
pub files: Vec<String>,
|
pub files: Vec<String>,
|
||||||
|
/// Superficies del sistema que este paquete **reclama** → hash del contenido (H4b, SDD 15 §H4).
|
||||||
|
/// Vacío por defecto (compatibilidad hacia atrás: una DB previa a H4b no lo tiene). Con esto,
|
||||||
|
/// `system_state` deriva el `slot → hash` instalado que el gate de compatibilidad consulta.
|
||||||
|
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
|
||||||
|
pub claims: std::collections::BTreeMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InstalledDb {
|
impl InstalledDb {
|
||||||
@@ -78,6 +83,21 @@ impl InstalledDb {
|
|||||||
Some(self.packages.remove(i))
|
Some(self.packages.remove(i))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// El **estado del sistema** como `slot → hash`: la unión de los `claims` de todos los
|
||||||
|
/// paquetes instalados (H4b, SDD 15 §H4). Es el `Estado` contra el que el gate de
|
||||||
|
/// compatibilidad evalúa una config entrante. Un slot reclamado por dos paquetes no debería
|
||||||
|
/// pasar (el gate lo impide al instalar); si aún así ocurre, gana el orden de la lista (ya
|
||||||
|
/// ordenada por nombre) — determinista.
|
||||||
|
pub fn system_state(&self) -> std::collections::BTreeMap<String, String> {
|
||||||
|
let mut estado = std::collections::BTreeMap::new();
|
||||||
|
for p in &self.packages {
|
||||||
|
for (slot, hash) in &p.claims {
|
||||||
|
estado.entry(slot.clone()).or_insert_with(|| hash.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
estado
|
||||||
|
}
|
||||||
|
|
||||||
/// ¿Algún OTRO paquete instalado (≠ `name`) aporta también `file`? Si sí, `uninstall` no debe
|
/// ¿Algún OTRO paquete instalado (≠ `name`) aporta también `file`? Si sí, `uninstall` no debe
|
||||||
/// borrarlo (lo seguiría necesitando ese tercero).
|
/// borrarlo (lo seguiría necesitando ese tercero).
|
||||||
pub fn owned_by_others(&self, name: &str, file: &str) -> bool {
|
pub fn owned_by_others(&self, name: &str, file: &str) -> bool {
|
||||||
@@ -97,9 +117,26 @@ mod tests {
|
|||||||
version: "1".into(),
|
version: "1".into(),
|
||||||
hash: None,
|
hash: None,
|
||||||
files: files.iter().map(|s| s.to_string()).collect(),
|
files: files.iter().map(|s| s.to_string()).collect(),
|
||||||
|
claims: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn system_state_une_los_claims() {
|
||||||
|
let mut db = InstalledDb::default();
|
||||||
|
db.record(InstalledPackage {
|
||||||
|
name: "tema-oscuro".into(),
|
||||||
|
version: "1".into(),
|
||||||
|
hash: None,
|
||||||
|
files: vec![],
|
||||||
|
claims: [("logo".to_string(), "b3:rojo".to_string())]
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
|
});
|
||||||
|
let estado = db.system_state();
|
||||||
|
assert_eq!(estado.get("logo"), Some(&"b3:rojo".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn load_missing_is_empty() {
|
fn load_missing_is_empty() {
|
||||||
let d = tempfile::tempdir().unwrap();
|
let d = tempfile::tempdir().unwrap();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
pub mod apply;
|
pub mod apply;
|
||||||
pub mod caps;
|
pub mod caps;
|
||||||
|
pub mod compat;
|
||||||
pub mod hash;
|
pub mod hash;
|
||||||
pub mod installed;
|
pub mod installed;
|
||||||
pub mod proto;
|
pub mod proto;
|
||||||
@@ -19,9 +20,10 @@ pub mod swm;
|
|||||||
pub use caps::{AgentCapsConfig, CapRule};
|
pub use caps::{AgentCapsConfig, CapRule};
|
||||||
pub use hash::ArtifactHash;
|
pub use hash::ArtifactHash;
|
||||||
pub use installed::{InstalledDb, InstalledPackage};
|
pub use installed::{InstalledDb, InstalledPackage};
|
||||||
|
pub use compat::Veredicto as CompatVeredicto;
|
||||||
pub use recipe::{
|
pub use recipe::{
|
||||||
CheckOutcome, Compiler, Deps, Evidence, EvidenceCheck, EvidenceKind, LinkMode, Phases, Recipe,
|
CheckOutcome, Compiler, Deps, Evidence, EvidenceCheck, EvidenceKind, LinkMode, Phases, Recipe,
|
||||||
Source, SourceKind,
|
Slots, Source, SourceKind,
|
||||||
};
|
};
|
||||||
pub use repo::{PackageEntry, RepoIndex};
|
pub use repo::{PackageEntry, RepoIndex};
|
||||||
pub use sign::{KeyPair, SigStatus, TrustStore};
|
pub use sign::{KeyPair, SigStatus, TrustStore};
|
||||||
|
|||||||
@@ -19,6 +19,13 @@ pub struct Recipe {
|
|||||||
/// cambiar evidencia NO mueve el `artifact_hash` (el baseline de reproducibilidad no se toca).
|
/// cambiar evidencia NO mueve el `artifact_hash` (el baseline de reproducibilidad no se toca).
|
||||||
#[serde(default, skip_serializing_if = "Evidence::is_empty")]
|
#[serde(default, skip_serializing_if = "Evidence::is_empty")]
|
||||||
pub evidence: Evidence,
|
pub evidence: Evidence,
|
||||||
|
/// **Superficies del sistema** que este paquete reclama y requiere (H4b — configs
|
||||||
|
/// compatibles, SDD 15 §H4). Igual que `evidence`: **NO entra en `hash_inputs`** — declara
|
||||||
|
/// TOPOLOGÍA (qué toca / de qué depende), no identidad del artefacto; agregar o cambiar el
|
||||||
|
/// bloque no mueve el `artifact_hash`. Lo consume el gate de compatibilidad de `install`
|
||||||
|
/// (`crate::compat`) contra el estado instalado, no el build.
|
||||||
|
#[serde(default, skip_serializing_if = "Slots::is_empty")]
|
||||||
|
pub slots: Slots,
|
||||||
/// Directorio base contra el que se resuelven rutas relativas de la receta
|
/// Directorio base contra el que se resuelven rutas relativas de la receta
|
||||||
/// (típicamente, `patches`). Lo fija `load_from_path`; al deserializar puro queda vacío.
|
/// (típicamente, `patches`). Lo fija `load_from_path`; al deserializar puro queda vacío.
|
||||||
#[serde(skip, default)]
|
#[serde(skip, default)]
|
||||||
@@ -163,6 +170,51 @@ impl Deps {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Las **superficies del sistema** de una config/paquete (H4b — SDD 15 §H4). Una config del
|
||||||
|
/// usuario = un conjunto de modificaciones direccionadas por contenido: cada modificación
|
||||||
|
/// **reclama** una superficie (la escribe) y/o **requiere** otra (depende de ella a una versión
|
||||||
|
/// exacta). El valor de cada slot es un hash `b3:…` del contenido — misma disciplina de "todo por
|
||||||
|
/// hash" que el resto del stack.
|
||||||
|
///
|
||||||
|
/// El gate de compatibilidad (`crate::compat`) usa estos dos mapas contra el estado instalado:
|
||||||
|
/// - un `requires` que no resuelve contra el estado ⇒ **incompatible** (el caso "modifiqué el
|
||||||
|
/// protocolo de Wayland y bajo algo que depende del Wayland stock"); es el análogo, a nivel de
|
||||||
|
/// sistema, de una referencia colgante.
|
||||||
|
/// - un `claims` que pisa una superficie ya ocupada por otro contenido ⇒ **colisión = elección**
|
||||||
|
/// (el caso "ya cambié el logo y bajo otra config que también lo cambia"), no un error.
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct Slots {
|
||||||
|
/// Superficies que este paquete **escribe** → hash del contenido nuevo (`slot → b3:…`).
|
||||||
|
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
|
||||||
|
pub claims: std::collections::BTreeMap<String, String>,
|
||||||
|
/// Superficies de las que **depende** → versión exacta requerida (`slot → b3:…`).
|
||||||
|
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
|
||||||
|
pub requires: std::collections::BTreeMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Slots {
|
||||||
|
/// `true` si no declara ninguna superficie (ni claims ni requires). Para serde: una receta
|
||||||
|
/// sin bloque `slots` no lo emite.
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.claims.is_empty() && self.requires.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Valida la forma (schema, no compatibilidad): cada valor de slot debe ser un hash `b3:…`.
|
||||||
|
/// El gate de `install` corre esto antes de evaluar.
|
||||||
|
pub fn validate(&self) -> crate::Result<()> {
|
||||||
|
for (mapa, etiqueta) in [(&self.claims, "claims"), (&self.requires, "requires")] {
|
||||||
|
for (slot, hash) in mapa {
|
||||||
|
if !hash.starts_with("b3:") {
|
||||||
|
return Err(crate::Error::Recipe(format!(
|
||||||
|
"slots.{etiqueta}[{slot:?}]: el valor debe ser un hash `b3:…`, es {hash:?}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Evidencia de comportamiento de una receta (H1 — proof-carrying recipes, SDD 15 §H1). La IA
|
/// Evidencia de comportamiento de una receta (H1 — proof-carrying recipes, SDD 15 §H1). La IA
|
||||||
/// entrega, junto al build, un conjunto de `checks` que el verificador (`hammer swm-verify
|
/// entrega, junto al build, un conjunto de `checks` que el verificador (`hammer swm-verify
|
||||||
/// --evidence`) corre DENTRO del sandbox reproducible; la mutación sólo se propone si todos pasan.
|
/// --evidence`) corre DENTRO del sandbox reproducible; la mutación sólo se propone si todos pasan.
|
||||||
@@ -674,6 +726,40 @@ expected_output = "b3:deadbeef"
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn slots_round_trip_y_no_afectan_el_hash() {
|
||||||
|
// H4b: el bloque `slots` declara TOPOLOGÍA (qué toca / de qué depende), no identidad ⇒
|
||||||
|
// como la evidencia, NO entra en hash_inputs. Y round-trippea por TOML.
|
||||||
|
let con_toml = format!(
|
||||||
|
"{SAMPLE}\n[slots.claims]\nlogo = \"b3:rojo\"\n[slots.requires]\n\"wayland-protocol\" = \"b3:stock\"\n"
|
||||||
|
);
|
||||||
|
let con = Recipe::from_toml(&con_toml).unwrap();
|
||||||
|
assert_eq!(con.slots.claims.get("logo").unwrap(), "b3:rojo");
|
||||||
|
assert_eq!(con.slots.requires.get("wayland-protocol").unwrap(), "b3:stock");
|
||||||
|
con.slots.validate().unwrap();
|
||||||
|
|
||||||
|
// Round-trip: serializa y vuelve a parsear ⇒ mismos slots.
|
||||||
|
let re = Recipe::from_toml(&con.to_toml().unwrap()).unwrap();
|
||||||
|
assert_eq!(re.slots, con.slots);
|
||||||
|
|
||||||
|
// Mismo hash con y sin slots.
|
||||||
|
let mut sin = con.clone();
|
||||||
|
sin.slots = Slots::default();
|
||||||
|
assert!(!con.slots.is_empty() && sin.slots.is_empty());
|
||||||
|
assert_eq!(
|
||||||
|
sin.hash_inputs(&[]).unwrap(),
|
||||||
|
con.hash_inputs(&[]).unwrap(),
|
||||||
|
"los slots NO deben entrar en hash_inputs"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn slots_validate_rechaza_hash_sin_prefijo() {
|
||||||
|
let mut s = Slots::default();
|
||||||
|
s.claims.insert("logo".into(), "rojo".into()); // falta `b3:`
|
||||||
|
assert!(s.validate().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn evidence_validate_rejects_empty_cmd_and_bad_hash() {
|
fn evidence_validate_rejects_empty_cmd_and_bad_hash() {
|
||||||
let bad_cmd = r#"
|
let bad_cmd = r#"
|
||||||
|
|||||||
@@ -106,6 +106,12 @@ pub enum Mutation {
|
|||||||
/// sandbox antes de aceptar la mutación. Vacío ⇒ paquete sin evidencia (sólo reproducibilidad).
|
/// sandbox antes de aceptar la mutación. Vacío ⇒ paquete sin evidencia (sólo reproducibilidad).
|
||||||
#[serde(default, skip_serializing_if = "crate::recipe::Evidence::is_empty")]
|
#[serde(default, skip_serializing_if = "crate::recipe::Evidence::is_empty")]
|
||||||
evidence: crate::recipe::Evidence,
|
evidence: crate::recipe::Evidence,
|
||||||
|
/// Superficies del sistema que el paquete reclama/requiere (H4b — configs compatibles, SDD
|
||||||
|
/// 15 §H4). El receptor las evalúa contra su estado instalado (`crate::compat`) antes de
|
||||||
|
/// hidratar: un requisito que no resuelve aborta, un reclamo que pisa otra config pide
|
||||||
|
/// elección. Vacío ⇒ paquete que no declara topología (no participa del gate).
|
||||||
|
#[serde(default, skip_serializing_if = "crate::recipe::Slots::is_empty")]
|
||||||
|
slots: crate::recipe::Slots,
|
||||||
},
|
},
|
||||||
ConfigEdit {
|
ConfigEdit {
|
||||||
file: String,
|
file: String,
|
||||||
@@ -296,6 +302,7 @@ impl Swm {
|
|||||||
expected_hash,
|
expected_hash,
|
||||||
deps: recipe.deps.clone(),
|
deps: recipe.deps.clone(),
|
||||||
evidence: recipe.evidence.clone(),
|
evidence: recipe.evidence.clone(),
|
||||||
|
slots: recipe.slots.clone(),
|
||||||
}],
|
}],
|
||||||
signature: None,
|
signature: None,
|
||||||
};
|
};
|
||||||
@@ -588,6 +595,7 @@ mutations:
|
|||||||
expected_hash: None,
|
expected_hash: None,
|
||||||
deps: Default::default(),
|
deps: Default::default(),
|
||||||
evidence: Default::default(),
|
evidence: Default::default(),
|
||||||
|
slots: Default::default(),
|
||||||
}],
|
}],
|
||||||
signature: None,
|
signature: None,
|
||||||
};
|
};
|
||||||
@@ -619,6 +627,7 @@ mutations:
|
|||||||
expected_hash: None,
|
expected_hash: None,
|
||||||
deps: Default::default(),
|
deps: Default::default(),
|
||||||
evidence: Default::default(),
|
evidence: Default::default(),
|
||||||
|
slots: Default::default(),
|
||||||
}],
|
}],
|
||||||
signature: None,
|
signature: None,
|
||||||
};
|
};
|
||||||
@@ -647,6 +656,7 @@ mutations:
|
|||||||
expected_hash: None,
|
expected_hash: None,
|
||||||
deps: Default::default(),
|
deps: Default::default(),
|
||||||
evidence: Default::default(),
|
evidence: Default::default(),
|
||||||
|
slots: Default::default(),
|
||||||
};
|
};
|
||||||
let err = m.verify_schema().unwrap_err();
|
let err = m.verify_schema().unwrap_err();
|
||||||
assert!(err.contains("no ambos"), "mensaje inesperado: {err}");
|
assert!(err.contains("no ambos"), "mensaje inesperado: {err}");
|
||||||
@@ -748,6 +758,7 @@ zig_version = "0.13.0"
|
|||||||
expected_hash: None,
|
expected_hash: None,
|
||||||
deps: Default::default(),
|
deps: Default::default(),
|
||||||
evidence: Default::default(),
|
evidence: Default::default(),
|
||||||
|
slots: Default::default(),
|
||||||
};
|
};
|
||||||
let err = m.verify_schema().unwrap_err();
|
let err = m.verify_schema().unwrap_err();
|
||||||
assert!(err.contains("faltan campos"), "mensaje inesperado: {err}");
|
assert!(err.contains("faltan campos"), "mensaje inesperado: {err}");
|
||||||
|
|||||||
@@ -385,6 +385,9 @@ fn run_compile(recipe: RecipeInline, store_root: PathBuf, tx: Sender<Event>) {
|
|||||||
// [H1c] La evidencia declarada por el cliente viaja a la mutación para ejecutarse tras
|
// [H1c] La evidencia declarada por el cliente viaja a la mutación para ejecutarse tras
|
||||||
// sellar el artefacto (ver `evidence_verdict`).
|
// sellar el artefacto (ver `evidence_verdict`).
|
||||||
evidence: recipe.evidence,
|
evidence: recipe.evidence,
|
||||||
|
// [H4b] El camino del daemon (agente propone → construye) no declara superficies: el gate
|
||||||
|
// de compatibilidad vive en `install` (CLI) contra el estado instalado. Vacío aquí.
|
||||||
|
slots: Default::default(),
|
||||||
};
|
};
|
||||||
let store = match hammer_core::Store::open(&store_root) {
|
let store = match hammer_core::Store::open(&store_root) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
|
|||||||
@@ -303,14 +303,25 @@ La definición dura de compatibilidad de una config `C` contra mi estado `E`:
|
|||||||
chequeo de slots y ser inservible por **incompleta** (paquete manipulado ⇒ `RefColgante`)
|
chequeo de slots y ser inservible por **incompleta** (paquete manipulado ⇒ `RefColgante`)
|
||||||
o **insegura** (resultado afirmado falso ⇒ recomputar lo rechaza) — las tres propiedades,
|
o **insegura** (resultado afirmado falso ⇒ recomputar lo rechaza) — las tres propiedades,
|
||||||
un solo hash.
|
un solo hash.
|
||||||
- **H4b** — (siguiente) subir el modelo de slots de `wawa-memo` (host/std) a la **receta**
|
- **H4b** ✅ — El modelo de slots **subido a la receta real de hammer** (no ya un prototipo
|
||||||
real: que un `.swm`/receta declare `reclama`/`requiere` (fuera de `hash_inputs`, misma
|
host). Cambios:
|
||||||
disciplina que la evidencia de H1 y la procedencia de H3a). El `Estado` instalado es la
|
- `Recipe` (y el `source_patch` del `.swm`) llevan un bloque **`slots`** con `claims`/
|
||||||
`InstalledDb` de Etapa F vista como `slot → Id`. Entonces `hammer install <config-de-malla>`
|
`requires` (`slot → b3:…`), **fuera de `hash_inputs`** — misma disciplina que la evidencia
|
||||||
corre `filtrar` contra el estado local antes de tocar nada. Frontera: definir el
|
de H1: declara topología, no identidad ⇒ no mueve el `artifact_hash` (test que lo ancla).
|
||||||
**espacio de slots** del sistema (qué es una "superficie": un fichero, un módulo wasm de
|
Viaja intacto por los dos sentidos del puente (`Recipe → .swm → Recipe`, test de round-trip).
|
||||||
wawa, un componente) — ahí está el trabajo de diseño real, no en el álgebra de
|
- `InstalledDb` (Etapa F) registra los `claims` por paquete y expone `system_state() →
|
||||||
compatibilidad (que ya está).
|
slot → hash`: el `Estado` que el gate consulta.
|
||||||
|
- `hammer-core::compat::evaluar(estado, slots) → Veredicto` (el álgebra probada en `wawa-memo`,
|
||||||
|
ahora sobre tipos de hammer).
|
||||||
|
- **El gate en `hammer install`**: antes de reconstruir o hidratar nada, evalúa el paquete
|
||||||
|
entrante contra el estado instalado. **Incompatible** (requisito sin resolver, caso wayland)
|
||||||
|
⇒ aborta con un mensaje claro; **Colisión** (caso logo) ⇒ aborta pidiendo elección, salvo
|
||||||
|
`--force-slots`; **Compatible** ⇒ procede y registra los `claims` (para que la PRÓXIMA
|
||||||
|
instalación detecte la colisión). `hammer install --help` expone `--force-slots`.
|
||||||
|
Frontera que queda: definir el **espacio de slots** del sistema (qué es una "superficie": un
|
||||||
|
fichero, un módulo wasm de wawa, un componente) — ahí está el diseño real, no en el álgebra
|
||||||
|
(que ya está). Hoy el productor de recetas declara los slots a mano; falta la **convención**
|
||||||
|
y, más allá, **derivarlos** de lo que el paquete de verdad toca.
|
||||||
|
|
||||||
**Frontera honesta.** H4a demuestra el *álgebra* sobre hashes abstractos; lo que **no**
|
**Frontera honesta.** H4a demuestra el *álgebra* sobre hashes abstractos; lo que **no**
|
||||||
resuelve es la **granularidad de los slots** — dos configs pueden no colisionar en el slot
|
resuelve es la **granularidad de los slots** — dos configs pueden no colisionar en el slot
|
||||||
@@ -370,7 +381,7 @@ H3a (design-doc) ──► registrar la visión, barato
|
|||||||
└► H3b ✅ (experimento wasm-por-función) [sobre H2 puro: núcleo Unison verde]
|
└► H3b ✅ (experimento wasm-por-función) [sobre H2 puro: núcleo Unison verde]
|
||||||
└► H3c ✅ (linker de contenido: imports por hash, Merkle-DAG intra-función)
|
└► H3c ✅ (linker de contenido: imports por hash, Merkle-DAG intra-función)
|
||||||
└► H4a ✅ (config = conjunto de slots por hash: compatible/completa/segura)
|
└► H4a ✅ (config = conjunto de slots por hash: compatible/completa/segura)
|
||||||
└► H4b (subir el modelo de slots a la receta/.swm real)
|
└► H4b ✅ (slots en la receta/.swm real + gate en `hammer install`)
|
||||||
└► [proceso] replay del MonotonicLog ──► plan OS-CRDT (otro agente)
|
└► [proceso] replay del MonotonicLog ──► plan OS-CRDT (otro agente)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user