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.
|
// H4b — vía DECLARADA: las superficies (`slots`) que el autor declara reclamar/requerir.
|
||||||
let entrant_slots = source_patch_slots(&entrant_swm);
|
let entrant_slots = source_patch_slots(&entrant_swm);
|
||||||
if !entrant_slots.is_empty() {
|
if !entrant_slots.is_empty() {
|
||||||
@@ -1999,7 +2019,15 @@ fn run_compat(repo: &str, db_path: &std::path::Path) -> anyhow::Result<()> {
|
|||||||
continue;
|
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 slots = source_patch_slots(&swm);
|
||||||
let veredicto = hammer_core::compat::evaluar(&estado, &slots);
|
let veredicto = hammer_core::compat::evaluar(&estado, &slots);
|
||||||
if let hammer_core::CompatVeredicto::Incompatible { slot, requerido, presente } = &veredicto {
|
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("requieren elección (1)"), "stdout: {stdout}");
|
||||||
assert!(stdout.contains("tema-oscuro"), "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
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -258,6 +328,73 @@ mod tests {
|
|||||||
assert!(path_collisions(&db, "tema-oscuro", &paths).is_empty());
|
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]
|
#[test]
|
||||||
fn output_paths_lee_target_bin_y_file_drop() {
|
fn output_paths_lee_target_bin_y_file_drop() {
|
||||||
let swm = crate::Swm {
|
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**
|
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
|
(`tests/compat_gate.rs`): un repo de dos paquetes se parte correctamente (uno choca de fichero
|
||||||
con lo instalado ⇒ elección, otro limpio ⇒ compatible).
|
con lo instalado ⇒ elección, otro limpio ⇒ compatible).
|
||||||
Lo que queda abierto: **derivar la otra mitad** (los `requires` observados: de qué depende el
|
- **H4e** ✅ — **`requires` OBSERVADOS: la versión contra la que el paquete se construyó.** Cierra
|
||||||
paquete, no sólo qué escribe), subir el grano del path-slot a superficies más ricas (un módulo
|
la simetría de H4c: además de observar lo que un paquete *escribe*, observamos de qué *depende a
|
||||||
wasm de wawa, un componente), y el **colapso** con el lado proceso.
|
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**
|
**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
|
||||||
@@ -409,7 +423,8 @@ H3a (design-doc) ──► registrar la visión, barato
|
|||||||
└► H4b ✅ (slots en la receta/.swm real + gate en `hammer install`)
|
└► H4b ✅ (slots en la receta/.swm real + gate en `hammer install`)
|
||||||
└► H4c ✅ (superficies OBSERVADAS: colisión de fichero, sin declarar slots)
|
└► H4c ✅ (superficies OBSERVADAS: colisión de fichero, sin declarar slots)
|
||||||
└► H4d ✅ (`hammer compat <repo>`: la búsqueda que particiona un repo)
|
└► 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
|
Recomendación: **H1 primero** (empuja la frontera que ya tenemos, sin apuestas). **H2a** en
|
||||||
|
|||||||
Reference in New Issue
Block a user