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…
⎇ ca9bde0251d05fbfd34c93033d1aff2eb2ee62b5
crates/hammer-core/src/kernel/plan.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
kernel: la clausura contrastada contra un olddefconfig de verdad — 65/68 con 0 falsos positivos
1//! `plan` — de una selección de bundles a una **receta derivada**.
2//!
3//! ## Por qué una receta y no un parámetro
4//! Las fases de build entran en `Recipe::hash_inputs` y el config del kernel vive en la fase
5//! `configure` ⇒ **el config ES la identidad del artefacto** (SDD 22 §1). No hay forma de tener un
6//! config variable sin generar recetas: una «perilla» de la UI no es un parámetro de runtime, es
7//! una edición de receta. Fingir lo contrario daría un store que dice que todo está al día mientras
8//! los artefactos ya no corresponden — en un sistema direccionado por contenido, el peor fallo
9//! posible no es fallar, es **mentir**.
10//!
11//! De ahí sale gratis lo que §7 del handoff pedía: el plan **determina** el artefacto, así que el
12//! JSON lleva su `ArtifactHash` y la UI puede decir «esto ya está construido y firmado» sin
13//! construir nada.
14//!
15//! ## Qué se emite y qué no
16//! Se emiten **raíces**, no clausuras. `-d WIRELESS -d WLAN` son dos banderas; los ~400 símbolos
17//! que caen con ellas los calcula el `olddefconfig` **del propio kernel**. hammer sabe cuáles son
18//! (para poder explicarlos), pero no los escribe: la app nunca escribe un `.config`.
19//!
20//! ## Procedencia por símbolo
21//! Cada símbolo emitido carga quién lo pidió ([`PlannedSymbol::from`]). Es el #7 del handoff, y con
22//! él el diff entre dos generaciones deja de ser «347 símbolos cambiaron» para ser «activaste
23//! bluetooth y entraron 12 símbolos».
24
25use std::collections::BTreeMap;
26
27use serde::{Deserialize, Serialize};
28
29use super::catalog::{Catalog, Side};
30use super::kconfig::KconfigTree;
31use crate::recipe::Recipe;
32
33/// Qué se le pide a `scripts/config` para un símbolo.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "lowercase")]
36pub enum Action {
37 Enable,
38 Disable,
39}
40
41impl Action {
42 fn flag(self) -> &'static str {
43 match self {
44 Action::Enable => "-e",
45 Action::Disable => "-d",
46 }
47 }
48}
49
50/// Un símbolo emitido, con su procedencia.
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct PlannedSymbol {
53 pub symbol: String,
54 pub action: Action,
55 /// `bundle:sin-wifi`, `knob:tick-1000`.
56 pub from: String,
57 /// Por qué está: raíz declarada, o fuga `select` que hubo que cerrar.
58 pub reason: String,
59}
60
61/// Una perilla que **no** cae del lado de Kconfig: el plan la declara y no la aplica.
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct RecipeKnob {
64 pub id: String,
65 pub title: String,
66 pub recipe_field: String,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct Plan {
71 pub base_recipe: String,
72 pub base_name: String,
73 pub derived_name: String,
74 pub bundles: Vec<String>,
75 pub knobs: Vec<String>,
76 /// Símbolos que se emiten, con procedencia.
77 pub symbols: Vec<PlannedSymbol>,
78 /// **Diff-back estático**: pedidos que no existen en este árbol. Un `-d` sobre uno de éstos es
79 /// un no-op que nadie ve. Le pasaba a `THUNDERBOLT` y a `REISERFS_FS` en las cuatro recetas de
80 /// kernel del repo hasta que se corrigieron (`2602218`), que es cómo se descubrió el patrón.
81 pub unknown_symbols: Vec<PlannedSymbol>,
82 /// Perillas `side = "recipe"` seleccionadas: declaradas, no aplicadas.
83 pub recipe_side: Vec<RecipeKnob>,
84 /// Cuántos símbolos apaga de verdad la selección, contando la clausura. Es informativo: quien
85 /// la calcula de vuelta, y manda, es `olddefconfig`.
86 pub closure_size: usize,
87 /// La invocación de `scripts/config` que se inserta.
88 pub fragment: String,
89 /// La fase `configure` derivada, entera.
90 pub configure: String,
91 /// El `ArtifactHash` de la receta derivada. `None` si no se pudo calcular (deps sin resolver).
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub artifact_hash: Option<String>,
94}
95
96/// Construye el plan. No toca el disco.
97pub fn make_plan(
98 base: &Recipe,
99 base_path: &str,
100 cat: &Catalog,
101 tree: &KconfigTree,
102 bundle_ids: &[String],
103 knob_ids: &[String],
104) -> crate::Result<Plan> {
105 let mut wanted: BTreeMap<String, PlannedSymbol> = BTreeMap::new();
106 let mut disable_roots: Vec<String> = Vec::new();
107
108 let push = |wanted: &mut BTreeMap<String, PlannedSymbol>,
109 sym: &str,
110 action: Action,
111 from: String,
112 reason: &str|
113 -> crate::Result<()> {
114 // Dos selecciones que se contradicen sobre el mismo símbolo no se resuelven por orden de
115 // aparición: se rechazan. El orden sería una respuesta plausible y arbitraria.
116 if let Some(prev) = wanted.get(sym) {
117 if prev.action != action {
118 return Err(crate::Error::Recipe(format!(
119 "conflicto en {sym}: «{}» lo quiere {:?} y «{from}» lo quiere {:?}",
120 prev.from, prev.action, action
121 )));
122 }
123 return Ok(());
124 }
125 wanted.insert(
126 sym.to_string(),
127 PlannedSymbol {
128 symbol: sym.to_string(),
129 action,
130 from,
131 reason: reason.to_string(),
132 },
133 );
134 Ok(())
135 };
136
137 for id in bundle_ids {
138 let b = cat.bundle(id).ok_or_else(|| {
139 crate::Error::Recipe(format!("no existe el bundle «{id}» en el catálogo"))
140 })?;
141 let from = format!("bundle:{id}");
142 for s in &b.disable {
143 push(&mut wanted, s, Action::Disable, from.clone(), "raíz del bundle")?;
144 disable_roots.push(s.clone());
145 }
146 for s in &b.close_leaks {
147 push(
148 &mut wanted,
149 s,
150 Action::Disable,
151 from.clone(),
152 "fuga `select` cerrada: sin esto el olddefconfig lo reenciende",
153 )?;
154 disable_roots.push(s.clone());
155 }
156 for s in &b.enable {
157 push(&mut wanted, s, Action::Enable, from.clone(), "raíz del bundle")?;
158 }
159 }
160
161 let mut recipe_side = Vec::new();
162 for id in knob_ids {
163 let k = cat.knob(id).ok_or_else(|| {
164 crate::Error::Recipe(format!("no existe la perilla «{id}» en el catálogo"))
165 })?;
166 let from = format!("knob:{id}");
167 if k.side == Side::Recipe {
168 recipe_side.push(RecipeKnob {
169 id: k.id.clone(),
170 title: k.title.clone(),
171 recipe_field: k.recipe_field.clone().unwrap_or_default(),
172 });
173 continue;
174 }
175 for s in &k.enable {
176 push(&mut wanted, s, Action::Enable, from.clone(), "perilla")?;
177 }
178 for s in &k.disable {
179 push(&mut wanted, s, Action::Disable, from.clone(), "perilla")?;
180 disable_roots.push(s.clone());
181 }
182 }
183
184 // Un `enable` que cae DENTRO de lo que otro bundle apaga es un conflicto que el símbolo solo no
185 // delata: `olddefconfig` lo descartaría en silencio por dependencia insatisfecha.
186 let closure = tree.closure_off(&disable_roots);
187 for p in wanted.values() {
188 if p.action == Action::Enable && closure.contains(&p.symbol) {
189 return Err(crate::Error::Recipe(format!(
190 "conflicto: «{}» quiere encender {} pero cae dentro de lo que apaga la selección \
191 (el olddefconfig lo descartaría sin decir nada)",
192 p.from, p.symbol
193 )));
194 }
195 }
196
197 let (known, unknown_symbols): (Vec<_>, Vec<_>) = wanted
198 .into_values()
199 .partition(|p| tree.symbols.contains_key(&p.symbol));
200
201 let arch = detect_arch(base);
202 let fragment = render_fragment(&known);
203 let base_configure = base
204 .build
205 .phases
206 .configure
207 .as_deref()
208 .ok_or_else(|| {
209 crate::Error::Recipe(format!(
210 "la receta base «{}» no tiene fase `configure`: no hay dónde insertar el config",
211 base.name
212 ))
213 })?
214 .trim_end()
215 .trim_end_matches('\\')
216 .trim_end()
217 .to_string();
218
219 // Se AÑADE una segunda ronda en vez de reescribir la fase base: no hay que parsear el shell de
220 // nadie, la base sigue siendo literalmente la de siempre en el diff, y `olddefconfig` es
221 // idempotente. Lo que la app aporta es un fragmento; resolver sigue siendo del kernel.
222 let configure = format!("{base_configure} && \\\n{fragment} && \\\nmake ARCH={arch} olddefconfig\n");
223
224 let derived_name = derive_name(&base.name, bundle_ids, knob_ids);
225
226 Ok(Plan {
227 base_recipe: base_path.to_string(),
228 base_name: base.name.clone(),
229 derived_name,
230 bundles: bundle_ids.to_vec(),
231 knobs: knob_ids.to_vec(),
232 symbols: known,
233 unknown_symbols,
234 recipe_side,
235 closure_size: closure.len(),
236 fragment,
237 configure,
238 artifact_hash: None,
239 })
240}
241
242/// La receta derivada: la base con la fase `configure` del plan y un nombre propio.
243///
244/// Conserva el `base_dir` de la base a propósito — es lo que hace resolubles sus `deps.build` y,
245/// con ellas, calculable el `ArtifactHash`.
246pub fn derive_recipe(base: &Recipe, plan: &Plan) -> Recipe {
247 let mut r = base.clone();
248 r.name = plan.derived_name.clone();
249 r.build.phases.configure = Some(plan.configure.clone());
250 r
251}
252
253/// Serializa la receta derivada a TOML. Vive acá y no en la CLI porque el formato de receta es del
254/// core: quien lo cambie tiene que cambiarlo en un solo sitio.
255pub fn render_recipe_toml(r: &Recipe) -> crate::Result<String> {
256 toml::to_string_pretty(r).map_err(|e| crate::Error::Serde(e.to_string()))
257}
258
259fn detect_arch(base: &Recipe) -> String {
260 let hay = base
261 .build
262 .phases
263 .configure
264 .as_deref()
265 .unwrap_or_default();
266 for tok in hay.split_whitespace() {
267 if let Some(v) = tok.strip_prefix("ARCH=") {
268 if !v.is_empty() {
269 return v.to_string();
270 }
271 }
272 }
273 "x86_64".to_string()
274}
275
276/// `scripts/config -d A -d B -e C …`, en líneas de ancho razonable y **orden estable**: el texto
277/// entra al hash, así que un orden que dependiera del recorrido daría dos hashes para el mismo plan.
278fn render_fragment(syms: &[PlannedSymbol]) -> String {
279 let mut out = String::from("scripts/config");
280 let mut col = out.len();
281 for p in syms {
282 let pieza = format!(" {} {}", p.action.flag(), p.symbol);
283 if col + pieza.len() > 96 {
284 out.push_str(" \\\n ");
285 col = 1;
286 }
287 col += pieza.len();
288 out.push_str(&pieza);
289 }
290 out
291}
292
293/// Nombre de la receta derivada. Lleva un hash corto de la SELECCIÓN, no del contenido: dos planes
294/// con los mismos bundles dan el mismo nombre, y el `ArtifactHash` sigue siendo quien manda.
295fn derive_name(base: &str, bundles: &[String], knobs: &[String]) -> String {
296 let mut material = String::new();
297 let mut b: Vec<&String> = bundles.iter().collect();
298 b.sort();
299 for x in b {
300 material.push_str("bundle:");
301 material.push_str(x);
302 material.push('\n');
303 }
304 let mut k: Vec<&String> = knobs.iter().collect();
305 k.sort();
306 for x in k {
307 material.push_str("knob:");
308 material.push_str(x);
309 material.push('\n');
310 }
311 let h = crate::hash::ArtifactHash::of_bytes(material.as_bytes());
312 let hex = h.as_str().trim_start_matches("b3:");
313 format!("{base}-plan-{}", &hex[..8])
314}
315
316/// **Diff-back** (§6 del handoff, la mitad que faltaba): ¿sobrevivió cada símbolo pedido al
317/// `olddefconfig`?
318///
319/// Sin esto la UI miente: un `-e FOO` cuya dependencia no se cumple se pierde en silencio.
320#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct DiffBack {
322 pub honored: Vec<String>,
323 /// Pedidos que el `.config` resultante contradice. **Cada uno es una promesa incumplida.**
324 pub violated: Vec<Violation>,
325 /// Apagados que el `.config` ni menciona. **Esto es éxito, no fallo**: Kconfig no emite un
326 /// símbolo cuyas dependencias no se cumplen, así que un `-d` sobre algo que ya cayó con su
327 /// raíz simplemente no aparece. Contarlo como incumplido —el error que cometí antes de correr
328 /// un `olddefconfig` de verdad— hace que un plan perfecto se reporte como roto.
329 pub off_by_dependency: Vec<String>,
330 /// Símbolos que no existen en este árbol: la bandera no hace nada. Ya se sabía al planear; se
331 /// repite acá porque quien lee el diff-back no siempre vio el plan.
332 pub never_existed: Vec<String>,
333}
334
335#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct Violation {
337 pub symbol: String,
338 pub asked: Action,
339 /// Cómo quedó de verdad.
340 pub got: String,
341 pub from: String,
342}
343
344impl DiffBack {
345 /// Limpio = ninguna promesa incumplida. Los `off_by_dependency` son el resultado buscado y los
346 /// `never_existed` ya los reportó el plan: ninguno de los dos ensucia el veredicto.
347 pub fn is_clean(&self) -> bool {
348 self.violated.is_empty()
349 }
350}
351
352/// Contrasta el plan contra el `.config` que produjo el kernel.
353pub fn diff_back(plan: &Plan, produced: &super::KernelConfig) -> DiffBack {
354 use super::ConfigValue;
355 let mut d = DiffBack {
356 honored: Vec::new(),
357 violated: Vec::new(),
358 off_by_dependency: Vec::new(),
359 never_existed: Vec::new(),
360 };
361 for p in &plan.symbols {
362 match (p.action, produced.get(&p.symbol)) {
363 // Ausente + se pedía apagar = está apagado. Kconfig omite lo que no puede existir.
364 (Action::Disable, None) => d.off_by_dependency.push(p.symbol.clone()),
365 // Ausente + se pedía encender = NO está. Eso sí es una promesa rota.
366 (Action::Enable, None) => d.violated.push(Violation {
367 symbol: p.symbol.clone(),
368 asked: Action::Enable,
369 got: "ausente del .config".to_string(),
370 from: p.from.clone(),
371 }),
372 (Action::Enable, Some(v)) if v.is_on() => d.honored.push(p.symbol.clone()),
373 (Action::Disable, Some(ConfigValue::No)) => d.honored.push(p.symbol.clone()),
374 (asked, Some(v)) => d.violated.push(Violation {
375 symbol: p.symbol.clone(),
376 asked,
377 got: format!("{v:?}"),
378 from: p.from.clone(),
379 }),
380 }
381 }
382 d.never_existed = plan
383 .unknown_symbols
384 .iter()
385 .map(|p| p.symbol.clone())
386 .collect();
387 d.never_existed.sort();
388 d.never_existed.dedup();
389 d
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395 use std::collections::BTreeMap;
396
397 fn escenario() -> (tempfile::TempDir, KconfigTree, Catalog, Recipe) {
398 let d = tempfile::tempdir().unwrap();
399 std::fs::write(
400 d.path().join("Kconfig"),
401 r#"
402config WIRELESS
403 bool "Wireless"
404
405config CFG80211
406 tristate "cfg80211"
407 depends on WIRELESS
408
409config WLAN
410 bool "WLAN"
411 select WIRELESS
412
413config AUDIT
414 bool "Audit"
415
416config MAC80211_LEDS
417 bool "leds"
418 depends on CFG80211
419"#,
420 )
421 .unwrap();
422 let tree = KconfigTree::parse(d.path(), &BTreeMap::new()).unwrap();
423 let cat: Catalog = toml::from_str(
424 r#"
425version = 1
426[[bundle]]
427id = "sin-wifi"
428title = "No necesito wifi"
429disable = ["WIRELESS"]
430close_leaks = ["WLAN"]
431
432[[bundle]]
433id = "con-leds"
434title = "Quiero los leds"
435enable = ["MAC80211_LEDS"]
436
437[[knob]]
438id = "auditoria"
439title = "Auditoría"
440side = "kconfig"
441enable = ["AUDIT"]
442
443[[knob]]
444id = "lto"
445title = "LTO"
446side = "recipe"
447recipe_field = "build.compiler"
448"#,
449 )
450 .unwrap();
451 let recipe: Recipe = toml::from_str(
452 r#"
453name = "linux"
454version = "6.16.12"
455[source]
456tarball = "https://x/linux.tar.gz"
457sha256 = "aa"
458[build]
459compiler = "zig-cc"
460target = "x86_64-linux-musl"
461link = "dynamic"
462[build.phases]
463configure = "make ARCH=x86_64 defconfig && make ARCH=x86_64 olddefconfig"
464compile = "make bzImage"
465"#,
466 )
467 .unwrap();
468 (d, tree, cat, recipe)
469 }
470
471 #[test]
472 fn emite_raices_no_clausuras() {
473 let (_d, tree, cat, base) = escenario();
474 let p = make_plan(&base, "recipes/linux.toml", &cat, &tree, &["sin-wifi".into()], &[]).unwrap();
475 // Dos banderas, no cuatro símbolos: la clausura la calcula olddefconfig.
476 assert_eq!(p.symbols.len(), 2);
477 assert!(p.fragment.contains("-d WIRELESS"));
478 assert!(p.fragment.contains("-d WLAN"));
479 assert!(!p.fragment.contains("CFG80211"));
480 // …pero hammer SABE cuántos caen, para poder explicarlo.
481 assert_eq!(p.closure_size, 4);
482 }
483
484 #[test]
485 fn la_fase_derivada_conserva_la_base_y_reresuelve() {
486 let (_d, tree, cat, base) = escenario();
487 let p = make_plan(&base, "recipes/linux.toml", &cat, &tree, &["sin-wifi".into()], &[]).unwrap();
488 assert!(p.configure.starts_with("make ARCH=x86_64 defconfig"));
489 assert!(p.configure.trim_end().ends_with("make ARCH=x86_64 olddefconfig"));
490 // El fragmento va ANTES del olddefconfig final: si fuera después, no se resolvería.
491 let i_frag = p.configure.find("scripts/config").unwrap();
492 let i_last = p.configure.rfind("olddefconfig").unwrap();
493 assert!(i_frag < i_last);
494 }
495
496 #[test]
497 fn el_nombre_derivado_es_estable_e_independiente_del_orden() {
498 let (_d, tree, cat, base) = escenario();
499 let a = make_plan(&base, "r", &cat, &tree, &["sin-wifi".into()], &["auditoria".into()]).unwrap();
500 let b = make_plan(&base, "r", &cat, &tree, &["sin-wifi".into()], &["auditoria".into()]).unwrap();
501 assert_eq!(a.derived_name, b.derived_name);
502 assert!(a.derived_name.starts_with("linux-plan-"));
503 let c = make_plan(&base, "r", &cat, &tree, &["sin-wifi".into()], &[]).unwrap();
504 assert_ne!(a.derived_name, c.derived_name);
505 }
506
507 #[test]
508 fn encender_algo_que_la_seleccion_apaga_es_conflicto() {
509 let (_d, tree, cat, base) = escenario();
510 // MAC80211_LEDS depende de CFG80211, que cae con WIRELESS. olddefconfig lo tiraría callado.
511 let e = make_plan(
512 &base,
513 "r",
514 &cat,
515 &tree,
516 &["sin-wifi".into(), "con-leds".into()],
517 &[],
518 )
519 .unwrap_err()
520 .to_string();
521 assert!(e.contains("MAC80211_LEDS"), "{e}");
522 }
523
524 #[test]
525 fn la_perilla_de_receta_se_declara_y_no_se_aplica() {
526 let (_d, tree, cat, base) = escenario();
527 let p = make_plan(&base, "r", &cat, &tree, &[], &["lto".into()]).unwrap();
528 assert!(p.symbols.is_empty());
529 assert_eq!(p.recipe_side.len(), 1);
530 assert_eq!(p.recipe_side[0].recipe_field, "build.compiler");
531 }
532
533 #[test]
534 fn el_simbolo_inexistente_no_se_pierde_en_silencio() {
535 let (_d, tree, _cat, base) = escenario();
536 let cat: Catalog = toml::from_str(
537 r#"
538version = 1
539[[bundle]]
540id = "fantasma"
541title = "Apaga algo que ya no existe"
542disable = ["REISERFS_FS"]
543"#,
544 )
545 .unwrap();
546 let p = make_plan(&base, "r", &cat, &tree, &["fantasma".into()], &[]).unwrap();
547 assert!(p.symbols.is_empty(), "no se emite una bandera que sería no-op");
548 assert_eq!(p.unknown_symbols.len(), 1);
549 assert_eq!(p.unknown_symbols[0].symbol, "REISERFS_FS");
550 }
551
552 #[test]
553 fn el_diff_back_ve_la_promesa_incumplida() {
554 let (_d, tree, cat, base) = escenario();
555 let p = make_plan(&base, "r", &cat, &tree, &["sin-wifi".into()], &["auditoria".into()]).unwrap();
556 // El kernel produjo un config donde WLAN sobrevivió y AUDIT no se pudo encender.
557 let cfg = super::super::KernelConfig::parse(
558 "# CONFIG_WIRELESS is not set\nCONFIG_WLAN=y\n# CONFIG_AUDIT is not set\n",
559 );
560 let d = diff_back(&p, &cfg);
561 assert!(!d.is_clean());
562 let violados: Vec<&str> = d.violated.iter().map(|v| v.symbol.as_str()).collect();
563 assert_eq!(violados, vec!["AUDIT", "WLAN"]);
564 assert_eq!(d.honored, vec!["WIRELESS"]);
565 }
566
567 /// La lección de correr un `olddefconfig` de verdad (SDD 22 §12): un `-d` sobre un símbolo que
568 /// el `.config` ni menciona **es el resultado buscado**, no una promesa rota. Contarlo como
569 /// fallo hacía que un plan perfecto se reportara roto.
570 #[test]
571 fn apagar_algo_ausente_es_exito_y_encenderlo_es_fallo() {
572 let (_d, tree, cat, base) = escenario();
573 let p = make_plan(&base, "r", &cat, &tree, &["sin-wifi".into()], &["auditoria".into()]).unwrap();
574 // WIRELESS apagado; WLAN ni aparece (cayó con él); AUDIT tampoco aparece, y ése SÍ se pidió
575 // encender.
576 let cfg = super::super::KernelConfig::parse("# CONFIG_WIRELESS is not set\n");
577 let d = diff_back(&p, &cfg);
578 assert_eq!(d.honored, vec!["WIRELESS"]);
579 assert_eq!(d.off_by_dependency, vec!["WLAN"]);
580 assert_eq!(d.violated.len(), 1);
581 assert_eq!(d.violated[0].symbol, "AUDIT");
582 assert!(!d.is_clean(), "falta un símbolo que se pidió ENCENDER");
583 }
584
585 #[test]
586 fn el_diff_back_limpio_es_limpio() {
587 let (_d, tree, cat, base) = escenario();
588 let p = make_plan(&base, "r", &cat, &tree, &["sin-wifi".into()], &[]).unwrap();
589 let cfg = super::super::KernelConfig::parse(
590 "# CONFIG_WIRELESS is not set\n# CONFIG_WLAN is not set\n",
591 );
592 assert!(diff_back(&p, &cfg).is_clean());
593 }
594}