//! Corpus data types — shared between SSR render pass and UI components. //! //! The server crate owns the loaders and populates the structures from //! disk; the UI crate only reads them. Everything here is //! `Serialize + Deserialize + Clone` so it survives Leptos Resources //! and possible future hydration round-trips. //! //! Library structure: //! Corpus //! └─ cycles: Vec ← each cycle is one research pass //! ├─ slug, title, subtitle //! ├─ order: reading order of source slugs //! ├─ sources: slug → Source (distillation) //! └─ themes: running patterns extracted from _index.md //! └─ artifacts: Vec ← closing documents (manifest, applied-principles) //! └─ slug, title, closes_cycle, body_html use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; /// One research cycle: a parallel distillation pass over a set of /// sources, bound by a root question. cycle-1-philosophy asks "why does /// form move the human heart?"; cycle-2-design-theory asks "how was /// that practiced by masters?". #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct Cycle { pub slug: String, pub title: String, pub subtitle: String, pub order: Vec, pub sources: HashMap, pub themes: Vec, } /// A closing document of a cycle — manifest (cycle 1), applied-principles /// (cycle 2), and future cycle 3's visual-signature document. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct Artifact { pub slug: String, pub title: String, pub subtitle: String, pub closes_cycle: Option, pub body_html: String, } /// The entire loaded corpus. Shared via Leptos context as `Arc` /// on the server; UI reads via server fns + Resources. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Corpus { pub cycles: Vec, pub artifacts: Vec, } impl Corpus { /// Find a source by slug across ALL cycles. pub fn get(&self, slug: &str) -> Option<(&Cycle, &Source)> { for cycle in &self.cycles { if let Some(src) = cycle.sources.get(slug) { return Some((cycle, src)); } } None } /// Neighbors (prev, next) within the same cycle — wrap-around. pub fn neighbors(&self, slug: &str) -> (Option<&str>, Option<&str>) { for cycle in &self.cycles { let Some(i) = cycle.order.iter().position(|s| s == slug) else { continue; }; let n = cycle.order.len(); if n <= 1 { return (None, None); } let prev = Some(cycle.order[(i + n - 1) % n].as_str()); let next = Some(cycle.order[(i + 1) % n].as_str()); return (prev, next); } (None, None) } /// Find a theme by slug across ALL cycles. pub fn get_theme(&self, slug: &str) -> Option<(&Cycle, &Theme)> { for cycle in &self.cycles { if let Some(t) = cycle.themes.iter().find(|t| t.slug == slug) { return Some((cycle, t)); } } None } /// Find an artifact by slug. pub fn get_artifact(&self, slug: &str) -> Option<&Artifact> { self.artifacts.iter().find(|a| a.slug == slug) } /// Get a cycle by slug. pub fn get_cycle(&self, slug: &str) -> Option<&Cycle> { self.cycles.iter().find(|c| c.slug == slug) } } /// One distillation. Body is pre-rendered HTML (pulldown-cmark output). #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct Source { pub slug: String, /// Display title — "Symposium (Plato)" etc. pub title: String, /// Short author label — "Plato", "Ibn ʿArabī" etc. pub author: String, /// The core-claim one-liner (extracted from `## Core claim` section). pub core_claim: String, /// Tags from YAML frontmatter. pub tags: Vec, /// Confidence level (high / medium / low) from frontmatter. pub confidence: String, /// The full markdown body rendered to HTML. pub body_html: String, /// Which cycle this source belongs to. pub cycle: String, } /// A running theme — pattern that proustep across multiple sources. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct Theme { pub slug: String, pub title: String, /// Short pitch (one paragraph). pub description_html: String, /// Sources that contribute to this theme (by slug). pub contributing: Vec, /// Which cycle this theme belongs to. pub cycle: String, } /// Convenience alias — we always thread the corpus through context as /// an `Arc` so multiple concurrent requests share one in-memory copy. pub type CorpusHandle = Arc; /// Reading language. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum Lang { Ru, En, } impl Lang { pub fn as_str(&self) -> &'static str { match self { Lang::Ru => "ru", Lang::En => "en" } } pub fn from_query(s: Option<&str>) -> Self { match s { Some("en") => Lang::En, _ => Lang::Ru, } } pub fn other(&self) -> Lang { match self { Lang::Ru => Lang::En, Lang::En => Lang::Ru } } } /// Bilingual container — same slugs in both languages where possible. /// Cycle 1 is bilingual; cycle 2 and artifacts are Russian-only, and /// both language variants point at the same data for those. #[derive(Debug, Clone)] pub struct BilingualCorpus { pub ru: Arc, pub en: Arc, } impl BilingualCorpus { pub fn for_lang(&self, lang: Lang) -> Arc { match lang { Lang::Ru => self.ru.clone(), Lang::En => self.en.clone(), } } } pub type BilingualHandle = Arc;