use std::collections::HashMap;
use areev_cal::render as shared;
use areev_cal::store_types::SearchHit;
use areev_core::format::deserialize::DeserializedGrain;
use areev_core::types::GrainType;
use super::policy::{FormatPolicy, MetadataLevel, OutputFormat};
pub trait GrainRenderer: Send + Sync {
fn grain_type(&self) -> GrainType;
fn render(&self, grain: &DeserializedGrain, policy: &FormatPolicy) -> String;
fn render_summary(&self, grain: &DeserializedGrain, policy: &FormatPolicy) -> String {
self.render(grain, policy)
}
fn token_estimate(&self, grain: &DeserializedGrain, policy: &FormatPolicy) -> usize {
with_view(grain, |view| {
shared::estimate_tokens(view, detail(policy.metadata))
})
}
fn context_priority(&self, grain: &DeserializedGrain, hit: &SearchHit) -> f32;
}
pub struct RendererRegistry {
renderers: HashMap<GrainType, Box<dyn GrainRenderer>>,
}
impl Default for RendererRegistry {
fn default() -> Self {
Self::new()
}
}
const ALL_TYPES: [GrainType; 12] = [
GrainType::Fact,
GrainType::Event,
GrainType::State,
GrainType::Workflow,
GrainType::Tool,
GrainType::Observation,
GrainType::Goal,
GrainType::Reasoning,
GrainType::Consensus,
GrainType::Consent,
GrainType::Skill,
GrainType::Recommendation,
];
impl RendererRegistry {
pub fn new() -> Self {
let mut registry = Self {
renderers: HashMap::with_capacity(ALL_TYPES.len()),
};
for gt in ALL_TYPES {
registry.register(Box::new(SharedRenderer { gt }));
}
registry
}
pub fn get(&self, gt: GrainType) -> Option<&dyn GrainRenderer> {
self.renderers.get(>).map(|r| r.as_ref())
}
pub fn register(&mut self, renderer: Box<dyn GrainRenderer>) {
self.renderers.insert(renderer.grain_type(), renderer);
}
}
pub fn toon_columns(gt: &GrainType) -> &'static [&'static str] {
areev_core::types::registry::meta(*gt).toon_columns
}
fn detail(level: MetadataLevel) -> shared::MetadataDetail {
match level {
MetadataLevel::None => shared::MetadataDetail::None,
MetadataLevel::Minimal => shared::MetadataDetail::Minimal,
MetadataLevel::Full => shared::MetadataDetail::Full,
}
}
fn with_view<R>(grain: &DeserializedGrain, f: impl FnOnce(&shared::GrainView<'_>) -> R) -> R {
let fields = serde_json::Value::Object(
grain
.fields
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
);
let hash_hex = grain.hash.to_hex();
let view = shared::GrainView {
grain_type: grain.grain_type.as_str(),
hash: &hash_hex,
fields: &fields,
created_at_sec: grain.header.created_at_sec,
};
f(&view)
}
struct SharedRenderer {
gt: GrainType,
}
impl GrainRenderer for SharedRenderer {
fn grain_type(&self) -> GrainType {
self.gt
}
fn render(&self, grain: &DeserializedGrain, policy: &FormatPolicy) -> String {
with_view(grain, |view| match &policy.format {
OutputFormat::Sml => shared::render_grain_sml(view, detail(policy.metadata)),
OutputFormat::Markdown => shared::render_grain_markdown(view),
OutputFormat::PlainText => shared::render_grain_text_line(view, None),
OutputFormat::Json => shared::grain_json(view).to_string(),
OutputFormat::Toon => shared::toon_row(view),
})
}
fn render_summary(&self, grain: &DeserializedGrain, policy: &FormatPolicy) -> String {
with_view(grain, |view| {
let summary = shared::render_grain_summary(view);
match &policy.format {
OutputFormat::Sml => {
let tag = view.grain_type;
format!("<{tag}>{}</{tag}>", shared::sml_escape(&summary))
}
OutputFormat::Markdown => format!("- {summary}"),
_ => summary,
}
})
}
fn context_priority(&self, grain: &DeserializedGrain, hit: &SearchHit) -> f32 {
let base = match self.gt {
GrainType::Fact => 0.7,
GrainType::Event => 0.6,
GrainType::State => 0.9,
GrainType::Workflow => 0.5,
GrainType::Tool => {
if grain.get_bool("is_error").unwrap_or(false) {
0.65
} else {
0.5
}
}
GrainType::Observation => 0.6,
GrainType::Goal => {
let state_mod = match grain.get_str("goal_state") {
Some("active") => 0.15,
Some("suspended") => 0.05,
Some("satisfied" | "failed") => -0.2,
_ => 0.0,
};
let priority_mod = match grain.get_str("priority") {
Some("critical") => 0.1,
Some("high") => 0.05,
Some("low") => -0.1,
_ => 0.0,
};
0.8 + state_mod + priority_mod
}
GrainType::Reasoning => 0.7,
GrainType::Consensus => 0.65,
GrainType::Consent => 0.95,
GrainType::Skill => 0.6,
GrainType::Recommendation => 0.7,
};
adjusted_priority(base, grain, hit)
}
}
fn adjusted_priority(base: f32, grain: &DeserializedGrain, hit: &SearchHit) -> f32 {
let score_boost = hit.score as f32 * 0.15;
let confidence = grain.get_f64("confidence").unwrap_or(1.0) as f32;
let confidence_boost = (confidence - 0.5).max(0.0) * 0.1;
let verification_penalty = match grain.get_str("verification_status") {
Some("retracted") => -0.3,
Some("contested") => -0.15,
_ => 0.0,
};
(base + score_boost + confidence_boost + verification_penalty).clamp(0.0, 1.0)
}
#[cfg(test)]
mod tests {
use super::*;
use areev_core::format::header::MgHeader;
use std::collections::HashMap;
fn test_grain(gt: GrainType, fields: Vec<(&str, serde_json::Value)>) -> DeserializedGrain {
let mut map = HashMap::new();
for (k, v) in fields {
map.insert(k.to_string(), v);
}
DeserializedGrain {
header: MgHeader {
version: 1,
flags: 0,
grain_type: gt.type_byte(),
ns_hash: 0,
created_at_sec: 1740000000, },
grain_type: gt,
fields: map,
hash: areev_core::error::Hash::from_bytes(&[0u8; 32]),
}
}
fn test_hit(grain: DeserializedGrain) -> SearchHit {
SearchHit {
hash: grain.hash,
score: 0.85,
grain,
score_breakdown: None,
#[cfg(feature = "rerank")]
rerank_score: None,
#[cfg(feature = "llm-rerank")]
llm_rerank_score: None,
explanation: None,
scope_depth: None,
source_namespace: None,
relative_time: None,
conflict_status: None,
supersession_status: None,
superseded_by_hash: None,
recall_source: None,
}
}
fn fact() -> DeserializedGrain {
test_grain(
GrainType::Fact,
vec![
("subject", serde_json::json!("john")),
("relation", serde_json::json!("likes")),
("object", serde_json::json!("coffee")),
("confidence", serde_json::json!(0.95)),
],
)
}
#[test]
fn fact_sml_is_semantic_and_identical_to_the_shared_renderer() {
let grain = fact();
let policy = FormatPolicy::new(OutputFormat::Sml).metadata(MetadataLevel::Minimal);
let registry = RendererRegistry::new();
let via_registry = registry.get(GrainType::Fact).unwrap().render(&grain, &policy);
let direct = with_view(&grain, |v| {
shared::render_grain_sml(v, shared::MetadataDetail::Minimal)
});
assert_eq!(via_registry, direct);
assert!(via_registry.starts_with("<fact"));
assert!(via_registry.contains("confidence=\"0.95\""));
assert!(via_registry.contains("john likes coffee"));
}
#[test]
fn fact_markdown_matches_the_documented_cal_shape() {
let policy = FormatPolicy::new(OutputFormat::Markdown).metadata(MetadataLevel::None);
let registry = RendererRegistry::new();
let grain = test_grain(
GrainType::Fact,
vec![
("subject", serde_json::json!("john")),
("relation", serde_json::json!("likes")),
("object", serde_json::json!("coffee")),
],
);
let result = registry.get(GrainType::Fact).unwrap().render(&grain, &policy);
assert_eq!(result, "- **john** likes coffee *(2025-02-19)*");
}
#[test]
fn fact_summary_is_the_bare_triple() {
let policy = FormatPolicy::default();
let registry = RendererRegistry::new();
let result = registry
.get(GrainType::Fact)
.unwrap()
.render_summary(&fact(), &policy);
assert_eq!(result, "john likes coffee");
}
#[test]
fn json_is_the_envelope() {
let policy = FormatPolicy::new(OutputFormat::Json).metadata(MetadataLevel::None);
let registry = RendererRegistry::new();
let out = registry.get(GrainType::Fact).unwrap().render(&fact(), &policy);
let v: serde_json::Value = serde_json::from_str(&out).unwrap();
assert_eq!(v["grain_type"], "fact");
assert_eq!(v["fields"]["subject"], "john");
assert!(v["hash"].as_str().is_some());
}
#[test]
fn every_grain_type_has_a_default_renderer() {
let registry = RendererRegistry::new();
for gt in ALL_TYPES {
assert!(registry.get(gt).is_some(), "missing renderer for {gt:?}");
}
}
#[test]
fn custom_renderer_replaces_the_default() {
struct Loud;
impl GrainRenderer for Loud {
fn grain_type(&self) -> GrainType {
GrainType::Fact
}
fn render(&self, _g: &DeserializedGrain, _p: &FormatPolicy) -> String {
"LOUD".into()
}
fn context_priority(&self, _g: &DeserializedGrain, _h: &SearchHit) -> f32 {
1.0
}
}
let mut registry = RendererRegistry::new();
registry.register(Box::new(Loud));
let policy = FormatPolicy::default();
assert_eq!(
registry.get(GrainType::Fact).unwrap().render(&fact(), &policy),
"LOUD"
);
}
#[test]
fn priorities_keep_their_per_type_semantics() {
let registry = RendererRegistry::new();
let policy_hit = |gt: GrainType, fields: Vec<(&str, serde_json::Value)>| {
let grain = test_grain(gt, fields);
let hit = test_hit(grain);
registry
.get(gt)
.unwrap()
.context_priority(&hit.grain, &hit)
};
let consent = policy_hit(GrainType::Consent, vec![]);
let fact_p = policy_hit(GrainType::Fact, vec![]);
assert!(consent > fact_p);
let tool_ok = policy_hit(GrainType::Tool, vec![("is_error", serde_json::json!(false))]);
let tool_err = policy_hit(GrainType::Tool, vec![("is_error", serde_json::json!(true))]);
assert!(tool_err > tool_ok);
let g_active = policy_hit(
GrainType::Goal,
vec![("goal_state", serde_json::json!("active"))],
);
let g_done = policy_hit(
GrainType::Goal,
vec![("goal_state", serde_json::json!("satisfied"))],
);
assert!(g_active > g_done);
}
#[test]
fn token_estimate_uses_the_shared_estimator() {
let registry = RendererRegistry::new();
let grain = fact();
let none = registry.get(GrainType::Fact).unwrap().token_estimate(
&grain,
&FormatPolicy::new(OutputFormat::Sml).metadata(MetadataLevel::None),
);
let full = registry.get(GrainType::Fact).unwrap().token_estimate(
&grain,
&FormatPolicy::new(OutputFormat::Sml).metadata(MetadataLevel::Full),
);
assert!(none >= 1);
assert_eq!(full, none + 20); }
#[test]
fn event_toon_row_carries_the_header_date() {
let registry = RendererRegistry::new();
let grain = test_grain(
GrainType::Event,
vec![
("content", serde_json::json!("hello")),
("role", serde_json::json!("user")),
],
);
let policy = FormatPolicy::new(OutputFormat::Toon).metadata(MetadataLevel::None);
let row = registry.get(GrainType::Event).unwrap().render(&grain, &policy);
assert_eq!(row, "user,2025-02-19,hello");
}
}