gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1471Shell 174Rust 105Markdown 953567 Sutichaykuna

⎇ d13af44ddb6bfc1554ea3bfee80ac130de44de4b crates/hammer-core/src/kernel/reverse.rs Rust · 328 siq'ikuna · 10.5 KB Ñawpa willay Pi imata llamkarqan ch'uya

kernel: modo reversa y catálogo de bundles — y las cuatro recetas apagan dos símbolos que ya no existen cf2cf5491 · Sergio ·

1//! **Modo reversa** (#9 del handoff): leer el kernel que ya corre a través del lente de bundles.
2//!
3//! Es el primer movimiento por una razón concreta: no compila nada, no arriesga nada, y **valida el
4//! catálogo contra la realidad antes de que exista el compilador**. Si el modo reversa muestra
5//! basura, el diseño de N1 está mal y se sabe por el precio de dos días en vez de dos meses.
6//!
7//! Da tres cosas que ninguna lista de símbolos da sola:
8//!
9//! 1. **Cuánto de cada bundle ya está aplicado** en el kernel que arrancó esta máquina.
10//! 2. **Si el hardware lo contradice** — y sólo en ese sentido (ver [`super::hw`]).
11//! 3. **Si el catálogo envejeció**: cada `select` que entra a la clausura y no está declarado en el
12//!    bundle es un símbolo que upstream agregó y nadie revisó. Es la mitad barata de la curación
13//!    del delta (§3 del handoff), y sale de comparar el grafo con el catálogo, sin IA.
14
15use std::collections::BTreeSet;
16
17use serde::{Deserialize, Serialize};
18
19use super::catalog::{Bundle, Catalog};
20use super::hw::Hardware;
21use super::kconfig::{KconfigTree, SelectLeak};
22use super::{ConfigValue, KernelConfig};
23
24/// Cuánto de un bundle está ya en efecto en el `.config` mirado.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "kebab-case")]
27pub enum BundleState {
28    /// Ni un símbolo de la clausura está encendido: el bundle ya rige.
29    Aplicado,
30    /// Algunos sí, algunos no.
31    Parcial,
32    /// Todo encendido: el kernel trae el subsistema entero.
33    Sin,
34}
35
36/// Qué dice el hardware sobre un bundle. **Sólo contradice, nunca poda.**
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "kebab-case", tag = "señal")]
39pub enum HwSignal {
40    /// El bundle no declara ninguna señal: no se puede decir nada.
41    SinSeñal,
42    /// Hay hardware presente que este bundle apagaría. Aplicarlo es una decisión, no un descuido.
43    Presente { evidencia: Vec<String> },
44    /// Ninguna de las señales declaradas está presente.
45    Ausente,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct BundleStatus {
50    pub id: String,
51    pub title: String,
52    /// Tamaño de la clausura del bundle en ESTE árbol de Kconfig.
53    pub closure_len: usize,
54    /// De la clausura, cuántos están `=y` en el config mirado.
55    pub on_builtin: usize,
56    /// …y cuántos `=m`.
57    pub on_module: usize,
58    pub state: BundleState,
59    pub hardware: HwSignal,
60    /// Símbolos que el bundle nombra y **no existen en este árbol**. Hoy un `-d` sobre uno de éstos
61    /// se pierde en silencio; acá se ve.
62    pub unknown_symbols: Vec<String>,
63    /// Fugas `select` que el catálogo no declara (ni cierra ni acepta): el bundle envejeció.
64    pub undeclared_leaks: Vec<SelectLeak>,
65}
66
67/// El informe entero del modo reversa.
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct ReverseReport {
70    /// Banner del `.config` leído, si lo trae.
71    pub kernel: Option<String>,
72    pub fingerprint: String,
73    pub config_symbols: usize,
74    pub bundles: Vec<BundleStatus>,
75    /// Bundles con `state != Aplicado` cuyo hardware está **ausente**: capacidad que este kernel
76    /// carga y esta máquina no usa. Es la frase del handoff §9 vuelta número.
77    pub applicable_savings: usize,
78    /// Bundles ya aplicados cuyo hardware SÍ está presente: la contradicción que hay que mirar.
79    pub contradictions: usize,
80}
81
82/// Corre el modo reversa. `hw` opcional: sin hardware el informe sigue valiendo, sólo que toda
83/// señal queda en `SinSeñal`.
84pub fn analyze(
85    catalog: &Catalog,
86    tree: &KconfigTree,
87    config: &KernelConfig,
88    hw: Option<&Hardware>,
89) -> ReverseReport {
90    let mut bundles = Vec::new();
91    for b in &catalog.bundles {
92        bundles.push(status_of(b, tree, config, hw));
93    }
94    let applicable_savings = bundles
95        .iter()
96        .filter(|s| s.state != BundleState::Aplicado && s.hardware == HwSignal::Ausente)
97        .count();
98    let contradictions = bundles
99        .iter()
100        .filter(|s| s.state == BundleState::Aplicado && matches!(s.hardware, HwSignal::Presente { .. }))
101        .count();
102    ReverseReport {
103        kernel: config.banner.clone(),
104        fingerprint: hw.map(|h| h.fingerprint().to_string()).unwrap_or_default(),
105        config_symbols: config.len(),
106        bundles,
107        applicable_savings,
108        contradictions,
109    }
110}
111
112fn status_of(
113    b: &Bundle,
114    tree: &KconfigTree,
115    config: &KernelConfig,
116    hw: Option<&Hardware>,
117) -> BundleStatus {
118    // Las raíces del bundle son lo que apaga MÁS las fugas que declaró cerrar.
119    let mut roots: Vec<String> = b.disable.clone();
120    roots.extend(b.close_leaks.iter().cloned());
121
122    let unknown_symbols: Vec<String> = roots
123        .iter()
124        .chain(b.enable.iter())
125        .filter(|s| !tree.symbols.contains_key(*s))
126        .cloned()
127        .collect();
128
129    let closure = tree.closure_off(&roots);
130    let mut on_builtin = 0usize;
131    let mut on_module = 0usize;
132    for s in &closure {
133        match config.get(s) {
134            Some(ConfigValue::Yes) => on_builtin += 1,
135            Some(ConfigValue::Module) => on_module += 1,
136            _ => {}
137        }
138    }
139    let on = on_builtin + on_module;
140    let state = if on == 0 {
141        BundleState::Aplicado
142    } else if on < closure.len() {
143        BundleState::Parcial
144    } else {
145        BundleState::Sin
146    };
147
148    // Fugas que el catálogo no contempla: ni las cerró (estarían dentro de la clausura) ni las
149    // aceptó por escrito.
150    let aceptadas: BTreeSet<&str> = b.accept_leaks.keys().map(|s| s.as_str()).collect();
151    let undeclared_leaks: Vec<SelectLeak> = tree
152        .select_leaks(&closure)
153        .into_iter()
154        .filter(|l| !aceptadas.contains(l.target.as_str()))
155        .collect();
156
157    BundleStatus {
158        id: b.id.clone(),
159        title: b.title.clone(),
160        closure_len: closure.len(),
161        on_builtin,
162        on_module,
163        state,
164        hardware: hw_signal(b, hw),
165        unknown_symbols,
166        undeclared_leaks,
167    }
168}
169
170fn hw_signal(b: &Bundle, hw: Option<&Hardware>) -> HwSignal {
171    let c = &b.contradicted_by;
172    if c.is_empty() {
173        return HwSignal::SinSeñal;
174    }
175    let Some(hw) = hw else {
176        return HwSignal::SinSeñal;
177    };
178    let mut ev = Vec::new();
179    for p in &c.pci_class {
180        for d in hw.pci.iter().filter(|d| d.class.starts_with(p)) {
181            ev.push(format!(
182                "pci {}:{} clase {} ({})",
183                d.vendor,
184                d.device,
185                d.class,
186                d.driver.as_deref().unwrap_or("sin driver")
187            ));
188        }
189    }
190    for d in &c.driver {
191        if hw.has_driver(d) {
192            ev.push(format!("driver {d} con dispositivo bindeado"));
193        }
194    }
195    for f in &c.cpu_flag {
196        if hw.has_cpu_flag(f) {
197            ev.push(format!("cpu flag {f}"));
198        }
199    }
200    if ev.is_empty() {
201        HwSignal::Ausente
202    } else {
203        HwSignal::Presente { evidencia: ev }
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210    use std::collections::BTreeMap;
211
212    fn escenario() -> (tempfile::TempDir, KconfigTree, Catalog) {
213        let d = tempfile::tempdir().unwrap();
214        std::fs::write(
215            d.path().join("Kconfig"),
216            r#"
217config WIRELESS
218	bool "Wireless"
219
220config CFG80211
221	tristate "cfg80211"
222	depends on WIRELESS
223
224config WLAN
225	bool "WLAN"
226	select WIRELESS
227
228config SOUND
229	bool "Sound"
230"#,
231        )
232        .unwrap();
233        let tree = KconfigTree::parse(d.path(), &BTreeMap::new()).unwrap();
234        let cat: Catalog = toml::from_str(
235            r#"
236version = 1
237[[bundle]]
238id = "sin-wifi"
239title = "No necesito wifi"
240disable = ["WIRELESS"]
241close_leaks = ["WLAN"]
242[bundle.contradicted_by]
243pci_class = ["0280"]
244
245[[bundle]]
246id = "sin-audio"
247title = "No necesito audio"
248disable = ["SOUND", "SND_QUE_NO_EXISTE"]
249"#,
250        )
251        .unwrap();
252        (d, tree, cat)
253    }
254
255    #[test]
256    fn cuenta_lo_aplicado_y_lo_que_falta() {
257        let (_d, tree, cat) = escenario();
258        let cfg = KernelConfig::parse(
259            "CONFIG_WIRELESS=y\nCONFIG_CFG80211=m\nCONFIG_WLAN=y\n# CONFIG_SOUND is not set\n",
260        );
261        let r = analyze(&cat, &tree, &cfg, None);
262        let wifi = &r.bundles[0];
263        assert_eq!(wifi.closure_len, 3, "WIRELESS + CFG80211 + WLAN");
264        assert_eq!(wifi.on_builtin, 2);
265        assert_eq!(wifi.on_module, 1);
266        assert_eq!(wifi.state, BundleState::Sin);
267
268        let audio = &r.bundles[1];
269        assert_eq!(audio.state, BundleState::Aplicado);
270        // El símbolo inventado se ve en vez de perderse en silencio.
271        assert_eq!(audio.unknown_symbols, vec!["SND_QUE_NO_EXISTE"]);
272    }
273
274    #[test]
275    fn el_hardware_contradice_pero_no_poda() {
276        let (_d, tree, cat) = escenario();
277        let cfg = KernelConfig::parse("# CONFIG_WIRELESS is not set\n# CONFIG_WLAN is not set\n");
278        let mut hw = Hardware::default();
279        hw.pci.push(super::super::hw::PciDevice {
280            slot: "0000:00:14.3".into(),
281            vendor: "8086".into(),
282            device: "a0f0".into(),
283            class: "028000".into(),
284            driver: Some("iwlwifi".into()),
285        });
286        let r = analyze(&cat, &tree, &cfg, Some(&hw));
287        let wifi = &r.bundles[0];
288        assert_eq!(wifi.state, BundleState::Aplicado);
289        assert!(matches!(wifi.hardware, HwSignal::Presente { .. }));
290        // Aplicado + hardware presente = la contradicción que hay que mirar, no un error.
291        assert_eq!(r.contradictions, 1);
292    }
293
294    #[test]
295    fn el_ahorro_es_bundle_no_aplicado_con_hardware_ausente() {
296        let (_d, tree, cat) = escenario();
297        let cfg = KernelConfig::parse("CONFIG_WIRELESS=y\nCONFIG_CFG80211=y\nCONFIG_WLAN=y\n");
298        let hw = Hardware::default(); // máquina sin PCI wifi
299        let r = analyze(&cat, &tree, &cfg, Some(&hw));
300        assert_eq!(r.bundles[0].hardware, HwSignal::Ausente);
301        assert_eq!(r.applicable_savings, 1);
302    }
303
304    #[test]
305    fn una_fuga_select_no_declarada_se_reporta() {
306        let (_d, tree, _cat) = escenario();
307        // Un catálogo que NO cierra la fuga WLAN→WIRELESS: es el bundle envejecido.
308        let cat: Catalog = toml::from_str(
309            r#"
310version = 1
311[[bundle]]
312id = "sin-wifi"
313title = "No necesito wifi"
314disable = ["WIRELESS"]
315"#,
316        )
317        .unwrap();
318        let cfg = KernelConfig::parse("CONFIG_WIRELESS=y\n");
319        let r = analyze(&cat, &tree, &cfg, None);
320        assert_eq!(
321            r.bundles[0].undeclared_leaks,
322            vec![SelectLeak {
323                selector: "WLAN".into(),
324                target: "WIRELESS".into()
325            }]
326        );
327    }
328}