tawasuyu

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 1457Shell 147Markdown 90Rust 853012 Definitions

⎇ main crates/takana-core/src/kernel/catalog.rs Rust · 293 lines · 10.7 KB History Who touched what raw

takana etapa 5d: comentarios de crates, CLAUDE.md y el skill de granja b818f5249 · Sergio ·

1//! El catálogo de bundles (N1) y perillas (N2).
2//!
3//! ## Por qué un bundle no es una lista de símbolos
4//! Escrito como lista, «no necesito wifi» son ~400 `CONFIG_*` que envejecen en cada release y
5//! exigen un curador humano para siempre (handoff §2.1). Escrito como **predicado sobre el grafo**
6//! —una raíz y su clausura— son dos líneas y se auto-cura: el driver que entra en 6.17 cae dentro
7//! sin que nadie lo toque.
8//!
9//! Lo que la medición del SDD 22 §9 agregó a esa idea: **la clausura sola no cierra**. `select`
10//! fuerza símbolos ignorando sus `depends on`, así que un bundle honesto declara además qué hace
11//! con cada fuga — cerrarla (`close_leaks`) o aceptarla a sabiendas (`accept_leaks`). Esa revisión
12//! se hace **una vez por bundle**, no una vez por release.
13//!
14//! ## Por qué las perillas N2 llevan un campo `side`
15//! «La mitad de N2 no es Kconfig sino variables de receta» (handoff §5). Con el hecho del §1 del
16//! SDD 22 en la mano —el config ES la identidad del artefacto— eso es más grave de lo que parece:
17//! las dos mitades mueven el hash igual, pero se aplican en fases distintas y fallan distinto. Una
18//! UI que no sepa de qué lado cae cada perilla prometerá diffs que no puede explicar.
19
20use std::collections::BTreeMap;
21use std::path::Path;
22
23use serde::{Deserialize, Serialize};
24
25/// Formato del fichero de catálogo. Se versiona para poder migrarlo sin adivinar.
26pub const CATALOG_VERSION: u32 = 1;
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct Catalog {
30    pub version: u32,
31    /// Kernel contra el que se revisaron las fugas. Cambiar de versión no invalida el catálogo,
32    /// pero sí obliga a re-mirar las fugas (`takana kernel closure`).
33    #[serde(default)]
34    pub reviewed_against: Option<String>,
35    #[serde(default, rename = "bundle")]
36    pub bundles: Vec<Bundle>,
37    #[serde(default, rename = "knob")]
38    pub knobs: Vec<Knob>,
39    #[serde(default, rename = "objective")]
40    pub objectives: Vec<Objective>,
41}
42
43/// Para qué es este kernel. **El gate de no-regresión no corre sin uno.**
44///
45/// `recipes/linux.toml` apaga USB, HID e INPUT a propósito — es el kernel de QEMU con consola
46/// serie. Un gate global «todo dispositivo en uso debe seguir teniendo driver» rechazaría esa
47/// receta, que está sana. El objetivo declara qué pérdidas son legítimas para este uso.
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct Objective {
50    pub id: String,
51    pub title: String,
52    #[serde(default)]
53    pub help: Option<String>,
54    /// Bundles cuyas pérdidas de driver este objetivo autoriza. Todo lo demás bloquea.
55    #[serde(default)]
56    pub allow_bundles: Vec<String>,
57}
58
59/// Un bundle de nivel 1: una aserción del dueño de la máquina («no necesito wifi»).
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct Bundle {
62    /// Identificador estable, en kebab-case. Es lo que la UI y `plan` se pasan.
63    pub id: String,
64    /// Título en castellano, para el humano.
65    pub title: String,
66    #[serde(default)]
67    pub help: Option<String>,
68    /// **El predicado**: apagar estas raíces y, con ellas, todo lo que dependa duro de ellas.
69    #[serde(default)]
70    pub disable: Vec<String>,
71    /// Encender estos símbolos (bundles «positivos», raros en N1 pero necesarios en N2).
72    #[serde(default)]
73    pub enable: Vec<String>,
74    /// Fugas `select` que se cierran también, revisadas a mano. Cada entrada es un símbolo de
75    /// FUERA de la clausura que reencendería algo de dentro.
76    #[serde(default)]
77    pub close_leaks: Vec<String>,
78    /// Fugas que se dejan abiertas a sabiendas, con su motivo. Clave = símbolo destino.
79    #[serde(default)]
80    pub accept_leaks: BTreeMap<String, String>,
81    /// Detección de hardware, **en un solo sentido**: sirve para CONTRADECIR al usuario
82    /// («marcaste "no necesito wifi" y tenés un AX211 activo ahora mismo»), nunca para podar solo.
83    /// Un kernel podado por autodetección es cómo se fabrica un ladrillo (handoff §6).
84    #[serde(default)]
85    pub contradicted_by: Contradiction,
86    #[serde(default)]
87    pub notes: Option<String>,
88}
89
90/// Señales de hardware que contradicen a un bundle si están presentes AHORA.
91#[derive(Debug, Clone, Default, Serialize, Deserialize)]
92pub struct Contradiction {
93    /// Clases PCI, como los 6 dígitos hex de `/sys/bus/pci/devices/*/class`, o un prefijo
94    /// (`"0280"` = controlador de red inalámbrica).
95    #[serde(default)]
96    pub pci_class: Vec<String>,
97    /// Drivers con dispositivo bindeado ahora mismo (`/sys/bus/*/devices/*/driver`).
98    #[serde(default)]
99    pub driver: Vec<String>,
100    /// Flags de CPU de `/proc/cpuinfo`.
101    #[serde(default)]
102    pub cpu_flag: Vec<String>,
103}
104
105impl Contradiction {
106    pub fn is_empty(&self) -> bool {
107        self.pci_class.is_empty() && self.driver.is_empty() && self.cpu_flag.is_empty()
108    }
109}
110
111/// De qué lado del build cae una perilla de nivel 2.
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
113#[serde(rename_all = "lowercase")]
114pub enum Side {
115    /// Símbolos de Kconfig: se aplican en la fase `configure`.
116    Kconfig,
117    /// Variables de la receta (LTO, `-march`, `zig_version`): otra fase, otro modo de fallo.
118    /// Mueven el `ArtifactHash` igual que los símbolos.
119    Recipe,
120}
121
122/// Una perilla de nivel 2: política y rendimiento.
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct Knob {
125    pub id: String,
126    pub title: String,
127    #[serde(default)]
128    pub help: Option<String>,
129    /// La mitad de N2 no es Kconfig. Este campo obliga a decirlo.
130    pub side: Side,
131    #[serde(default)]
132    pub enable: Vec<String>,
133    #[serde(default)]
134    pub disable: Vec<String>,
135    /// Sólo con `side = "recipe"`: qué campo de la receta toca, en texto, para que la UI pueda
136    /// explicar el diff aunque `plan` todavía no sepa aplicarlo.
137    #[serde(default)]
138    pub recipe_field: Option<String>,
139    #[serde(default)]
140    pub notes: Option<String>,
141}
142
143impl Catalog {
144    pub fn load(path: &Path) -> crate::Result<Catalog> {
145        let text = std::fs::read_to_string(path)?;
146        let cat: Catalog = toml::from_str(&text)
147            .map_err(|e| crate::Error::Recipe(format!("{}: {e}", path.display())))?;
148        cat.validate(path)?;
149        Ok(cat)
150    }
151
152    fn validate(&self, path: &Path) -> crate::Result<()> {
153        let p = path.display();
154        if self.version != CATALOG_VERSION {
155            return Err(crate::Error::Recipe(format!(
156                "{p}: version = {}, esperaba {CATALOG_VERSION}",
157                self.version
158            )));
159        }
160        let mut ids = std::collections::BTreeSet::new();
161        for b in &self.bundles {
162            if !ids.insert(("bundle", b.id.as_str())) {
163                return Err(crate::Error::Recipe(format!("{p}: bundle duplicado «{}»", b.id)));
164            }
165            if b.disable.is_empty() && b.enable.is_empty() {
166                return Err(crate::Error::Recipe(format!(
167                    "{p}: el bundle «{}» no apaga ni enciende nada",
168                    b.id
169                )));
170            }
171        }
172        for k in &self.knobs {
173            if !ids.insert(("knob", k.id.as_str())) {
174                return Err(crate::Error::Recipe(format!("{p}: perilla duplicada «{}»", k.id)));
175            }
176            // Una perilla de receta que no dice qué campo toca es exactamente el diff que la UI no
177            // va a poder explicar.
178            if k.side == Side::Recipe && k.recipe_field.is_none() {
179                return Err(crate::Error::Recipe(format!(
180                    "{p}: la perilla «{}» es side=recipe y no declara recipe_field",
181                    k.id
182                )));
183            }
184            if k.side == Side::Kconfig && k.enable.is_empty() && k.disable.is_empty() {
185                return Err(crate::Error::Recipe(format!(
186                    "{p}: la perilla «{}» es side=kconfig y no toca ningún símbolo",
187                    k.id
188                )));
189            }
190        }
191        let conocidos: std::collections::BTreeSet<&str> =
192            self.bundles.iter().map(|b| b.id.as_str()).collect();
193        for o in &self.objectives {
194            if !ids.insert(("objective", o.id.as_str())) {
195                return Err(crate::Error::Recipe(format!("{p}: objetivo duplicado «{}»", o.id)));
196            }
197            // Un `allow_bundles` con un id mal escrito autorizaría NADA y el gate bloquearía sin
198            // que se entienda por qué. Se rechaza al cargar, no en el portón.
199            for b in &o.allow_bundles {
200                if !conocidos.contains(b.as_str()) {
201                    return Err(crate::Error::Recipe(format!(
202                        "{p}: el objetivo «{}» autoriza el bundle «{b}», que no existe",
203                        o.id
204                    )));
205                }
206            }
207        }
208        Ok(())
209    }
210
211    pub fn bundle(&self, id: &str) -> Option<&Bundle> {
212        self.bundles.iter().find(|b| b.id == id)
213    }
214
215    pub fn knob(&self, id: &str) -> Option<&Knob> {
216        self.knobs.iter().find(|k| k.id == id)
217    }
218
219    pub fn objective(&self, id: &str) -> Option<&Objective> {
220        self.objectives.iter().find(|o| o.id == id)
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    fn escribir(s: &str) -> (tempfile::TempDir, std::path::PathBuf) {
229        let d = tempfile::tempdir().unwrap();
230        let p = d.path().join("bundles.toml");
231        std::fs::write(&p, s).unwrap();
232        (d, p)
233    }
234
235    #[test]
236    fn carga_y_resuelve_por_id() {
237        let (_d, p) = escribir(
238            r#"
239version = 1
240[[bundle]]
241id = "sin-wifi"
242title = "No necesito wifi"
243disable = ["WIRELESS"]
244close_leaks = ["WLAN"]
245[bundle.accept_leaks]
246MAC80211_LEDS = "lo deja encendido IWLEGACY, que igual no se compila"
247[bundle.contradicted_by]
248pci_class = ["0280"]
249"#,
250        );
251        let c = Catalog::load(&p).unwrap();
252        let b = c.bundle("sin-wifi").unwrap();
253        assert_eq!(b.disable, vec!["WIRELESS"]);
254        assert_eq!(b.close_leaks, vec!["WLAN"]);
255        assert_eq!(b.accept_leaks.len(), 1);
256        assert_eq!(b.contradicted_by.pci_class, vec!["0280"]);
257    }
258
259    #[test]
260    fn perilla_de_receta_sin_campo_es_error() {
261        let (_d, p) = escribir(
262            r#"
263version = 1
264[[knob]]
265id = "lto"
266title = "LTO"
267side = "recipe"
268"#,
269        );
270        // Sin `recipe_field` la UI prometería un diff que no puede explicar (handoff §5).
271        let e = Catalog::load(&p).unwrap_err().to_string();
272        assert!(e.contains("recipe_field"), "{e}");
273    }
274
275    #[test]
276    fn bundle_vacio_es_error() {
277        let (_d, p) = escribir(
278            r#"
279version = 1
280[[bundle]]
281id = "nada"
282title = "Nada"
283"#,
284        );
285        assert!(Catalog::load(&p).is_err());
286    }
287
288    #[test]
289    fn version_ajena_es_error() {
290        let (_d, p) = escribir("version = 99\n");
291        assert!(Catalog::load(&p).is_err());
292    }
293}