H4e: requires OBSERVADOS — dep de runtime divergida = incompatible (SDD 15 §H4)
Cierra la simetría de la vía observada: además de observar lo que un paquete escribe (H4c), observa de qué depende A UNA VERSIÓN. Fuente observable sin declaración: el paquete se construyó contra la versión de sus deps que hay en el repo (deps.runtime del .swm + expected_hash de cada dep en el índice); si el usuario tiene esa dep instalada a OTRO hash, la divergió -> rechazo duro. Es el caso wayland DERIVADO (el que H4b captura cuando el autor declara requires, ahora leído del cierre). - compat::observed_requires(swm, index) + version_conflicts(db, req) — reusan deps del .swm, expected_hash del índice e InstalledDb.hash (cero declaración nueva). - Cableado en install (rechazo duro, no lo salva --force-slots) y en `hammer compat`. - Verificado e2e real: `hammer compat` marca app INCOMPATIBLE por su dep wayland-protocol instalada a un hash divergido del repo (read-only, ve el source_patch sin construirlo). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1707,6 +1707,26 @@ fn run_install(
|
||||
}
|
||||
}
|
||||
|
||||
// H4e — vía OBSERVADA de los requisitos: por cada dep de runtime, la versión (expected_hash)
|
||||
// que hay en el repo (contra la que el paquete se construyó). Si la tengo instalada a OTRO
|
||||
// hash, la diverguí ⇒ rechazo duro (el caso wayland, derivado — no lo salva --force-slots).
|
||||
{
|
||||
let idb = hammer_core::InstalledDb::load(db_path)?;
|
||||
let req = hammer_core::compat::observed_requires(&entrant_swm, &index);
|
||||
let conflicts = hammer_core::compat::version_conflicts(&idb, &req);
|
||||
if !conflicts.is_empty() {
|
||||
let d = conflicts
|
||||
.iter()
|
||||
.map(|c| format!("{} necesita {} pero tenés {}", c.dep, c.requerido, c.instalado))
|
||||
.collect::<Vec<_>>()
|
||||
.join("; ");
|
||||
anyhow::bail!(
|
||||
"compat: INCOMPATIBLE — '{name}' se construyó contra deps que divergiste: {d}. \
|
||||
No encaja con tu sistema (rechazo duro, no lo fuerza --force-slots)."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// H4b — vía DECLARADA: las superficies (`slots`) que el autor declara reclamar/requerir.
|
||||
let entrant_slots = source_patch_slots(&entrant_swm);
|
||||
if !entrant_slots.is_empty() {
|
||||
@@ -1999,7 +2019,15 @@ fn run_compat(repo: &str, db_path: &std::path::Path) -> anyhow::Result<()> {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
// 1) Vía DECLARADA (slots): una incompatibilidad domina todo.
|
||||
// 1a) Vía OBSERVADA de requisitos (H4e): una dep de runtime divergida ⇒ incompatible.
|
||||
let req = hammer_core::compat::observed_requires(&swm, &index);
|
||||
let vconf = hammer_core::compat::version_conflicts(&idb, &req);
|
||||
if !vconf.is_empty() {
|
||||
let d = vconf.iter().map(|c| format!("{}@{} (tenés {})", c.dep, c.requerido, c.instalado)).collect::<Vec<_>>().join(", ");
|
||||
incompatibles.push(format!("{:<16} deps divergidas: {d}", entry.name));
|
||||
continue;
|
||||
}
|
||||
// 1b) Vía DECLARADA (slots): una incompatibilidad domina todo.
|
||||
let slots = source_patch_slots(&swm);
|
||||
let veredicto = hammer_core::compat::evaluar(&estado, &slots);
|
||||
if let hammer_core::CompatVeredicto::Incompatible { slot, requerido, presente } = &veredicto {
|
||||
|
||||
@@ -122,3 +122,44 @@ fn compat_particiona_el_repo_contra_lo_instalado() {
|
||||
assert!(stdout.contains("requieren elección (1)"), "stdout: {stdout}");
|
||||
assert!(stdout.contains("tema-oscuro"), "stdout: {stdout}");
|
||||
}
|
||||
|
||||
/// H4e e2e: `hammer compat` marca INCOMPATIBLE un paquete cuya dep de runtime divergiste
|
||||
/// (el caso wayland, DERIVADO — sin que el paquete declare `requires`). Read-only: ve el
|
||||
/// source_patch sin construirlo.
|
||||
#[test]
|
||||
fn compat_marca_incompatible_una_dep_divergida() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let repo = tmp.path().join("repo");
|
||||
std::fs::create_dir_all(&repo).unwrap();
|
||||
let db = tmp.path().join("installed.json");
|
||||
|
||||
// Tengo wayland-protocol instalado a b3:mine (lo diverguí del stock del repo).
|
||||
std::fs::write(
|
||||
&db,
|
||||
r#"{"packages":[{"name":"wayland-protocol","version":"1","hash":"b3:mine","files":[]}]}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// 'app' es un source_patch que depende (runtime) de wayland-protocol.
|
||||
let app_swm = "swm_version: 1\nbase:\n distro_version: dev\n pins: {}\nmutations:\n\
|
||||
- type: source_patch\n repo: git://x/app.git\n commit: abc\n target_bin: /usr/bin/app\n\
|
||||
\x20 build:\n compiler: zig-cc\n deps:\n runtime:\n - wayland-protocol\n";
|
||||
std::fs::write(repo.join("app-1.swm"), app_swm).unwrap();
|
||||
// wayland-protocol también está en el repo, a b3:stock (la versión contra la que app se hizo).
|
||||
let w_swm = "swm_version: 1\nbase:\n distro_version: dev\n pins: {}\nmutations: []\n";
|
||||
std::fs::write(repo.join("w-1.swm"), w_swm).unwrap();
|
||||
let index = r#"{"packages":[
|
||||
{"name":"app","version":"1","file":"app-1.swm","distro_version":"dev","deps":["wayland-protocol"],"expected_hash":"b3:app"},
|
||||
{"name":"wayland-protocol","version":"1","file":"w-1.swm","distro_version":"dev","deps":[],"expected_hash":"b3:stock"}
|
||||
]}"#;
|
||||
std::fs::write(repo.join("index.json"), index).unwrap();
|
||||
|
||||
let out = Command::new(env!("CARGO_BIN_EXE_hammer"))
|
||||
.args(["compat", "--repo", repo.to_str().unwrap(), "--db", db.to_str().unwrap()])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(out.status.success());
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
assert!(stdout.contains("incompatibles (1)"), "stdout: {stdout}");
|
||||
assert!(stdout.contains("app") && stdout.contains("deps divergidas"), "stdout: {stdout}");
|
||||
}
|
||||
|
||||
@@ -163,6 +163,76 @@ pub fn path_collisions(
|
||||
cols
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// H4e — `requires` OBSERVADOS: la versión contra la que el paquete se construyó
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// H4c observó lo que un paquete *escribe*; falta observar de qué *depende a una
|
||||
// versión* (el caso wayland automático). La fuente observable: un paquete se
|
||||
// construyó contra la versión de sus deps que hay EN EL REPO. Si el usuario tiene
|
||||
// esa dep instalada a OTRO hash, la diverguió ⇒ el paquete no encaja. No hace
|
||||
// falta que el autor prometa `requires`: se lee del cierre (deps del `.swm` +
|
||||
// `expected_hash` de cada dep en el índice). Es el análogo de sistema a un
|
||||
// requisito que no resuelve — el mismo rechazo duro de H4b, ahora derivado.
|
||||
|
||||
/// [H4e] Los `requires` OBSERVADOS de un paquete: por cada dep de **runtime**
|
||||
/// declarada en su `.swm`, la versión (`expected_hash`) que esa dep tiene **en el
|
||||
/// índice** — o sea, contra la que el paquete se construyó. Deps sin `expected_hash`
|
||||
/// en el índice se omiten (no hay versión observable que exigir).
|
||||
pub fn observed_requires(
|
||||
swm: &Swm,
|
||||
index: &crate::RepoIndex,
|
||||
) -> std::collections::BTreeMap<String, String> {
|
||||
let mut req = std::collections::BTreeMap::new();
|
||||
for m in &swm.mutations {
|
||||
if let Mutation::SourcePatch { deps, .. } = m {
|
||||
for dep in &deps.runtime {
|
||||
if let Some(h) = index.find(dep).and_then(|e| e.expected_hash.clone()) {
|
||||
req.insert(dep.clone(), h);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
req
|
||||
}
|
||||
|
||||
/// Un conflicto de versión observado: una dep de runtime está instalada a un hash
|
||||
/// **distinto** del que el paquete espera (el que hay en el repo). Es el caso
|
||||
/// *wayland* derivado: divergiste una superficie de la que este paquete depende.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ConflictoVersion {
|
||||
pub dep: String,
|
||||
/// El hash que el paquete espera (la versión del repo contra la que se construyó).
|
||||
pub requerido: String,
|
||||
/// El hash al que la dep está instalada AHORA (divergido).
|
||||
pub instalado: String,
|
||||
}
|
||||
|
||||
/// Conflictos de versión del entrante contra el estado instalado: por cada
|
||||
/// `requires` observado, si la dep está instalada a otro hash. Una dep que NO está
|
||||
/// instalada no cuenta acá (es un asunto de presencia, no de divergencia) — sólo
|
||||
/// pisar una versión distinta rechaza. Reusa lo que `InstalledDb` ya registra.
|
||||
pub fn version_conflicts(
|
||||
db: &crate::InstalledDb,
|
||||
requires: &std::collections::BTreeMap<String, String>,
|
||||
) -> Vec<ConflictoVersion> {
|
||||
let mut out = Vec::new();
|
||||
for (dep, requerido) in requires {
|
||||
if let Some(pkg) = db.find(dep) {
|
||||
if let Some(instalado) = &pkg.hash {
|
||||
if instalado != requerido {
|
||||
out.push(ConflictoVersion {
|
||||
dep: dep.clone(),
|
||||
requerido: requerido.clone(),
|
||||
instalado: instalado.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -258,6 +328,73 @@ mod tests {
|
||||
assert!(path_collisions(&db, "tema-oscuro", &paths).is_empty());
|
||||
}
|
||||
|
||||
// ── H4e: requires observados (versión de la dep contra la que se construyó) ──
|
||||
|
||||
#[test]
|
||||
fn wayland_divergido_observado_da_conflicto_de_version() {
|
||||
// El paquete 'app' depende (runtime) de 'wayland-protocol'. En el repo, wayland está a
|
||||
// b3:stock (la versión contra la que app se construyó).
|
||||
let swm = crate::Swm {
|
||||
swm_version: 1,
|
||||
base: crate::swm::Base { distro_version: "dev".into(), pins: Default::default() },
|
||||
mutations: vec![Mutation::SourcePatch {
|
||||
repo: Some("git://x/app.git".into()),
|
||||
commit: Some("abc".into()),
|
||||
tarball: None,
|
||||
sha256: None,
|
||||
strip_components: None,
|
||||
patch: None,
|
||||
patch_url: None,
|
||||
build: crate::swm::SwmBuild {
|
||||
compiler: "zig-cc".into(),
|
||||
target: "x86_64-linux-musl".into(),
|
||||
link: "static".into(),
|
||||
flags: vec![],
|
||||
phases: Default::default(),
|
||||
zig_version: None,
|
||||
},
|
||||
target_bin: "/usr/bin/app".into(),
|
||||
expected_hash: Some("b3:app".into()),
|
||||
deps: crate::recipe::Deps { build: vec![], runtime: vec!["wayland-protocol".into()] },
|
||||
evidence: Default::default(),
|
||||
slots: Default::default(),
|
||||
}],
|
||||
signature: None,
|
||||
};
|
||||
let index: crate::RepoIndex = serde_json::from_str(
|
||||
r#"{"packages":[{"name":"wayland-protocol","version":"1","file":"w.swm","distro_version":"dev","deps":[],"expected_hash":"b3:stock"}]}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let req = observed_requires(&swm, &index);
|
||||
assert_eq!(req.get("wayland-protocol").unwrap(), "b3:stock");
|
||||
|
||||
// Tengo wayland instalado a b3:mine (divergido) ⇒ conflicto.
|
||||
let mut db = crate::InstalledDb::default();
|
||||
db.record(crate::InstalledPackage {
|
||||
name: "wayland-protocol".into(),
|
||||
version: "1".into(),
|
||||
hash: Some("b3:mine".into()),
|
||||
files: vec![],
|
||||
claims: Default::default(),
|
||||
});
|
||||
let cs = version_conflicts(&db, &req);
|
||||
assert_eq!(cs.len(), 1);
|
||||
assert_eq!(cs[0].requerido, "b3:stock");
|
||||
assert_eq!(cs[0].instalado, "b3:mine");
|
||||
|
||||
// Si tengo la versión stock, no hay conflicto.
|
||||
let mut ok = crate::InstalledDb::default();
|
||||
ok.record(crate::InstalledPackage {
|
||||
name: "wayland-protocol".into(),
|
||||
version: "1".into(),
|
||||
hash: Some("b3:stock".into()),
|
||||
files: vec![],
|
||||
claims: Default::default(),
|
||||
});
|
||||
assert!(version_conflicts(&ok, &req).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_paths_lee_target_bin_y_file_drop() {
|
||||
let swm = crate::Swm {
|
||||
|
||||
@@ -344,9 +344,23 @@ La definición dura de compatibilidad de una config `C` contra mi estado `E`:
|
||||
colisión (de slot o de fichero) ⇒ elección, si no compatible. **Verificado e2e real**
|
||||
(`tests/compat_gate.rs`): un repo de dos paquetes se parte correctamente (uno choca de fichero
|
||||
con lo instalado ⇒ elección, otro limpio ⇒ compatible).
|
||||
Lo que queda abierto: **derivar la otra mitad** (los `requires` observados: de qué depende el
|
||||
paquete, no sólo qué escribe), subir el grano del path-slot a superficies más ricas (un módulo
|
||||
wasm de wawa, un componente), y el **colapso** con el lado proceso.
|
||||
- **H4e** ✅ — **`requires` OBSERVADOS: la versión contra la que el paquete se construyó.** Cierra
|
||||
la simetría de H4c: además de observar lo que un paquete *escribe*, observamos de qué *depende a
|
||||
una versión*. La fuente observable, sin que el autor prometa nada: un paquete se construyó contra
|
||||
la versión de sus deps que hay **en el repo** (`deps.runtime` del `.swm` + `expected_hash` de cada
|
||||
dep en el índice); si el usuario tiene esa dep instalada a **otro** hash, la divergió ⇒ rechazo
|
||||
duro. Es el **caso wayland derivado** — el mismo que H4b captura cuando el autor declara
|
||||
`requires`, ahora leído del cierre.
|
||||
- `compat::observed_requires(swm, index)` + `compat::version_conflicts(db, req)` (reusan `deps`
|
||||
del `.swm`, `expected_hash` del índice y `InstalledDb.hash` — cero declaración nueva).
|
||||
- Cableado en `install` (rechazo duro, no lo salva `--force-slots`: es un requisito, no una
|
||||
elección) y en `hammer compat` (bucket incompatibles).
|
||||
- **Verificado e2e real** (`tests/compat_gate.rs`): `hammer compat` marca `app` INCOMPATIBLE
|
||||
porque su dep `wayland-protocol` está instalada a un hash divergido del repo — read-only, ve
|
||||
el `source_patch` sin construirlo.
|
||||
Con H4e, la vía observada es simétrica: **lo que el paquete escribe** (H4c) *y* **de qué depende**
|
||||
(H4e), ambas sin declaración. Lo que queda abierto: subir el grano del path-slot a superficies
|
||||
más ricas (un módulo wasm de wawa, un componente) y el **colapso** con el lado proceso.
|
||||
|
||||
**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
|
||||
@@ -409,7 +423,8 @@ H3a (design-doc) ──► registrar la visión, barato
|
||||
└► H4b ✅ (slots en la receta/.swm real + gate en `hammer install`)
|
||||
└► H4c ✅ (superficies OBSERVADAS: colisión de fichero, sin declarar slots)
|
||||
└► H4d ✅ (`hammer compat <repo>`: la búsqueda que particiona un repo)
|
||||
└► [proceso] replay del MonotonicLog ──► plan OS-CRDT (otro agente)
|
||||
└► H4e ✅ (requires OBSERVADOS: dep divergida = incompatible)
|
||||
└► [proceso] replay del MonotonicLog ──► plan OS-CRDT (otro agente)
|
||||
```
|
||||
|
||||
Recomendación: **H1 primero** (empuja la frontera que ya tenemos, sin apuestas). **H2a** en
|
||||
|
||||
Reference in New Issue
Block a user