⎇ 43e6b32e15dea99bd57a9a11fdafa7bd9a2a236f
crates/modules/cosmobiologia/cosmobiologia-corpus/src/lib.rs
Historia
Quién tocó qué
crudo
feat(cosmobiologia-corpus): tomografía por dominio + plantilla y guía
1//! `cosmobiologia-corpus` — la biblioteca de interpretación, indexada.
2//!
3//! El corpus **no calcula nada** y **no es un set de reglas
4//! matemáticas**. Las reglas —qué planeta en qué signo, qué aspecto—
5//! las computa el motor astronómico (`cosmobiologia-engine`). El corpus
6//! es la **evidencia textual**: fragmentos de los libros —y de la
7//! escritura del propio astrólogo— recortados y etiquetados con la
8//! combinación exacta que describen. En runtime, las combinaciones de
9//! una carta hacen un JOIN contra el corpus y traen los textos —
10//! citados, con fuente, sin que ninguna IA invente una palabra.
11//!
12//! ## Estructura — con TIPOS, porque la astrología tiene gramática
13//!
14//! Un planeta es una FUNCIÓN; un signo, un ESTILO; una casa, un
15//! DOMINIO; un aspecto, una RELACIÓN. No son vectores intercambiables
16//! de un mismo espacio plano — colapsarlos a uno solo destruye el
17//! significado. El corpus respeta esa gramática:
18//!
19//! 1. **Arquetipos** ([`Arquetipo`]) — los bloques: cada planeta /
20//! signo / casa / aspecto, con su [`PerfilSemantico`] (dimensiones
21//! psicológicas con peso). Es la ontología que el astrólogo escribe.
22//! 2. **Pasajes** ([`Pasaje`]) — el corpus propiamente dicho: texto
23//! real etiquetado por [`CombinacionId`], con su fuente. La
24//! evidencia citable.
25//! 3. **Composición** — deducir el perfil de una combinación NO leída a
26//! partir de los bloques. Es un problema de diseño **abierto**: un
27//! producto Hadamard ingenuo da resultados falsos (la dimensión que
28//! un bloque tiene en 0 se queda en 0, no «se enciende»). Este crate
29//! trae las capas 1-2 y deja la 3 sin resolver a propósito.
30//!
31//! La **rebanada por dominio** —ver el cuerpo de la carta en tajadas—
32//! sí vive aquí ([`rebanar_por_dominio`]): es geometría sobre las
33//! claves, no síntesis. La carta es una sola configuración; cortarla
34//! por dominio vivencial no la promedia, la MIRA desde un plano. Lo
35//! único que queda fuera es la síntesis narrativa —tejer los pasajes
36//! recuperados en un texto continuo—, trabajo de una capa superior.
37
38#![forbid(unsafe_code)]
39
40use std::collections::BTreeMap;
41use std::str::FromStr;
42
43use serde::{Deserialize, Deserializer, Serialize, Serializer};
44
45/// Perfil semántico: dimensiones psicológicas/vivenciales con un peso,
46/// por convención en `[-1.0, 1.0]`. Los **nombres** de las dimensiones
47/// los define el astrólogo en los datos — el esquema NO los fija (no
48/// presupone "Acción", "Estructura", …: el modelo es decisión del
49/// astrólogo, no del código).
50pub type PerfilSemantico = BTreeMap<String, f32>;
51
52/// El rol gramatical de un arquetipo. No es decorativo: marca que
53/// planeta y signo NO son la misma clase de cosa, y por eso no se
54/// combinan con un operador único e indiferenciado.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
56#[serde(rename_all = "snake_case")]
57pub enum TipoArquetipo {
58 /// Una función psíquica (Marte = impulso, Mercurio = cognición…).
59 Planeta,
60 /// Un estilo o modo (el signo colorea CÓMO se expresa la función).
61 Signo,
62 /// Un dominio o arena de la vida (la casa dice DÓNDE opera).
63 Casa,
64 /// Una relación entre dos funciones (conjunción, cuadratura…).
65 Aspecto,
66}
67
68/// Un bloque constructor: un planeta, signo, casa o aspecto, con el
69/// perfil semántico que el astrólogo le asigna.
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct Arquetipo {
72 /// Identificador estable — `"mars"`, `"virgo"`, `"conjunction"`…
73 pub nombre: String,
74 pub tipo: TipoArquetipo,
75 pub perfil: PerfilSemantico,
76}
77
78/// El plano vivencial donde una configuración descarga su energía. La
79/// contradicción «hiperdisciplinado vs. disperso» no se promedia: cada
80/// fuerza vive intacta en su dominio (general en la oficina, poeta
81/// disperso en la soledad).
82#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
83#[serde(rename_all = "snake_case")]
84pub enum Dominio {
85 /// Cuerpo, salud, acción directa (casas 1/5/9).
86 Vital,
87 /// Trabajo, vínculos, entorno (casas 3/7/11).
88 Social,
89 /// Inconsciente, miedos, indagación interna (casas 4/8/12).
90 Psiquico,
91}
92
93impl Dominio {
94 /// Dominio vivencial de una casa `1..=12`.
95 pub fn de_casa(casa: u8) -> Option<Dominio> {
96 match casa {
97 1 | 5 | 9 => Some(Dominio::Vital),
98 3 | 7 | 11 => Some(Dominio::Social),
99 4 | 8 | 12 => Some(Dominio::Psiquico),
100 2 | 6 | 10 => Some(Dominio::Social), // casas de recursos/trabajo
101 _ => None,
102 }
103 }
104}
105
106/// La «etiqueta de código de barras» de una combinación astrológica —
107/// la clave del JOIN. Respeta la gramática: cada variante es un tipo
108/// distinto de combinación, no una bolsa plana.
109///
110/// Se (de)serializa como una **cadena** legible (`mars·virgo`,
111/// `mars@c6`, `mars square saturn`) para que el corpus se escriba a
112/// mano sin pelear con la sintaxis de enums. El punto medio `·` admite
113/// el alias ASCII `/` (`mars/virgo`), más fácil de teclear.
114#[derive(Debug, Clone, PartialEq, Eq, Hash)]
115pub enum CombinacionId {
116 /// Un planeta en un signo — `mars·virgo`.
117 PlanetaSigno { planeta: String, signo: String },
118 /// Un planeta en una casa — `mars@c6`.
119 PlanetaCasa { planeta: String, casa: u8 },
120 /// Un aspecto entre dos planetas — `mars□saturn`. Los dos extremos
121 /// se guardan ORDENADOS, así `mars□saturn` y `saturn□mars` son la
122 /// misma clave.
123 Aspecto { a: String, kind: String, b: String },
124}
125
126impl CombinacionId {
127 pub fn planeta_signo(planeta: impl Into<String>, signo: impl Into<String>) -> Self {
128 CombinacionId::PlanetaSigno {
129 planeta: planeta.into(),
130 signo: signo.into(),
131 }
132 }
133
134 pub fn planeta_casa(planeta: impl Into<String>, casa: u8) -> Self {
135 CombinacionId::PlanetaCasa {
136 planeta: planeta.into(),
137 casa,
138 }
139 }
140
141 /// Construye un aspecto NORMALIZANDO el orden de los extremos, para
142 /// que la dirección no genere dos claves distintas.
143 pub fn aspecto(
144 a: impl Into<String>,
145 kind: impl Into<String>,
146 b: impl Into<String>,
147 ) -> Self {
148 let (a, b) = (a.into(), b.into());
149 let (a, b) = if a <= b { (a, b) } else { (b, a) };
150 CombinacionId::Aspecto {
151 a,
152 kind: kind.into(),
153 b,
154 }
155 }
156}
157
158impl std::fmt::Display for CombinacionId {
159 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160 match self {
161 CombinacionId::PlanetaSigno { planeta, signo } => {
162 write!(f, "{planeta}·{signo}")
163 }
164 CombinacionId::PlanetaCasa { planeta, casa } => write!(f, "{planeta}@c{casa}"),
165 CombinacionId::Aspecto { a, kind, b } => write!(f, "{a} {kind} {b}"),
166 }
167 }
168}
169
170impl FromStr for CombinacionId {
171 type Err = String;
172
173 /// Parsea el código de barras: `planeta·signo` (o `planeta/signo`),
174 /// `planeta@cN`, o `a kind b` (tres tokens separados por espacios).
175 fn from_str(s: &str) -> Result<Self, String> {
176 let s = s.trim();
177 if let Some((planeta, signo)) = s.split_once('·').or_else(|| s.split_once('/')) {
178 return Ok(CombinacionId::planeta_signo(planeta.trim(), signo.trim()));
179 }
180 if let Some((planeta, casa)) = s.split_once("@c") {
181 let casa: u8 = casa
182 .trim()
183 .parse()
184 .map_err(|_| format!("casa inválida en '{s}'"))?;
185 return Ok(CombinacionId::planeta_casa(planeta.trim(), casa));
186 }
187 let toks: Vec<&str> = s.split_whitespace().collect();
188 if toks.len() == 3 {
189 return Ok(CombinacionId::aspecto(toks[0], toks[1], toks[2]));
190 }
191 Err(format!("combinación no reconocida: '{s}'"))
192 }
193}
194
195impl Serialize for CombinacionId {
196 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
197 s.serialize_str(&self.to_string())
198 }
199}
200
201impl<'de> Deserialize<'de> for CombinacionId {
202 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
203 let s = String::deserialize(d)?;
204 s.parse().map_err(serde::de::Error::custom)
205 }
206}
207
208/// La posición de un planeta en una carta concreta: en qué signo y en
209/// qué casa cae. Es la materia prima desde la que se derivan las
210/// [`CombinacionId`] de la carta — el puente entre lo que el motor
211/// astronómico calcula y las claves del JOIN del corpus.
212#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
213pub struct Colocacion {
214 pub planeta: String,
215 pub signo: String,
216 pub casa: u8,
217}
218
219/// Un aspecto medido en una carta: dos planetas y el ángulo que los une.
220#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
221pub struct AspectoEnCarta {
222 pub a: String,
223 pub kind: String,
224 pub b: String,
225}
226
227/// Deriva TODAS las combinaciones de una carta: por cada planeta, su
228/// `planeta·signo` y su `planeta@cN`; por cada aspecto medido, su
229/// `a kind b`. El resultado es la lista que se le pasa a
230/// [`Corpus::interpretar`] para hacer el JOIN.
231pub fn combinaciones_de_carta(
232 colocaciones: &[Colocacion],
233 aspectos: &[AspectoEnCarta],
234) -> Vec<CombinacionId> {
235 let mut out = Vec::with_capacity(colocaciones.len() * 2 + aspectos.len());
236 for c in colocaciones {
237 out.push(CombinacionId::planeta_signo(&c.planeta, &c.signo));
238 out.push(CombinacionId::planeta_casa(&c.planeta, c.casa));
239 }
240 for a in aspectos {
241 out.push(CombinacionId::aspecto(&a.a, &a.kind, &a.b));
242 }
243 out
244}
245
246/// La **tomografía** de la carta: reparte cada combinación en el dominio
247/// —o dominios— vivencial donde descarga su energía.
248///
249/// La carta es UNA sola configuración; rebanarla por dominio no la
250/// promedia ni la mutila, la MIRA desde un plano —como ver un cuerpo en
251/// tajadas—. Las reglas del corte:
252///
253/// - un `planeta@cN` cae en el dominio de su casa;
254/// - un `planeta·signo` hereda el dominio de la casa donde ESE planeta
255/// está colocado en la carta;
256/// - un aspecto **puentea**: aparece en el dominio de cada uno de sus
257/// dos extremos. Que una misma combinación salga en dos rebanadas no
258/// es un error — es la conexión real entre dos planos.
259///
260/// Una combinación cuyo planeta no figura en `colocaciones` se omite (no
261/// hay forma de saber en qué dominio ubicarla).
262pub fn rebanar_por_dominio(
263 colocaciones: &[Colocacion],
264 combinaciones: &[CombinacionId],
265) -> BTreeMap<Dominio, Vec<CombinacionId>> {
266 let casa_de: BTreeMap<&str, u8> = colocaciones
267 .iter()
268 .map(|c| (c.planeta.as_str(), c.casa))
269 .collect();
270 let dominio_de = |planeta: &str| -> Option<Dominio> {
271 casa_de.get(planeta).copied().and_then(Dominio::de_casa)
272 };
273
274 let mut tajadas: BTreeMap<Dominio, Vec<CombinacionId>> = BTreeMap::new();
275 for id in combinaciones {
276 let dominios: Vec<Dominio> = match id {
277 CombinacionId::PlanetaCasa { casa, .. } => {
278 Dominio::de_casa(*casa).into_iter().collect()
279 }
280 CombinacionId::PlanetaSigno { planeta, .. } => {
281 dominio_de(planeta).into_iter().collect()
282 }
283 CombinacionId::Aspecto { a, b, .. } => {
284 let mut ds = Vec::new();
285 for p in [a.as_str(), b.as_str()] {
286 if let Some(d) = dominio_de(p) {
287 if !ds.contains(&d) {
288 ds.push(d);
289 }
290 }
291 }
292 ds
293 }
294 };
295 for d in dominios {
296 tajadas.entry(d).or_default().push(id.clone());
297 }
298 }
299 tajadas
300}
301
302/// Un fragmento de interpretación: el texto de un autor (o del propio
303/// astrólogo) recortado y etiquetado con la combinación que describe.
304#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct Pasaje {
306 /// La combinación que este pasaje interpreta — la clave del JOIN.
307 pub combinacion: CombinacionId,
308 /// El texto, citado literalmente.
309 pub texto: String,
310 /// Procedencia — autor y obra, o `"propio"`. Convención: un pasaje
311 /// con fuente `"deducido"` es un perfil compuesto, no un texto de
312 /// libro (capa de composición, aún sin construir).
313 pub fuente: String,
314 /// Firma semántica del pasaje. Opcional: vacío hasta que se calcule.
315 #[serde(default)]
316 pub perfil: PerfilSemantico,
317 /// Dominio vivencial donde aplica, si el pasaje lo acota.
318 #[serde(default)]
319 pub dominio: Option<Dominio>,
320}
321
322/// El corpus completo: la ontología de arquetipos + los pasajes.
323#[derive(Debug, Clone, Default, Serialize, Deserialize)]
324pub struct Corpus {
325 pub arquetipos: Vec<Arquetipo>,
326 pub pasajes: Vec<Pasaje>,
327}
328
329impl Corpus {
330 /// Carga un corpus desde su forma RON (el formato de los archivos
331 /// que el astrólogo escribe a mano).
332 pub fn desde_ron(texto: &str) -> Result<Corpus, String> {
333 ron::from_str(texto).map_err(|e| format!("corpus :: RON inválido: {e}"))
334 }
335
336 /// Serializa el corpus a RON.
337 pub fn a_ron(&self) -> Result<String, String> {
338 ron::to_string(self).map_err(|e| format!("corpus :: no se pudo serializar: {e}"))
339 }
340
341 /// El arquetipo con ese nombre y tipo, si existe.
342 pub fn arquetipo(&self, nombre: &str, tipo: TipoArquetipo) -> Option<&Arquetipo> {
343 self.arquetipos
344 .iter()
345 .find(|a| a.nombre == nombre && a.tipo == tipo)
346 }
347
348 /// Todos los pasajes que interpretan una combinación dada.
349 pub fn pasajes_de(&self, id: &CombinacionId) -> Vec<&Pasaje> {
350 self.pasajes.iter().filter(|p| &p.combinacion == id).collect()
351 }
352
353 /// El JOIN: dada la lista de combinaciones de una carta, devuelve
354 /// todos los pasajes del corpus que las interpretan. Cobertura
355 /// total — no se salta una combinación que tenga texto. Combinar
356 /// estos pasajes en una narrativa coherente (síntesis) es trabajo
357 /// de una capa superior; aquí sólo se RECUPERA la evidencia.
358 pub fn interpretar(&self, combinaciones: &[CombinacionId]) -> Vec<&Pasaje> {
359 let mut out = Vec::new();
360 for id in combinaciones {
361 out.extend(self.pasajes_de(id));
362 }
363 out
364 }
365
366 /// El JOIN **rebanado por dominio**: para cada plano vivencial, los
367 /// pasajes que lo interpretan. Es la entrada directa de un gráfico
368 /// «por tajadas» — una rebanada, una vista del cuerpo de la carta.
369 /// Un aspecto que puentea dos dominios trae sus pasajes a las dos
370 /// rebanadas.
371 pub fn interpretar_por_dominio(
372 &self,
373 colocaciones: &[Colocacion],
374 aspectos: &[AspectoEnCarta],
375 ) -> BTreeMap<Dominio, Vec<&Pasaje>> {
376 let combinaciones = combinaciones_de_carta(colocaciones, aspectos);
377 rebanar_por_dominio(colocaciones, &combinaciones)
378 .into_iter()
379 .map(|(dominio, ids)| {
380 let mut pasajes = Vec::new();
381 for id in &ids {
382 pasajes.extend(self.pasajes_de(id));
383 }
384 (dominio, pasajes)
385 })
386 .collect()
387 }
388
389 /// Combinaciones del corpus que NO tienen ni un solo pasaje — los
390 /// huecos que habría que escribir, o cubrir con composición.
391 pub fn huecos(&self, combinaciones: &[CombinacionId]) -> Vec<CombinacionId> {
392 combinaciones
393 .iter()
394 .filter(|id| self.pasajes_de(id).is_empty())
395 .cloned()
396 .collect()
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403
404 #[test]
405 fn aspecto_normaliza_el_orden_de_los_extremos() {
406 let ab = CombinacionId::aspecto("mars", "square", "saturn");
407 let ba = CombinacionId::aspecto("saturn", "square", "mars");
408 assert_eq!(ab, ba, "mars□saturn y saturn□mars son la misma clave");
409 }
410
411 #[test]
412 fn display_da_un_codigo_de_barras_legible() {
413 assert_eq!(
414 CombinacionId::planeta_signo("mars", "virgo").to_string(),
415 "mars·virgo"
416 );
417 assert_eq!(
418 CombinacionId::planeta_casa("mars", 6).to_string(),
419 "mars@c6"
420 );
421 }
422
423 fn pasaje(id: CombinacionId, texto: &str) -> Pasaje {
424 Pasaje {
425 combinacion: id,
426 texto: texto.into(),
427 fuente: "test".into(),
428 perfil: PerfilSemantico::new(),
429 dominio: None,
430 }
431 }
432
433 #[test]
434 fn interpretar_hace_el_join_de_las_combinaciones() {
435 let corpus = Corpus {
436 arquetipos: Vec::new(),
437 pasajes: vec![
438 pasaje(
439 CombinacionId::planeta_signo("mars", "virgo"),
440 "el guerrero cirujano",
441 ),
442 pasaje(
443 CombinacionId::aspecto("mars", "square", "saturn"),
444 "acción frenada",
445 ),
446 pasaje(
447 CombinacionId::planeta_signo("moon", "pisces"),
448 "sensibilidad difusa",
449 ),
450 ],
451 };
452 // Una carta con sólo dos de las tres combinaciones.
453 let carta = [
454 CombinacionId::planeta_signo("mars", "virgo"),
455 // El orden inverso debe resolver igual.
456 CombinacionId::aspecto("saturn", "square", "mars"),
457 ];
458 let recuperados = corpus.interpretar(&carta);
459 assert_eq!(recuperados.len(), 2);
460 assert!(recuperados.iter().any(|p| p.texto == "el guerrero cirujano"));
461 assert!(recuperados.iter().any(|p| p.texto == "acción frenada"));
462 }
463
464 #[test]
465 fn huecos_detecta_combinaciones_sin_pasaje() {
466 let corpus = Corpus {
467 arquetipos: Vec::new(),
468 pasajes: vec![pasaje(
469 CombinacionId::planeta_signo("mars", "virgo"),
470 "x",
471 )],
472 };
473 let carta = [
474 CombinacionId::planeta_signo("mars", "virgo"),
475 CombinacionId::planeta_signo("venus", "leo"),
476 ];
477 let huecos = corpus.huecos(&carta);
478 assert_eq!(huecos.len(), 1);
479 assert_eq!(huecos[0], CombinacionId::planeta_signo("venus", "leo"));
480 }
481
482 #[test]
483 fn corpus_roundtrip_ron() {
484 let corpus = Corpus {
485 arquetipos: vec![Arquetipo {
486 nombre: "mars".into(),
487 tipo: TipoArquetipo::Planeta,
488 perfil: BTreeMap::from([("accion".into(), 0.9_f32)]),
489 }],
490 pasajes: vec![pasaje(
491 CombinacionId::planeta_signo("mars", "virgo"),
492 "el guerrero cirujano",
493 )],
494 };
495 let ron = corpus.a_ron().expect("serializa");
496 let vuelta = Corpus::desde_ron(&ron).expect("deserializa");
497 assert_eq!(vuelta.arquetipos.len(), 1);
498 assert_eq!(vuelta.pasajes.len(), 1);
499 assert_eq!(vuelta.pasajes[0].texto, "el guerrero cirujano");
500 }
501
502 #[test]
503 fn dominio_de_casa_clasifica_los_planos() {
504 assert_eq!(Dominio::de_casa(1), Some(Dominio::Vital));
505 assert_eq!(Dominio::de_casa(7), Some(Dominio::Social));
506 assert_eq!(Dominio::de_casa(12), Some(Dominio::Psiquico));
507 assert_eq!(Dominio::de_casa(13), None);
508 }
509
510 #[test]
511 fn combinacion_id_roundtrip_string() {
512 for id in [
513 CombinacionId::planeta_signo("venus", "leo"),
514 CombinacionId::planeta_casa("sun", 10),
515 CombinacionId::aspecto("moon", "trine", "jupiter"),
516 ] {
517 let s = id.to_string();
518 let vuelta: CombinacionId = s.parse().expect("parsea su propio Display");
519 assert_eq!(vuelta, id);
520 }
521 }
522
523 #[test]
524 fn barra_es_alias_ascii_del_punto_medio() {
525 assert_eq!(
526 "mars/virgo".parse::<CombinacionId>().unwrap(),
527 CombinacionId::planeta_signo("mars", "virgo"),
528 );
529 }
530
531 /// Una carta mínima: Marte en Virgo en casa 6 (Social), Saturno en
532 /// Aries en casa 1 (Vital), y una cuadratura que los une.
533 fn carta_de_prueba() -> (Vec<Colocacion>, Vec<AspectoEnCarta>) {
534 let colocaciones = vec![
535 Colocacion {
536 planeta: "mars".into(),
537 signo: "virgo".into(),
538 casa: 6,
539 },
540 Colocacion {
541 planeta: "saturn".into(),
542 signo: "aries".into(),
543 casa: 1,
544 },
545 ];
546 let aspectos = vec![AspectoEnCarta {
547 a: "mars".into(),
548 kind: "square".into(),
549 b: "saturn".into(),
550 }];
551 (colocaciones, aspectos)
552 }
553
554 #[test]
555 fn combinaciones_de_carta_deriva_signo_casa_y_aspectos() {
556 let (colocaciones, aspectos) = carta_de_prueba();
557 let combos = combinaciones_de_carta(&colocaciones, &aspectos);
558 // 2 planetas × (signo + casa) + 1 aspecto.
559 assert_eq!(combos.len(), 5);
560 assert!(combos.contains(&CombinacionId::planeta_signo("mars", "virgo")));
561 assert!(combos.contains(&CombinacionId::planeta_casa("saturn", 1)));
562 assert!(combos.contains(&CombinacionId::aspecto("mars", "square", "saturn")));
563 }
564
565 #[test]
566 fn rebanar_por_dominio_reparte_y_el_aspecto_puentea() {
567 let (colocaciones, aspectos) = carta_de_prueba();
568 let combos = combinaciones_de_carta(&colocaciones, &aspectos);
569 let tajadas = rebanar_por_dominio(&colocaciones, &combos);
570
571 // Marte en casa 6 → Social ; Saturno en casa 1 → Vital.
572 let social = tajadas.get(&Dominio::Social).expect("hay tajada social");
573 let vital = tajadas.get(&Dominio::Vital).expect("hay tajada vital");
574 assert_eq!(social.len(), 3);
575 assert_eq!(vital.len(), 3);
576
577 // El aspecto cruza los dos planos: sale en las dos tajadas.
578 let aspecto = CombinacionId::aspecto("mars", "square", "saturn");
579 assert!(social.contains(&aspecto));
580 assert!(vital.contains(&aspecto));
581 }
582
583 #[test]
584 fn interpretar_por_dominio_agrupa_pasajes() {
585 let (colocaciones, aspectos) = carta_de_prueba();
586 let corpus = Corpus {
587 arquetipos: Vec::new(),
588 pasajes: vec![
589 pasaje(CombinacionId::planeta_casa("mars", 6), "trabajo intenso"),
590 pasaje(CombinacionId::planeta_casa("saturn", 1), "cuerpo severo"),
591 ],
592 };
593 let por_dominio = corpus.interpretar_por_dominio(&colocaciones, &aspectos);
594 assert_eq!(por_dominio[&Dominio::Social].len(), 1);
595 assert_eq!(por_dominio[&Dominio::Vital].len(), 1);
596 assert_eq!(por_dominio[&Dominio::Social][0].texto, "trabajo intenso");
597 }
598
599 #[test]
600 fn ejemplo_ron_carga() {
601 let corpus = Corpus::desde_ron(include_str!("../ejemplo.ron"))
602 .expect("ejemplo.ron debe ser RON válido");
603 assert!(!corpus.arquetipos.is_empty(), "la plantilla trae arquetipos");
604 assert!(!corpus.pasajes.is_empty(), "la plantilla trae pasajes");
605 // El pasaje del aspecto fija su dominio explícitamente.
606 let aspecto = CombinacionId::aspecto("mars", "square", "saturn");
607 let p = corpus.pasajes_de(&aspecto);
608 assert_eq!(p.len(), 1);
609 assert_eq!(p[0].dominio, Some(Dominio::Psiquico));
610 }
611}