1use std::collections::HashMap;
7
8use dejadb_cal::store_types::SearchHit;
9use dejadb_core::format::deserialize::DeserializedGrain;
10use dejadb_core::types::GrainType;
11
12use super::policy::{FormatPolicy, MetadataLevel, OutputFormat};
13
14pub trait GrainRenderer: Send + Sync {
18 fn grain_type(&self) -> GrainType;
20
21 fn render(&self, grain: &DeserializedGrain, policy: &FormatPolicy) -> String;
23
24 fn render_summary(&self, grain: &DeserializedGrain, policy: &FormatPolicy) -> String {
26 self.render(grain, policy)
27 }
28
29 fn token_estimate(&self, grain: &DeserializedGrain, policy: &FormatPolicy) -> usize {
31 let chars: usize = grain
32 .fields
33 .iter()
34 .map(|(k, v)| k.len() + json_display_len(v) + 4)
35 .sum();
36 let meta_overhead = match policy.metadata {
37 MetadataLevel::None => 0,
38 MetadataLevel::Minimal => 30,
39 MetadataLevel::Full => 80,
40 };
41 (chars + meta_overhead) / 4
42 }
43
44 fn context_priority(&self, grain: &DeserializedGrain, hit: &SearchHit) -> f32;
46}
47
48fn json_display_len(v: &serde_json::Value) -> usize {
50 match v {
51 serde_json::Value::Null => 4,
52 serde_json::Value::Bool(b) => {
53 if *b {
54 4
55 } else {
56 5
57 }
58 }
59 serde_json::Value::Number(n) => n.to_string().len(),
60 serde_json::Value::String(s) => s.len(),
61 serde_json::Value::Array(a) => a.iter().map(json_display_len).sum::<usize>() + a.len() * 2,
62 serde_json::Value::Object(o) => o
63 .iter()
64 .map(|(k, v)| k.len() + json_display_len(v) + 4)
65 .sum(),
66 }
67}
68
69pub struct RendererRegistry {
71 renderers: HashMap<GrainType, Box<dyn GrainRenderer>>,
72}
73
74impl Default for RendererRegistry {
75 fn default() -> Self {
76 Self::new()
77 }
78}
79
80impl RendererRegistry {
81 pub fn new() -> Self {
83 let mut registry = Self {
84 renderers: HashMap::with_capacity(10),
85 };
86 registry.register(Box::new(FactRenderer));
87 registry.register(Box::new(EventRenderer));
88 registry.register(Box::new(StateRenderer));
89 registry.register(Box::new(WorkflowRenderer));
90 registry.register(Box::new(ToolRenderer));
91 registry.register(Box::new(ObservationRenderer));
92 registry.register(Box::new(GoalRenderer));
93 registry.register(Box::new(ReasoningRenderer));
94 registry.register(Box::new(ConsensusRenderer));
95 registry.register(Box::new(ConsentRenderer));
96 registry.register(Box::new(SkillRenderer));
97 registry
98 }
99
100 pub fn get(&self, gt: GrainType) -> Option<&dyn GrainRenderer> {
102 self.renderers.get(>).map(|r| r.as_ref())
103 }
104
105 pub fn register(&mut self, renderer: Box<dyn GrainRenderer>) {
107 self.renderers.insert(renderer.grain_type(), renderer);
108 }
109}
110
111fn adjusted_priority(base: f32, grain: &DeserializedGrain, hit: &SearchHit) -> f32 {
117 let score_boost = hit.score as f32 * 0.15;
118 let confidence = grain.get_f64("confidence").unwrap_or(1.0) as f32;
119 let confidence_boost = (confidence - 0.5).max(0.0) * 0.1;
120 let verification_penalty = match grain.get_str("verification_status") {
121 Some("retracted") => -0.3,
122 Some("contested") => -0.15,
123 _ => 0.0,
124 };
125 (base + score_boost + confidence_boost + verification_penalty).clamp(0.0, 1.0)
126}
127
128struct MetadataFragment {
130 confidence: Option<f64>,
131 created_at_sec: u32,
132 hash_hex: String,
133 tags: Vec<String>,
134 source_type: Option<String>,
135 namespace: Option<String>,
136 verification_status: Option<String>,
137}
138
139fn extract_metadata(grain: &DeserializedGrain, level: MetadataLevel) -> Option<MetadataFragment> {
140 match level {
141 MetadataLevel::None => None,
142 MetadataLevel::Minimal | MetadataLevel::Full => {
143 let confidence = grain.get_f64("confidence");
144 let created_at_sec = grain.header.created_at_sec;
145 let hash_hex = grain.hash.to_hex();
146
147 let (tags, source_type, namespace, verification_status) =
148 if level == MetadataLevel::Full {
149 let tags = grain
150 .fields
151 .get("structural_tags")
152 .or_else(|| grain.fields.get("tags"))
153 .and_then(|v| v.as_array())
154 .map(|arr| {
155 arr.iter()
156 .filter_map(|v| v.as_str().map(String::from))
157 .collect()
158 })
159 .unwrap_or_default();
160 (
161 tags,
162 grain.get_str("source_type").map(String::from),
163 grain.get_str("namespace").map(String::from),
164 grain.get_str("verification_status").map(String::from),
165 )
166 } else {
167 (Vec::new(), None, None, None)
168 };
169
170 Some(MetadataFragment {
171 confidence,
172 created_at_sec,
173 hash_hex,
174 tags,
175 source_type,
176 namespace,
177 verification_status,
178 })
179 }
180 }
181}
182
183fn format_date(epoch_sec: u32) -> String {
185 let secs = epoch_sec as i64;
186 let days = secs / 86400;
188 let (y, m, d) = days_to_ymd(days);
190 format!("{y:04}-{m:02}-{d:02}")
191}
192
193fn days_to_ymd(mut days: i64) -> (i64, u32, u32) {
195 days += 719468;
197 let era = if days >= 0 { days } else { days - 146096 } / 146097;
198 let doe = (days - era * 146097) as u32;
199 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
200 let y = yoe as i64 + era * 400;
201 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
202 let mp = (5 * doy + 2) / 153;
203 let d = doy - (153 * mp + 2) / 5 + 1;
204 let m = if mp < 10 { mp + 3 } else { mp - 9 };
205 let y = if m <= 2 { y + 1 } else { y };
206 (y, m, d)
207}
208
209fn format_meta_sml_attrs(meta: &MetadataFragment, level: MetadataLevel) -> String {
210 let mut attrs = Vec::new();
211 if let Some(c) = meta.confidence {
212 attrs.push(format!("confidence=\"{c:.2}\""));
213 }
214 if meta.created_at_sec > 0 {
215 attrs.push(format!("date=\"{}\"", format_date(meta.created_at_sec)));
216 }
217 if level == MetadataLevel::Full {
218 attrs.push(format!("hash=\"{}\"", &meta.hash_hex[..16]));
219 if let Some(ref ns) = meta.namespace {
220 attrs.push(format!("namespace=\"{}\"", sml_escape(ns)));
221 }
222 if let Some(ref vs) = meta.verification_status {
223 attrs.push(format!("status=\"{}\"", sml_escape(vs)));
224 }
225 if !meta.tags.is_empty() {
226 attrs.push(format!("tags=\"{}\"", sml_escape(&meta.tags.join(","))));
227 }
228 }
229 if attrs.is_empty() {
230 String::new()
231 } else {
232 format!(" {}", attrs.join(" "))
233 }
234}
235
236fn format_meta_markdown(meta: &MetadataFragment, level: MetadataLevel) -> String {
237 let mut parts = Vec::new();
238 if let Some(c) = meta.confidence {
239 parts.push(format!("{c:.2}"));
240 }
241 if meta.created_at_sec > 0 {
242 parts.push(format_date(meta.created_at_sec));
243 }
244 if level == MetadataLevel::Full {
245 if let Some(ref vs) = meta.verification_status {
246 parts.push(vs.clone());
247 }
248 }
249 if parts.is_empty() {
250 String::new()
251 } else {
252 format!(" *({})*", parts.join(", "))
253 }
254}
255
256fn format_meta_plain(meta: &MetadataFragment, level: MetadataLevel) -> String {
257 let mut parts = Vec::new();
258 if let Some(c) = meta.confidence {
259 parts.push(format!("{c:.2}"));
260 }
261 if meta.created_at_sec > 0 {
262 parts.push(format_date(meta.created_at_sec));
263 }
264 if level == MetadataLevel::Full {
265 if let Some(ref vs) = meta.verification_status {
266 parts.push(vs.clone());
267 }
268 if let Some(ref ns) = meta.namespace {
269 parts.push(ns.clone());
270 }
271 }
272 if parts.is_empty() {
273 String::new()
274 } else {
275 format!(" [{}]", parts.join(", "))
276 }
277}
278
279fn sml_escape(s: &str) -> String {
284 s.replace('&', "&")
285 .replace('<', "<")
286 .replace('>', ">")
287 .replace('"', """)
288 .replace('\'', "'")
289}
290
291fn render_content_blocks_sml(blocks: &serde_json::Value, fallback: &str) -> String {
297 let Some(arr) = blocks.as_array() else {
298 return sml_escape(fallback);
299 };
300 let mut out = String::new();
301 for block in arr {
302 let Some(obj) = block.as_object() else {
303 continue;
304 };
305 let kind = obj.get("type").and_then(|v| v.as_str()).unwrap_or("");
306 match kind {
307 "text" => {
308 let text = obj.get("text").and_then(|v| v.as_str()).unwrap_or("");
309 out.push_str(&format!("<text>{}</text>", sml_escape(text)));
310 }
311 "tool_use" => {
312 let name = obj.get("name").and_then(|v| v.as_str()).unwrap_or("");
313 let id = obj.get("id").and_then(|v| v.as_str()).unwrap_or("");
314 let input_json = obj
315 .get("input")
316 .map(|v| serde_json::to_string(v).unwrap_or_else(|_| "{}".into()))
317 .unwrap_or_else(|| "{}".into());
318 out.push_str(&format!(
319 "<tool_use name=\"{}\" id=\"{}\"><args format=\"json\">{}</args></tool_use>",
320 sml_escape(name),
321 sml_escape(id),
322 sml_escape(&input_json)
323 ));
324 }
325 "tool_result" => {
326 let tool_use_id = obj
327 .get("tool_use_id")
328 .and_then(|v| v.as_str())
329 .unwrap_or("");
330 let content = obj.get("content").and_then(|v| v.as_str()).unwrap_or("");
331 let is_error = obj
332 .get("is_error")
333 .and_then(|v| v.as_bool())
334 .unwrap_or(false);
335 out.push_str(&format!(
336 "<tool_result tool_use_id=\"{}\" is_error=\"{}\">{}</tool_result>",
337 sml_escape(tool_use_id),
338 is_error,
339 sml_escape(content)
340 ));
341 }
342 _ => {}
343 }
344 }
345 if out.is_empty() {
346 sml_escape(fallback)
347 } else {
348 out
349 }
350}
351
352fn toon_escape(s: &str) -> String {
355 if s.is_empty() {
356 return "\"\"".to_string();
357 }
358 let needs_quoting = s != s.trim()
359 || matches!(s, "true" | "false" | "null")
360 || toon_looks_numeric(s)
361 || s.contains(':')
362 || s.contains('"')
363 || s.contains('\\')
364 || s.contains('[')
365 || s.contains(']')
366 || s.contains('{')
367 || s.contains('}')
368 || s.contains(',')
369 || s.contains('\n')
370 || s.contains('\r')
371 || s.contains('\t')
372 || s.starts_with('-');
373 if needs_quoting {
374 let escaped = s
375 .replace('\\', "\\\\")
376 .replace('"', "\\\"")
377 .replace('\n', "\\n")
378 .replace('\r', "\\r")
379 .replace('\t', "\\t");
380 format!("\"{}\"", escaped)
381 } else {
382 s.to_string()
383 }
384}
385
386fn toon_looks_numeric(s: &str) -> bool {
388 let s = s.strip_prefix('-').unwrap_or(s);
389 if s.is_empty() {
390 return false;
391 }
392 if s.len() > 1
394 && s.starts_with('0')
395 && !s.starts_with("0.")
396 && !s.starts_with("0e")
397 && !s.starts_with("0E")
398 {
399 return true;
400 }
401 s.parse::<f64>().is_ok()
402 && s.chars()
403 .all(|c| c.is_ascii_digit() || c == '.' || c == 'e' || c == 'E' || c == '+' || c == '-')
404}
405
406pub fn toon_columns(gt: &GrainType) -> &'static [&'static str] {
412 dejadb_core::types::registry::meta(*gt).toon_columns
413}
414
415fn toon_canonicalize_number(n: f64) -> String {
418 if n.is_nan() || n.is_infinite() {
419 return "null".to_string();
420 }
421 format!("{}", n)
423}
424
425fn truncate(s: &str, max_chars: usize) -> &str {
427 if s.len() <= max_chars {
428 s
429 } else {
430 let mut end = max_chars.saturating_sub(3).min(s.len());
431 while end > 0 && !s.is_char_boundary(end) {
433 end -= 1;
434 }
435 &s[..end]
436 }
437}
438
439struct FactRenderer;
444
445impl GrainRenderer for FactRenderer {
446 fn grain_type(&self) -> GrainType {
447 GrainType::Fact
448 }
449
450 fn render(&self, grain: &DeserializedGrain, policy: &FormatPolicy) -> String {
451 let subject = grain.get_str("subject").unwrap_or("?");
452 let relation = grain.get_str("relation").unwrap_or("?");
453 let object = grain.get_str("object").unwrap_or("?");
454 let meta = extract_metadata(grain, policy.metadata);
455
456 match &policy.format {
457 OutputFormat::Sml => {
458 let attrs = meta
459 .as_ref()
460 .map(|m| format_meta_sml_attrs(m, policy.metadata))
461 .unwrap_or_default();
462 format!(
463 "<fact{attrs}>{} {} {}</fact>",
464 sml_escape(subject),
465 sml_escape(relation),
466 sml_escape(object)
467 )
468 }
469 OutputFormat::Markdown => {
470 let suffix = meta
471 .as_ref()
472 .map(|m| format_meta_markdown(m, policy.metadata))
473 .unwrap_or_default();
474 format!("**{subject}** {relation} {object}{suffix}")
475 }
476 OutputFormat::PlainText => {
477 let suffix = meta
478 .as_ref()
479 .map(|m| format_meta_plain(m, policy.metadata))
480 .unwrap_or_default();
481 format!("{subject} {relation} {object}{suffix}")
482 }
483 OutputFormat::Json => {
484 let mut obj = serde_json::json!({
485 "type": "fact",
486 "subject": subject,
487 "relation": relation,
488 "object": object,
489 });
490 if let Some(ref m) = meta {
491 add_json_metadata(&mut obj, m, policy.metadata);
492 }
493 obj.to_string()
494 }
495 OutputFormat::Toon => {
496 let content = if !relation.is_empty() && !object.is_empty() {
498 format!("{relation} {object}")
499 } else if !object.is_empty() {
500 object.to_string()
501 } else {
502 relation.to_string()
503 };
504 let confidence_str = grain
505 .get_f64("confidence")
506 .map(toon_canonicalize_number)
507 .unwrap_or_default();
508 format!(
509 "{},{},{}",
510 toon_escape(subject),
511 toon_escape(&content),
512 confidence_str
513 )
514 }
515 }
516 }
517
518 fn render_summary(&self, grain: &DeserializedGrain, _policy: &FormatPolicy) -> String {
519 let s = grain.get_str("subject").unwrap_or("?");
520 let r = grain.get_str("relation").unwrap_or("?");
521 let o = grain.get_str("object").unwrap_or("?");
522 format!("{s} {r} {o}")
523 }
524
525 fn context_priority(&self, grain: &DeserializedGrain, hit: &SearchHit) -> f32 {
526 adjusted_priority(0.7, grain, hit)
527 }
528}
529
530struct EventRenderer;
535
536impl GrainRenderer for EventRenderer {
537 fn grain_type(&self) -> GrainType {
538 GrainType::Event
539 }
540
541 fn render(&self, grain: &DeserializedGrain, policy: &FormatPolicy) -> String {
542 let content = grain.get_str("content").unwrap_or("");
543 let display = truncate(content, 500);
544 let meta = extract_metadata(grain, policy.metadata);
545
546 match &policy.format {
547 OutputFormat::Sml => {
548 let attrs = meta
549 .as_ref()
550 .map(|m| format_meta_sml_attrs(m, policy.metadata))
551 .unwrap_or_default();
552 if let Some(blocks_json) = grain.fields.get("content_blocks") {
553 let inner = render_content_blocks_sml(blocks_json, display);
554 format!("<event{attrs}>{inner}</event>")
555 } else {
556 format!("<event{attrs}>{}</event>", sml_escape(display))
557 }
558 }
559 OutputFormat::Markdown => {
560 let suffix = meta
561 .as_ref()
562 .map(|m| format_meta_markdown(m, policy.metadata))
563 .unwrap_or_default();
564 format!("{display}{suffix}")
565 }
566 OutputFormat::PlainText => {
567 let suffix = meta
568 .as_ref()
569 .map(|m| format_meta_plain(m, policy.metadata))
570 .unwrap_or_default();
571 format!("{display}{suffix}")
572 }
573 OutputFormat::Json => {
574 let mut obj = serde_json::json!({
575 "type": "event",
576 "content": display,
577 });
578 if let Some(ref m) = meta {
579 add_json_metadata(&mut obj, m, policy.metadata);
580 }
581 obj.to_string()
582 }
583 OutputFormat::Toon => {
584 let role = grain
586 .get_str("role")
587 .or_else(|| grain.get_str("speaker"))
588 .unwrap_or("user");
589 let time = if grain.header.created_at_sec > 0 {
590 format_date(grain.header.created_at_sec)
591 } else {
592 String::new()
593 };
594 format!(
595 "{},{},{}",
596 toon_escape(role),
597 toon_escape(&time),
598 toon_escape(display)
599 )
600 }
601 }
602 }
603
604 fn render_summary(&self, grain: &DeserializedGrain, _policy: &FormatPolicy) -> String {
605 let content = grain.get_str("content").unwrap_or("");
606 truncate(content, 80).to_string()
607 }
608
609 fn context_priority(&self, grain: &DeserializedGrain, hit: &SearchHit) -> f32 {
610 adjusted_priority(0.6, grain, hit)
611 }
612}
613
614struct StateRenderer;
619
620impl GrainRenderer for StateRenderer {
621 fn grain_type(&self) -> GrainType {
622 GrainType::State
623 }
624
625 fn render(&self, grain: &DeserializedGrain, policy: &FormatPolicy) -> String {
626 let label = state_label(grain);
627 let meta = extract_metadata(grain, policy.metadata);
628
629 match &policy.format {
630 OutputFormat::Sml => {
631 let attrs = meta
632 .as_ref()
633 .map(|m| format_meta_sml_attrs(m, policy.metadata))
634 .unwrap_or_default();
635 format!("<state{attrs}>{}</state>", sml_escape(&label))
636 }
637 OutputFormat::Markdown => {
638 let suffix = meta
639 .as_ref()
640 .map(|m| format_meta_markdown(m, policy.metadata))
641 .unwrap_or_default();
642 format!("**State**: {label}{suffix}")
643 }
644 OutputFormat::PlainText => {
645 let suffix = meta
646 .as_ref()
647 .map(|m| format_meta_plain(m, policy.metadata))
648 .unwrap_or_default();
649 format!("State: {label}{suffix}")
650 }
651 OutputFormat::Json => {
652 let mut obj = serde_json::json!({
653 "type": "state",
654 "label": label,
655 });
656 if let Some(ctx) = grain.fields.get("context_data") {
658 obj["context_data"] = ctx.clone();
659 }
660 if let Some(ref m) = meta {
661 add_json_metadata(&mut obj, m, policy.metadata);
662 }
663 obj.to_string()
664 }
665 OutputFormat::Toon => {
666 let content_summary = if let Some(ctx) = grain.fields.get("context_data") {
668 if let Some(obj) = ctx.as_object() {
669 let summary: Vec<String> = obj
670 .iter()
671 .filter(|(k, _)| {
672 !matches!(k.as_str(), "label" | "description" | "title" | "name")
673 })
674 .take(3)
675 .map(|(k, v)| {
676 let val = match v {
677 serde_json::Value::String(s) => s.clone(),
678 _ => v.to_string(),
679 };
680 format!("{k}={val}")
681 })
682 .collect();
683 if summary.is_empty() {
684 label.clone()
685 } else {
686 summary.join("; ")
687 }
688 } else {
689 label.clone()
690 }
691 } else {
692 label.clone()
693 };
694 format!("{},{}", toon_escape(&label), toon_escape(&content_summary))
695 }
696 }
697 }
698
699 fn render_summary(&self, grain: &DeserializedGrain, _policy: &FormatPolicy) -> String {
700 state_label(grain)
701 }
702
703 fn context_priority(&self, grain: &DeserializedGrain, hit: &SearchHit) -> f32 {
704 adjusted_priority(0.9, grain, hit)
705 }
706}
707
708fn state_label(grain: &DeserializedGrain) -> String {
709 if let Some(ctx) = grain.fields.get("context_data") {
711 for key in &["label", "description", "title", "name"] {
712 if let Some(s) = ctx.get(key).and_then(|v| v.as_str()) {
713 if !s.is_empty() {
714 return s.to_string();
715 }
716 }
717 }
718 if let Some(obj) = ctx.as_object() {
720 return format!("{} keys", obj.len());
721 }
722 }
723 "state".to_string()
724}
725
726struct WorkflowRenderer;
731
732impl GrainRenderer for WorkflowRenderer {
733 fn grain_type(&self) -> GrainType {
734 GrainType::Workflow
735 }
736
737 fn render(&self, grain: &DeserializedGrain, policy: &FormatPolicy) -> String {
738 let trigger = grain.get_str("trigger").unwrap_or("workflow");
739 let node_count = grain
740 .fields
741 .get("nodes")
742 .and_then(|v| v.as_array())
743 .map(|a| a.len())
744 .unwrap_or(0);
745 let edge_count = grain
746 .fields
747 .get("edges")
748 .and_then(|v| v.as_array())
749 .map(|a| a.len())
750 .unwrap_or(0);
751 let meta = extract_metadata(grain, policy.metadata);
752
753 match &policy.format {
754 OutputFormat::Sml => {
755 let attrs = meta
756 .as_ref()
757 .map(|m| format_meta_sml_attrs(m, policy.metadata))
758 .unwrap_or_default();
759 format!(
760 "<workflow nodes=\"{node_count}\" edges=\"{edge_count}\"{attrs}>{}</workflow>",
761 sml_escape(trigger)
762 )
763 }
764 OutputFormat::Markdown => {
765 let suffix = meta
766 .as_ref()
767 .map(|m| format_meta_markdown(m, policy.metadata))
768 .unwrap_or_default();
769 format!("{trigger} ({node_count} nodes, {edge_count} edges){suffix}")
770 }
771 OutputFormat::PlainText => {
772 let suffix = meta
773 .as_ref()
774 .map(|m| format_meta_plain(m, policy.metadata))
775 .unwrap_or_default();
776 format!("{trigger} ({node_count} nodes, {edge_count} edges){suffix}")
777 }
778 OutputFormat::Json => {
779 let mut obj = serde_json::json!({
780 "type": "workflow",
781 "trigger": trigger,
782 "node_count": node_count,
783 "edge_count": edge_count,
784 });
785 if let Some(ref m) = meta {
786 add_json_metadata(&mut obj, m, policy.metadata);
787 }
788 obj.to_string()
789 }
790 OutputFormat::Toon => {
791 let content = format!("{node_count} nodes, {edge_count} edges");
793 format!("{},{}", toon_escape(trigger), toon_escape(&content))
794 }
795 }
796 }
797
798 fn render_summary(&self, grain: &DeserializedGrain, _policy: &FormatPolicy) -> String {
799 let trigger = grain.get_str("trigger").unwrap_or("workflow");
800 let node_count = grain
801 .fields
802 .get("nodes")
803 .and_then(|v| v.as_array())
804 .map(|a| a.len())
805 .unwrap_or(0);
806 let edge_count = grain
807 .fields
808 .get("edges")
809 .and_then(|v| v.as_array())
810 .map(|a| a.len())
811 .unwrap_or(0);
812 format!("{trigger} ({node_count} nodes, {edge_count} edges)")
813 }
814
815 fn context_priority(&self, grain: &DeserializedGrain, hit: &SearchHit) -> f32 {
816 adjusted_priority(0.5, grain, hit)
817 }
818}
819
820struct ToolRenderer;
825
826impl GrainRenderer for ToolRenderer {
827 fn grain_type(&self) -> GrainType {
828 GrainType::Tool
829 }
830
831 fn render(&self, grain: &DeserializedGrain, policy: &FormatPolicy) -> String {
832 let tool = grain.get_str("tool_name").unwrap_or("unknown");
833 let is_error = grain.get_bool("is_error").unwrap_or(false);
834 let status = if is_error { "FAIL" } else { "OK" };
835 let content = grain
836 .get_str("tool_content")
837 .or_else(|| grain.get_str("content"))
838 .unwrap_or("");
839 let error_msg = grain.get_str("error");
840 let duration = grain.get_u64("duration_ms");
841 let meta = extract_metadata(grain, policy.metadata);
842
843 let dur_str = duration.map(|d| format!(" ({d}ms)")).unwrap_or_default();
844 let result_str = if is_error {
845 error_msg.unwrap_or(content)
846 } else {
847 content
848 };
849
850 match &policy.format {
851 OutputFormat::Sml => {
852 let attrs = meta
853 .as_ref()
854 .map(|m| format_meta_sml_attrs(m, policy.metadata))
855 .unwrap_or_default();
856 let mut sml = format!(
857 "<tool tool=\"{}\" status=\"{}\"{attrs}>",
858 sml_escape(tool),
859 status.to_lowercase()
860 );
861 if !result_str.is_empty() {
862 sml.push_str(&sml_escape(truncate(result_str, 300)));
863 }
864 if let Some(d) = duration {
865 sml.push_str(&format!(" ({d}ms)"));
866 }
867 sml.push_str("</tool>");
868 sml
869 }
870 OutputFormat::Markdown => {
871 let suffix = meta
872 .as_ref()
873 .map(|m| format_meta_markdown(m, policy.metadata))
874 .unwrap_or_default();
875 let result_part = if !result_str.is_empty() {
876 format!(": {}", truncate(result_str, 200))
877 } else {
878 String::new()
879 };
880 format!("`{tool}` [{status}]{dur_str}{result_part}{suffix}")
881 }
882 OutputFormat::PlainText => {
883 let suffix = meta
884 .as_ref()
885 .map(|m| format_meta_plain(m, policy.metadata))
886 .unwrap_or_default();
887 let result_part = if !result_str.is_empty() {
888 format!(": {}", truncate(result_str, 200))
889 } else {
890 String::new()
891 };
892 format!("{tool} [{status}]{dur_str}{result_part}{suffix}")
893 }
894 OutputFormat::Json => {
895 let mut obj = serde_json::json!({
896 "type": "tool",
897 "tool_name": tool,
898 "status": status.to_lowercase(),
899 });
900 if !result_str.is_empty() {
901 obj["content"] = serde_json::Value::String(result_str.to_string());
902 }
903 if let Some(d) = duration {
904 obj["duration_ms"] = serde_json::json!(d);
905 }
906 if let Some(e) = error_msg {
907 obj["error"] = serde_json::Value::String(e.to_string());
908 }
909 if let Some(ref m) = meta {
910 add_json_metadata(&mut obj, m, policy.metadata);
911 }
912 obj.to_string()
913 }
914 OutputFormat::Toon => {
915 let phase = status.to_lowercase();
917 let content_val = if !result_str.is_empty() {
918 truncate(result_str, 300)
919 } else {
920 ""
921 };
922 format!(
923 "{},{},{}",
924 toon_escape(tool),
925 toon_escape(&phase),
926 toon_escape(content_val)
927 )
928 }
929 }
930 }
931
932 fn render_summary(&self, grain: &DeserializedGrain, _policy: &FormatPolicy) -> String {
933 let tool = grain.get_str("tool_name").unwrap_or("unknown");
934 let is_error = grain.get_bool("is_error").unwrap_or(false);
935 let status = if is_error { "FAIL" } else { "OK" };
936 format!("{tool} [{status}]")
937 }
938
939 fn context_priority(&self, grain: &DeserializedGrain, hit: &SearchHit) -> f32 {
940 let is_error = grain.get_bool("is_error").unwrap_or(false);
941 let error_boost = if is_error { 0.15 } else { 0.0 };
942 adjusted_priority(0.5 + error_boost, grain, hit)
943 }
944}
945
946struct ObservationRenderer;
951
952impl GrainRenderer for ObservationRenderer {
953 fn grain_type(&self) -> GrainType {
954 GrainType::Observation
955 }
956
957 fn render(&self, grain: &DeserializedGrain, policy: &FormatPolicy) -> String {
958 let observer = grain.get_str("observer_id").unwrap_or("?");
959 let subject = grain.get_str("subject").unwrap_or("");
960 let object = grain.get_str("object").unwrap_or("");
961 let meta = extract_metadata(grain, policy.metadata);
962
963 let content = if !subject.is_empty() && !object.is_empty() {
964 format!("{subject}: {object}")
965 } else if !subject.is_empty() {
966 subject.to_string()
967 } else if !object.is_empty() {
968 object.to_string()
969 } else {
970 String::new()
971 };
972
973 match &policy.format {
974 OutputFormat::Sml => {
975 let attrs = meta
976 .as_ref()
977 .map(|m| format_meta_sml_attrs(m, policy.metadata))
978 .unwrap_or_default();
979 format!(
980 "<observation observer=\"{}\"{attrs}>{}</observation>",
981 sml_escape(observer),
982 sml_escape(&content)
983 )
984 }
985 OutputFormat::Markdown => {
986 let suffix = meta
987 .as_ref()
988 .map(|m| format_meta_markdown(m, policy.metadata))
989 .unwrap_or_default();
990 if content.is_empty() {
991 format!("*{observer}*{suffix}")
992 } else {
993 format!("*{observer}*: {content}{suffix}")
994 }
995 }
996 OutputFormat::PlainText => {
997 let suffix = meta
998 .as_ref()
999 .map(|m| format_meta_plain(m, policy.metadata))
1000 .unwrap_or_default();
1001 if content.is_empty() {
1002 format!("{observer}{suffix}")
1003 } else {
1004 format!("{observer}: {content}{suffix}")
1005 }
1006 }
1007 OutputFormat::Json => {
1008 let mut obj = serde_json::json!({
1009 "type": "observation",
1010 "observer_id": observer,
1011 });
1012 if !subject.is_empty() {
1013 obj["subject"] = serde_json::Value::String(subject.to_string());
1014 }
1015 if !object.is_empty() {
1016 obj["object"] = serde_json::Value::String(object.to_string());
1017 }
1018 if let Some(ref m) = meta {
1019 add_json_metadata(&mut obj, m, policy.metadata);
1020 }
1021 obj.to_string()
1022 }
1023 OutputFormat::Toon => {
1024 format!("{},{}", toon_escape(observer), toon_escape(&content))
1026 }
1027 }
1028 }
1029
1030 fn render_summary(&self, grain: &DeserializedGrain, _policy: &FormatPolicy) -> String {
1031 let observer = grain.get_str("observer_id").unwrap_or("?");
1032 let subject = grain.get_str("subject").unwrap_or("");
1033 if subject.is_empty() {
1034 observer.to_string()
1035 } else {
1036 format!("{observer}: {subject}")
1037 }
1038 }
1039
1040 fn context_priority(&self, grain: &DeserializedGrain, hit: &SearchHit) -> f32 {
1041 adjusted_priority(0.6, grain, hit)
1042 }
1043}
1044
1045struct GoalRenderer;
1050
1051impl GrainRenderer for GoalRenderer {
1052 fn grain_type(&self) -> GrainType {
1053 GrainType::Goal
1054 }
1055
1056 fn render(&self, grain: &DeserializedGrain, policy: &FormatPolicy) -> String {
1057 let desc = grain.get_str("description").unwrap_or("?");
1058 let state = grain.get_str("goal_state").unwrap_or("active");
1059 let priority = grain.get_str("priority").unwrap_or("medium");
1060 let progress = grain.get_f64("progress");
1061 let criteria = grain.get_str("criteria");
1062 let meta = extract_metadata(grain, policy.metadata);
1063
1064 let progress_str = progress
1065 .map(|p| format!(" ({:.0}%)", p * 100.0))
1066 .unwrap_or_default();
1067
1068 match &policy.format {
1069 OutputFormat::Sml => {
1070 let attrs = meta
1071 .as_ref()
1072 .map(|m| format_meta_sml_attrs(m, policy.metadata))
1073 .unwrap_or_default();
1074 let mut sml = format!(
1075 "<goal state=\"{}\" priority=\"{}\"{attrs}>{}</goal>",
1076 sml_escape(state),
1077 sml_escape(priority),
1078 sml_escape(desc)
1079 );
1080 if let Some(c) = criteria {
1081 sml = sml.replace(
1083 "</goal>",
1084 &format!("<criteria>{}</criteria></goal>", sml_escape(c)),
1085 );
1086 }
1087 sml
1088 }
1089 OutputFormat::Markdown => {
1090 let suffix = meta
1091 .as_ref()
1092 .map(|m| format_meta_markdown(m, policy.metadata))
1093 .unwrap_or_default();
1094 format!("[{priority}/{state}] {desc}{progress_str}{suffix}")
1095 }
1096 OutputFormat::PlainText => {
1097 let suffix = meta
1098 .as_ref()
1099 .map(|m| format_meta_plain(m, policy.metadata))
1100 .unwrap_or_default();
1101 format!("[{priority}/{state}] {desc}{progress_str}{suffix}")
1102 }
1103 OutputFormat::Json => {
1104 let mut obj = serde_json::json!({
1105 "type": "goal",
1106 "description": desc,
1107 "goal_state": state,
1108 "priority": priority,
1109 });
1110 if let Some(p) = progress {
1111 obj["progress"] = serde_json::json!(p);
1112 }
1113 if let Some(c) = criteria {
1114 obj["criteria"] = serde_json::Value::String(c.to_string());
1115 }
1116 if let Some(ref m) = meta {
1117 add_json_metadata(&mut obj, m, policy.metadata);
1118 }
1119 obj.to_string()
1120 }
1121 OutputFormat::Toon => {
1122 let subject = grain.get_str("subject").unwrap_or(desc);
1124 format!(
1125 "{},{},{}",
1126 toon_escape(subject),
1127 toon_escape(desc),
1128 toon_escape(state)
1129 )
1130 }
1131 }
1132 }
1133
1134 fn render_summary(&self, grain: &DeserializedGrain, _policy: &FormatPolicy) -> String {
1135 let desc = grain.get_str("description").unwrap_or("?");
1136 let state = grain.get_str("goal_state").unwrap_or("active");
1137 let priority = grain.get_str("priority").unwrap_or("medium");
1138 format!("[{priority}/{state}] {desc}")
1139 }
1140
1141 fn context_priority(&self, grain: &DeserializedGrain, hit: &SearchHit) -> f32 {
1142 let state_mod = match grain.get_str("goal_state") {
1143 Some("active") => 0.15,
1144 Some("suspended") => 0.05,
1145 Some("satisfied" | "failed") => -0.2,
1146 _ => 0.0,
1147 };
1148 let priority_mod = match grain.get_str("priority") {
1149 Some("critical") => 0.1,
1150 Some("high") => 0.05,
1151 Some("low") => -0.1,
1152 _ => 0.0,
1153 };
1154 adjusted_priority(0.8 + state_mod + priority_mod, grain, hit)
1155 }
1156}
1157
1158struct ReasoningRenderer;
1163
1164impl GrainRenderer for ReasoningRenderer {
1165 fn grain_type(&self) -> GrainType {
1166 GrainType::Reasoning
1167 }
1168
1169 fn render(&self, grain: &DeserializedGrain, policy: &FormatPolicy) -> String {
1170 let conclusion = grain.get_str("conclusion").unwrap_or("");
1171 let method = grain.get_str("inference_method");
1172 let premise_count = grain
1173 .fields
1174 .get("premises")
1175 .and_then(|v| v.as_array())
1176 .map(|a| a.len())
1177 .unwrap_or(0);
1178 let meta = extract_metadata(grain, policy.metadata);
1179
1180 let method_str = method.map(|m| format!(" ({m})")).unwrap_or_default();
1181
1182 match &policy.format {
1183 OutputFormat::Sml => {
1184 let attrs = meta
1185 .as_ref()
1186 .map(|m| format_meta_sml_attrs(m, policy.metadata))
1187 .unwrap_or_default();
1188 let mut sml = format!("<reasoning{attrs}>");
1189 if !conclusion.is_empty() {
1190 sml.push_str(&format!(
1191 "<conclusion>{}</conclusion>",
1192 sml_escape(conclusion)
1193 ));
1194 }
1195 if premise_count > 0 {
1196 sml.push_str(&format!("<premises count=\"{premise_count}\"/>"));
1197 }
1198 sml.push_str("</reasoning>");
1199 sml
1200 }
1201 OutputFormat::Markdown => {
1202 let suffix = meta
1203 .as_ref()
1204 .map(|m| format_meta_markdown(m, policy.metadata))
1205 .unwrap_or_default();
1206 if conclusion.is_empty() {
1207 format!("Reasoning{method_str} ({premise_count} premises){suffix}")
1208 } else {
1209 format!("{conclusion}{method_str}{suffix}")
1210 }
1211 }
1212 OutputFormat::PlainText => {
1213 let suffix = meta
1214 .as_ref()
1215 .map(|m| format_meta_plain(m, policy.metadata))
1216 .unwrap_or_default();
1217 if conclusion.is_empty() {
1218 format!("Reasoning{method_str} ({premise_count} premises){suffix}")
1219 } else {
1220 format!("{conclusion}{method_str}{suffix}")
1221 }
1222 }
1223 OutputFormat::Json => {
1224 let mut obj = serde_json::json!({
1225 "type": "reasoning",
1226 "premise_count": premise_count,
1227 });
1228 if !conclusion.is_empty() {
1229 obj["conclusion"] = serde_json::Value::String(conclusion.to_string());
1230 }
1231 if let Some(m) = method {
1232 obj["inference_method"] = serde_json::Value::String(m.to_string());
1233 }
1234 if let Some(ref m) = meta {
1235 add_json_metadata(&mut obj, m, policy.metadata);
1236 }
1237 obj.to_string()
1238 }
1239 OutputFormat::Toon => {
1240 let type_val = method.unwrap_or("reasoning");
1242 let content_val = if conclusion.is_empty() {
1243 format!("{} premises", premise_count)
1244 } else {
1245 conclusion.to_string()
1246 };
1247 format!("{},{}", toon_escape(type_val), toon_escape(&content_val))
1248 }
1249 }
1250 }
1251
1252 fn render_summary(&self, grain: &DeserializedGrain, _policy: &FormatPolicy) -> String {
1253 grain
1254 .get_str("conclusion")
1255 .unwrap_or("reasoning")
1256 .to_string()
1257 }
1258
1259 fn context_priority(&self, grain: &DeserializedGrain, hit: &SearchHit) -> f32 {
1260 adjusted_priority(0.7, grain, hit)
1261 }
1262}
1263
1264struct ConsensusRenderer;
1269
1270impl GrainRenderer for ConsensusRenderer {
1271 fn grain_type(&self) -> GrainType {
1272 GrainType::Consensus
1273 }
1274
1275 fn render(&self, grain: &DeserializedGrain, policy: &FormatPolicy) -> String {
1276 let agreed = grain.get_str("agreed_content").unwrap_or("");
1277 let agree_count = grain.get_i64("agreement_count").unwrap_or(0);
1278 let dissent_count = grain.get_i64("dissent_count").unwrap_or(0);
1279 let meta = extract_metadata(grain, policy.metadata);
1280
1281 match &policy.format {
1282 OutputFormat::Sml => {
1283 let attrs = meta
1284 .as_ref()
1285 .map(|m| format_meta_sml_attrs(m, policy.metadata))
1286 .unwrap_or_default();
1287 format!(
1288 "<consensus agreed=\"{agree_count}\" dissent=\"{dissent_count}\"{attrs}>{}</consensus>",
1289 sml_escape(agreed)
1290 )
1291 }
1292 OutputFormat::Markdown => {
1293 let suffix = meta
1294 .as_ref()
1295 .map(|m| format_meta_markdown(m, policy.metadata))
1296 .unwrap_or_default();
1297 let vote_str = if agree_count > 0 || dissent_count > 0 {
1298 format!(" ({agree_count} agreed, {dissent_count} dissent)")
1299 } else {
1300 String::new()
1301 };
1302 if agreed.is_empty() {
1303 format!("Consensus{vote_str}{suffix}")
1304 } else {
1305 format!("{agreed}{vote_str}{suffix}")
1306 }
1307 }
1308 OutputFormat::PlainText => {
1309 let suffix = meta
1310 .as_ref()
1311 .map(|m| format_meta_plain(m, policy.metadata))
1312 .unwrap_or_default();
1313 let vote_str = if agree_count > 0 || dissent_count > 0 {
1314 format!(" ({agree_count}/{dissent_count})")
1315 } else {
1316 String::new()
1317 };
1318 if agreed.is_empty() {
1319 format!("Consensus{vote_str}{suffix}")
1320 } else {
1321 format!("{agreed}{vote_str}{suffix}")
1322 }
1323 }
1324 OutputFormat::Json => {
1325 let mut obj = serde_json::json!({
1326 "type": "consensus",
1327 "agreement_count": agree_count,
1328 "dissent_count": dissent_count,
1329 });
1330 if !agreed.is_empty() {
1331 obj["agreed_content"] = serde_json::Value::String(agreed.to_string());
1332 }
1333 if let Some(ref m) = meta {
1334 add_json_metadata(&mut obj, m, policy.metadata);
1335 }
1336 obj.to_string()
1337 }
1338 OutputFormat::Toon => {
1339 let threshold = grain.get_str("threshold").unwrap_or("-");
1341 format!(
1342 "{},{},{}",
1343 toon_escape(threshold),
1344 agree_count,
1345 toon_escape(agreed)
1346 )
1347 }
1348 }
1349 }
1350
1351 fn render_summary(&self, grain: &DeserializedGrain, _policy: &FormatPolicy) -> String {
1352 grain
1353 .get_str("agreed_content")
1354 .unwrap_or("consensus")
1355 .to_string()
1356 }
1357
1358 fn context_priority(&self, grain: &DeserializedGrain, hit: &SearchHit) -> f32 {
1359 adjusted_priority(0.65, grain, hit)
1360 }
1361}
1362
1363struct ConsentRenderer;
1368
1369impl GrainRenderer for ConsentRenderer {
1370 fn grain_type(&self) -> GrainType {
1371 GrainType::Consent
1372 }
1373
1374 fn render(&self, grain: &DeserializedGrain, policy: &FormatPolicy) -> String {
1375 let subject = grain.get_str("subject_did").unwrap_or("?");
1376 let is_withdrawal = grain.get_bool("is_withdrawal").unwrap_or(false);
1377 let action = if is_withdrawal { "withdraws" } else { "grants" };
1378 let scope = grain.get_str("scope").unwrap_or("");
1379 let grantee = grain.get_str("grantee_did");
1380 let meta = extract_metadata(grain, policy.metadata);
1381
1382 match &policy.format {
1383 OutputFormat::Sml => {
1384 let attrs = meta
1385 .as_ref()
1386 .map(|m| format_meta_sml_attrs(m, policy.metadata))
1387 .unwrap_or_default();
1388 let mut sml = format!(
1389 "<consent action=\"{action}\" subject=\"{}\"{attrs}>",
1390 sml_escape(subject)
1391 );
1392 if !scope.is_empty() {
1393 sml.push_str(&format!("<scope>{}</scope>", sml_escape(scope)));
1394 }
1395 if let Some(g) = grantee {
1396 sml.push_str(&format!("<grantee>{}</grantee>", sml_escape(g)));
1397 }
1398 sml.push_str("</consent>");
1399 sml
1400 }
1401 OutputFormat::Markdown => {
1402 let suffix = meta
1403 .as_ref()
1404 .map(|m| format_meta_markdown(m, policy.metadata))
1405 .unwrap_or_default();
1406 let scope_part = if scope.is_empty() {
1407 String::new()
1408 } else {
1409 format!(" for {scope}")
1410 };
1411 format!("{subject} {action}{scope_part}{suffix}")
1412 }
1413 OutputFormat::PlainText => {
1414 let suffix = meta
1415 .as_ref()
1416 .map(|m| format_meta_plain(m, policy.metadata))
1417 .unwrap_or_default();
1418 let scope_part = if scope.is_empty() {
1419 String::new()
1420 } else {
1421 format!(" for {scope}")
1422 };
1423 format!("{subject} {action}{scope_part}{suffix}")
1424 }
1425 OutputFormat::Json => {
1426 let mut obj = serde_json::json!({
1427 "type": "consent",
1428 "subject_did": subject,
1429 "action": action,
1430 });
1431 if !scope.is_empty() {
1432 obj["scope"] = serde_json::Value::String(scope.to_string());
1433 }
1434 if let Some(g) = grantee {
1435 obj["grantee_did"] = serde_json::Value::String(g.to_string());
1436 }
1437 if let Some(ref m) = meta {
1438 add_json_metadata(&mut obj, m, policy.metadata);
1439 }
1440 obj.to_string()
1441 }
1442 OutputFormat::Toon => {
1443 let grantee_val = grantee.unwrap_or("-");
1445 format!(
1446 "{},{},{},{}",
1447 toon_escape(subject),
1448 toon_escape(grantee_val),
1449 toon_escape(action),
1450 toon_escape(scope)
1451 )
1452 }
1453 }
1454 }
1455
1456 fn render_summary(&self, grain: &DeserializedGrain, _policy: &FormatPolicy) -> String {
1457 let subject = grain.get_str("subject_did").unwrap_or("?");
1458 let is_withdrawal = grain.get_bool("is_withdrawal").unwrap_or(false);
1459 let action = if is_withdrawal { "withdraws" } else { "grants" };
1460 let scope = grain.get_str("scope").unwrap_or("");
1461 if scope.is_empty() {
1462 format!("{subject} {action}")
1463 } else {
1464 format!("{subject} {action} {scope}")
1465 }
1466 }
1467
1468 fn context_priority(&self, grain: &DeserializedGrain, hit: &SearchHit) -> f32 {
1469 adjusted_priority(0.95, grain, hit)
1471 }
1472}
1473
1474struct SkillRenderer;
1481
1482impl GrainRenderer for SkillRenderer {
1483 fn grain_type(&self) -> GrainType {
1484 GrainType::Skill
1485 }
1486
1487 fn render(&self, grain: &DeserializedGrain, policy: &FormatPolicy) -> String {
1488 let name = grain.get_str("name").unwrap_or("?");
1489 let description = grain.get_str("description").unwrap_or("");
1490 let domain = grain.get_str("domain");
1491 let proficiency = grain
1493 .get_f64("proficiency")
1494 .or_else(|| grain.get_f64("confidence"));
1495 let meta = extract_metadata(grain, policy.metadata);
1496
1497 match &policy.format {
1498 OutputFormat::Sml => {
1499 let attrs = meta
1500 .as_ref()
1501 .map(|m| format_meta_sml_attrs(m, policy.metadata))
1502 .unwrap_or_default();
1503 let domain_attr = domain
1504 .map(|d| format!(" domain=\"{}\"", sml_escape(d)))
1505 .unwrap_or_default();
1506 let mut sml = format!("<skill name=\"{}\"{domain_attr}{attrs}>", sml_escape(name));
1507 if !description.is_empty() {
1508 sml.push_str(&sml_escape(description));
1509 }
1510 sml.push_str("</skill>");
1511 sml
1512 }
1513 OutputFormat::Markdown => {
1514 let suffix = meta
1515 .as_ref()
1516 .map(|m| format_meta_markdown(m, policy.metadata))
1517 .unwrap_or_default();
1518 let domain_part = domain.map(|d| format!(" [{d}]")).unwrap_or_default();
1519 format!("**{name}**: {description}{domain_part}{suffix}")
1520 }
1521 OutputFormat::PlainText => {
1522 let suffix = meta
1523 .as_ref()
1524 .map(|m| format_meta_plain(m, policy.metadata))
1525 .unwrap_or_default();
1526 let domain_part = domain.map(|d| format!(" [{d}]")).unwrap_or_default();
1527 format!("{name}: {description}{domain_part}{suffix}")
1528 }
1529 OutputFormat::Json => {
1530 let mut obj = serde_json::json!({
1531 "type": "skill",
1532 "name": name,
1533 "description": description,
1534 });
1535 if let Some(d) = domain {
1536 obj["domain"] = serde_json::Value::String(d.to_string());
1537 }
1538 if let Some(p) = proficiency {
1539 obj["proficiency"] = serde_json::json!(p);
1540 }
1541 if let Some(ref m) = meta {
1542 add_json_metadata(&mut obj, m, policy.metadata);
1543 }
1544 obj.to_string()
1545 }
1546 OutputFormat::Toon => {
1547 let prof_str = proficiency
1549 .map(toon_canonicalize_number)
1550 .unwrap_or_default();
1551 format!(
1552 "{},{},{}",
1553 toon_escape(name),
1554 toon_escape(domain.unwrap_or("-")),
1555 toon_escape(&prof_str)
1556 )
1557 }
1558 }
1559 }
1560
1561 fn render_summary(&self, grain: &DeserializedGrain, _policy: &FormatPolicy) -> String {
1562 let name = grain.get_str("name").unwrap_or("?");
1563 match grain.get_str("domain") {
1564 Some(d) => format!("{name} [{d}]"),
1565 None => name.to_string(),
1566 }
1567 }
1568
1569 fn context_priority(&self, grain: &DeserializedGrain, hit: &SearchHit) -> f32 {
1570 adjusted_priority(0.6, grain, hit)
1571 }
1572}
1573
1574fn add_json_metadata(obj: &mut serde_json::Value, meta: &MetadataFragment, level: MetadataLevel) {
1579 if let Some(c) = meta.confidence {
1580 obj["confidence"] = serde_json::json!(c);
1581 }
1582 if meta.created_at_sec > 0 {
1583 obj["created_at"] = serde_json::Value::String(format_date(meta.created_at_sec));
1584 }
1585 if level == MetadataLevel::Full {
1586 obj["hash"] = serde_json::Value::String(meta.hash_hex[..16].to_string());
1587 if let Some(ref ns) = meta.namespace {
1588 obj["namespace"] = serde_json::Value::String(ns.clone());
1589 }
1590 if let Some(ref vs) = meta.verification_status {
1591 obj["verification_status"] = serde_json::Value::String(vs.clone());
1592 }
1593 if !meta.tags.is_empty() {
1594 obj["tags"] = serde_json::json!(meta.tags);
1595 }
1596 if let Some(ref st) = meta.source_type {
1597 obj["source_type"] = serde_json::Value::String(st.clone());
1598 }
1599 }
1600}
1601
1602#[cfg(test)]
1607mod tests {
1608 use super::*;
1609 use dejadb_core::format::deserialize::DeserializedGrain;
1610 use dejadb_core::format::header::MgHeader;
1611 use std::collections::HashMap;
1612
1613 fn test_grain(gt: GrainType, fields: Vec<(&str, serde_json::Value)>) -> DeserializedGrain {
1615 let mut map = HashMap::new();
1616 for (k, v) in fields {
1617 map.insert(k.to_string(), v);
1618 }
1619 DeserializedGrain {
1620 header: MgHeader {
1621 version: 1,
1622 flags: 0,
1623 grain_type: gt.type_byte(),
1624 ns_hash: 0,
1625 created_at_sec: 1740000000, },
1627 grain_type: gt,
1628 fields: map,
1629 hash: dejadb_core::error::Hash::from_bytes(&[0u8; 32]),
1630 }
1631 }
1632
1633 fn test_hit(grain: DeserializedGrain) -> SearchHit {
1634 SearchHit {
1635 hash: grain.hash,
1636 score: 0.85,
1637 grain,
1638 score_breakdown: None,
1639 #[cfg(feature = "rerank")]
1640 rerank_score: None,
1641 #[cfg(feature = "llm-rerank")]
1642 llm_rerank_score: None,
1643 explanation: None,
1644 scope_depth: None,
1645 source_namespace: None,
1646 relative_time: None,
1647 conflict_status: None,
1648 supersession_status: None,
1649 superseded_by_hash: None,
1650 recall_source: None,
1651 }
1652 }
1653
1654 #[test]
1655 fn test_fact_sml() {
1656 let grain = test_grain(
1657 GrainType::Fact,
1658 vec![
1659 ("subject", serde_json::json!("john")),
1660 ("relation", serde_json::json!("likes")),
1661 ("object", serde_json::json!("coffee")),
1662 ("confidence", serde_json::json!(0.95)),
1663 ],
1664 );
1665 let policy = FormatPolicy::new(OutputFormat::Sml).metadata(MetadataLevel::Minimal);
1666 let registry = RendererRegistry::new();
1667 let renderer = registry.get(GrainType::Fact).unwrap();
1668 let result = renderer.render(&grain, &policy);
1669 assert!(result.starts_with("<fact"));
1670 assert!(result.contains("confidence=\"0.95\""));
1671 assert!(result.contains("john likes coffee"));
1672 assert!(result.ends_with("</fact>"));
1673 }
1674
1675 #[test]
1676 fn test_fact_markdown() {
1677 let grain = test_grain(
1678 GrainType::Fact,
1679 vec![
1680 ("subject", serde_json::json!("john")),
1681 ("relation", serde_json::json!("likes")),
1682 ("object", serde_json::json!("coffee")),
1683 ],
1684 );
1685 let policy = FormatPolicy::new(OutputFormat::Markdown).metadata(MetadataLevel::None);
1686 let registry = RendererRegistry::new();
1687 let result = registry
1688 .get(GrainType::Fact)
1689 .unwrap()
1690 .render(&grain, &policy);
1691 assert_eq!(result, "**john** likes coffee");
1692 }
1693
1694 #[test]
1695 fn test_fact_summary() {
1696 let grain = test_grain(
1697 GrainType::Fact,
1698 vec![
1699 ("subject", serde_json::json!("john")),
1700 ("relation", serde_json::json!("likes")),
1701 ("object", serde_json::json!("coffee")),
1702 ],
1703 );
1704 let policy = FormatPolicy::default();
1705 let registry = RendererRegistry::new();
1706 let result = registry
1707 .get(GrainType::Fact)
1708 .unwrap()
1709 .render_summary(&grain, &policy);
1710 assert_eq!(result, "john likes coffee");
1711 }
1712
1713 #[test]
1714 fn test_tool_sml() {
1715 let grain = test_grain(
1716 GrainType::Tool,
1717 vec![
1718 ("tool_name", serde_json::json!("search_api")),
1719 ("is_error", serde_json::json!(false)),
1720 ("tool_content", serde_json::json!("12 results")),
1721 ("duration_ms", serde_json::json!(340)),
1722 ],
1723 );
1724 let policy = FormatPolicy::new(OutputFormat::Sml).metadata(MetadataLevel::None);
1725 let registry = RendererRegistry::new();
1726 let result = registry
1727 .get(GrainType::Tool)
1728 .unwrap()
1729 .render(&grain, &policy);
1730 assert!(result.contains("tool=\"search_api\""));
1731 assert!(result.contains("status=\"ok\""));
1732 assert!(result.contains("12 results"));
1733 assert!(result.contains("340ms"));
1734 }
1735
1736 #[test]
1737 fn test_goal_plaintext() {
1738 let grain = test_grain(
1739 GrainType::Goal,
1740 vec![
1741 ("description", serde_json::json!("Deploy v2")),
1742 ("goal_state", serde_json::json!("active")),
1743 ("priority", serde_json::json!("high")),
1744 ("progress", serde_json::json!(0.78)),
1745 ],
1746 );
1747 let policy = FormatPolicy::new(OutputFormat::PlainText).metadata(MetadataLevel::None);
1748 let registry = RendererRegistry::new();
1749 let result = registry
1750 .get(GrainType::Goal)
1751 .unwrap()
1752 .render(&grain, &policy);
1753 assert_eq!(result, "[high/active] Deploy v2 (78%)");
1754 }
1755
1756 #[test]
1757 fn test_consent_markdown() {
1758 let grain = test_grain(
1759 GrainType::Consent,
1760 vec![
1761 ("subject_did", serde_json::json!("did:john")),
1762 ("is_withdrawal", serde_json::json!(false)),
1763 ("scope", serde_json::json!("analytics")),
1764 ],
1765 );
1766 let policy = FormatPolicy::new(OutputFormat::Markdown).metadata(MetadataLevel::None);
1767 let registry = RendererRegistry::new();
1768 let result = registry
1769 .get(GrainType::Consent)
1770 .unwrap()
1771 .render(&grain, &policy);
1772 assert_eq!(result, "did:john grants for analytics");
1773 }
1774
1775 #[test]
1776 fn test_state_label_extraction() {
1777 let grain = test_grain(
1778 GrainType::State,
1779 vec![(
1780 "context_data",
1781 serde_json::json!({"label": "planning_phase", "data": {}}),
1782 )],
1783 );
1784 let policy = FormatPolicy::new(OutputFormat::PlainText).metadata(MetadataLevel::None);
1785 let registry = RendererRegistry::new();
1786 let result = registry
1787 .get(GrainType::State)
1788 .unwrap()
1789 .render(&grain, &policy);
1790 assert_eq!(result, "State: planning_phase");
1791 }
1792
1793 #[test]
1794 fn test_context_priority_consent_highest() {
1795 let grain = test_grain(
1796 GrainType::Consent,
1797 vec![("subject_did", serde_json::json!("did:x"))],
1798 );
1799 let hit = test_hit(grain.clone());
1800 let registry = RendererRegistry::new();
1801 let consent_pri = registry
1802 .get(GrainType::Consent)
1803 .unwrap()
1804 .context_priority(&grain, &hit);
1805
1806 let fact_grain = test_grain(GrainType::Fact, vec![("subject", serde_json::json!("x"))]);
1807 let fact_hit = test_hit(fact_grain.clone());
1808 let fact_pri = registry
1809 .get(GrainType::Fact)
1810 .unwrap()
1811 .context_priority(&fact_grain, &fact_hit);
1812
1813 assert!(
1814 consent_pri > fact_pri,
1815 "Consent ({consent_pri}) should outrank Fact ({fact_pri})"
1816 );
1817 }
1818
1819 #[test]
1820 fn test_json_format_includes_type() {
1821 let grain = test_grain(
1822 GrainType::Fact,
1823 vec![
1824 ("subject", serde_json::json!("john")),
1825 ("relation", serde_json::json!("likes")),
1826 ("object", serde_json::json!("coffee")),
1827 ],
1828 );
1829 let policy = FormatPolicy::new(OutputFormat::Json).metadata(MetadataLevel::None);
1830 let registry = RendererRegistry::new();
1831 let result = registry
1832 .get(GrainType::Fact)
1833 .unwrap()
1834 .render(&grain, &policy);
1835 let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
1836 assert_eq!(parsed["type"], "fact");
1837 assert_eq!(parsed["subject"], "john");
1838 }
1839
1840 #[test]
1841 fn test_format_date() {
1842 assert_eq!(format_date(1740000000), "2025-02-19");
1843 assert_eq!(format_date(0), "1970-01-01");
1844 assert_eq!(format_date(1609459200), "2021-01-01");
1845 }
1846
1847 #[test]
1848 fn test_sml_escape() {
1849 assert_eq!(sml_escape("a < b & c"), "a < b & c");
1850 assert_eq!(sml_escape("\"hello\""), ""hello"");
1851 }
1852
1853 #[test]
1854 fn test_fact_toon() {
1855 let grain = test_grain(
1856 GrainType::Fact,
1857 vec![
1858 ("subject", serde_json::json!("john")),
1859 ("relation", serde_json::json!("likes")),
1860 ("object", serde_json::json!("coffee")),
1861 ("confidence", serde_json::json!(0.95)),
1862 ],
1863 );
1864 let policy = FormatPolicy::new(OutputFormat::Toon).metadata(MetadataLevel::Minimal);
1865 let registry = RendererRegistry::new();
1866 let renderer = registry.get(GrainType::Fact).unwrap();
1867 let rendered = renderer.render(&grain, &policy);
1868 assert_eq!(rendered, "john,likes coffee,0.95");
1870 }
1871
1872 #[test]
1873 fn test_toon_escape() {
1874 assert_eq!(toon_escape("hello"), "hello");
1875 assert_eq!(toon_escape(""), "\"\"");
1876 assert_eq!(toon_escape("true"), "\"true\"");
1877 assert_eq!(toon_escape("has:colon"), "\"has:colon\"");
1878 assert_eq!(toon_escape("has,comma"), "\"has,comma\"");
1879 assert_eq!(toon_escape("has\"quote"), "\"has\\\"quote\"");
1880 assert_eq!(toon_escape("line\nbreak"), "\"line\\nbreak\"");
1881 assert_eq!(toon_escape("42"), "\"42\"");
1882 assert_eq!(toon_escape("simple text"), "simple text");
1883 }
1884
1885 #[test]
1886 fn test_toon_columns() {
1887 assert_eq!(
1888 toon_columns(&GrainType::Fact),
1889 &["subject", "content", "confidence"]
1890 );
1891 assert_eq!(
1892 toon_columns(&GrainType::Event),
1893 &["role", "time", "content"]
1894 );
1895 assert_eq!(
1896 toon_columns(&GrainType::Consent),
1897 &["grantor", "grantee", "action", "content"]
1898 );
1899 }
1900
1901 #[test]
1902 fn test_event_sml_renders_content_blocks_as_typed_tags() {
1903 let blocks = serde_json::json!([
1904 { "type": "text", "text": "hello <world>" },
1905 { "type": "tool_use", "id": "tu_1", "name": "search", "input": {"q": "a&b"} },
1906 { "type": "tool_result", "tool_use_id": "tu_1", "content": "done", "is_error": false },
1907 ]);
1908 let grain = test_grain(
1909 GrainType::Event,
1910 vec![
1911 ("content", serde_json::json!("fallback")),
1912 ("content_blocks", blocks),
1913 ],
1914 );
1915 let policy = FormatPolicy::new(OutputFormat::Sml).metadata(MetadataLevel::None);
1916 let registry = RendererRegistry::new();
1917 let out = registry
1918 .get(GrainType::Event)
1919 .unwrap()
1920 .render(&grain, &policy);
1921 assert!(out.starts_with("<event"));
1922 assert!(out.contains("<text>hello <world></text>"));
1923 assert!(out.contains("<tool_use name=\"search\" id=\"tu_1\">"));
1924 assert!(out.contains("<args format=\"json\">"));
1925 assert!(out.contains("a&b"));
1926 assert!(
1927 out.contains("<tool_result tool_use_id=\"tu_1\" is_error=\"false\">done</tool_result>")
1928 );
1929 assert!(!out.contains("fallback"));
1931 }
1932
1933 #[test]
1934 fn test_event_sml_empty_blocks_falls_back_to_content() {
1935 let grain = test_grain(
1936 GrainType::Event,
1937 vec![
1938 ("content", serde_json::json!("plain text")),
1939 ("content_blocks", serde_json::json!([])),
1940 ],
1941 );
1942 let policy = FormatPolicy::new(OutputFormat::Sml).metadata(MetadataLevel::None);
1943 let registry = RendererRegistry::new();
1944 let out = registry
1945 .get(GrainType::Event)
1946 .unwrap()
1947 .render(&grain, &policy);
1948 assert!(out.contains("plain text"));
1949 }
1950
1951 #[test]
1952 fn test_all_renderers_registered() {
1953 let registry = RendererRegistry::new();
1954 let types = [
1955 GrainType::Fact,
1956 GrainType::Event,
1957 GrainType::State,
1958 GrainType::Workflow,
1959 GrainType::Tool,
1960 GrainType::Observation,
1961 GrainType::Goal,
1962 GrainType::Reasoning,
1963 GrainType::Consensus,
1964 GrainType::Consent,
1965 ];
1966 for gt in &types {
1967 assert!(registry.get(*gt).is_some(), "Missing renderer for {:?}", gt);
1968 }
1969 }
1970
1971 #[test]
1974 fn test_sml_escape_metacharacters_and_bidi_passthrough() {
1975 assert_eq!(sml_escape("<a>"), "<a>");
1976 assert_eq!(sml_escape("a & b"), "a & b");
1977 assert_eq!(sml_escape("\"q\""), ""q"");
1978 assert_eq!(sml_escape("it's"), "it's");
1979 let bidi = "safe\u{202E}evil\u{2066}x\u{200D}y";
1982 assert_eq!(sml_escape(bidi), bidi);
1983 }
1984}