181 lines
5.9 KiB
Rust
181 lines
5.9 KiB
Rust
//! 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<Cycle> ← 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<Artifact> ← 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<String>,
|
|
pub sources: HashMap<String, Source>,
|
|
pub themes: Vec<Theme>,
|
|
}
|
|
|
|
/// 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<String>,
|
|
pub body_html: String,
|
|
}
|
|
|
|
/// The entire loaded corpus. Shared via Leptos context as `Arc<Corpus>`
|
|
/// on the server; UI reads via server fns + Resources.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Corpus {
|
|
pub cycles: Vec<Cycle>,
|
|
pub artifacts: Vec<Artifact>,
|
|
}
|
|
|
|
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<String>,
|
|
/// 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<String>,
|
|
/// 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<Corpus>;
|
|
|
|
/// 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<Corpus>,
|
|
pub en: Arc<Corpus>,
|
|
}
|
|
|
|
impl BilingualCorpus {
|
|
pub fn for_lang(&self, lang: Lang) -> Arc<Corpus> {
|
|
match lang {
|
|
Lang::Ru => self.ru.clone(),
|
|
Lang::En => self.en.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub type BilingualHandle = Arc<BilingualCorpus>;
|