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…
⎇ d13af44ddb6bfc1554ea3bfee80ac130de44de4b
crates/hammer-core/src/kernel/reverse.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
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}