1use std::collections::HashMap;
2
3use rmpv::Value;
4
5use crate::error::{DejaDbError, Hash, Result};
6use crate::format::field_map::expand_field;
7use crate::format::header::MgHeader;
8#[allow(clippy::wildcard_imports)]
9use crate::types::*;
10
11fn unwrap_if_cose(raw_bytes: &[u8]) -> Result<Option<Vec<u8>>> {
17 if raw_bytes.first() != Some(&0x84) {
18 return Ok(None);
19 }
20 #[cfg(feature = "signing")]
21 {
22 use coset::{CborSerializable, CoseSign1};
23 let cose = CoseSign1::from_slice(raw_bytes)
24 .map_err(|e| DejaDbError::Format(format!("COSE Sign1 decode: {}", e)))?;
25 let inner = cose
26 .payload
27 .ok_or_else(|| DejaDbError::Format("COSE Sign1 envelope has no payload".into()))?;
28 Ok(Some(inner))
29 }
30 #[cfg(not(feature = "signing"))]
31 {
32 Err(DejaDbError::Format(
33 "blob is a COSE Sign1 envelope but the 'signing' feature is not enabled".into(),
34 ))
35 }
36}
37
38#[derive(Debug, Clone, serde::Serialize)]
40pub struct DeserializedGrain {
41 pub header: MgHeader,
42 pub grain_type: GrainType,
43 pub fields: HashMap<String, serde_json::Value>,
44 pub hash: Hash,
45}
46
47pub fn deserialize_blob(blob: &[u8]) -> Result<DeserializedGrain> {
53 let unwrapped: Option<Vec<u8>> = unwrap_if_cose(blob)?;
56 let inner: &[u8] = match unwrapped.as_deref() {
57 Some(b) => b,
58 None => blob,
59 };
60
61 if inner.len() < 10 {
62 return Err(DejaDbError::Format(
63 "blob too short (minimum 10 bytes)".into(),
64 ));
65 }
66 if inner.len() > MAX_GRAIN_BYTES {
67 return Err(DejaDbError::Format(format!(
68 "blob too large ({} bytes, maximum {MAX_GRAIN_BYTES})",
69 inner.len()
70 )));
71 }
72
73 let header = MgHeader::from_bytes(&inner[..9])?;
75
76 let grain_type = GrainType::from_byte(header.grain_type).ok_or_else(|| {
78 DejaDbError::Format(format!("unknown grain type byte: {:#x}", header.grain_type))
79 })?;
80
81 let payload = &inner[9..];
86 guard_msgpack_shape(payload)?;
87 let value: Value = rmpv::decode::read_value(&mut &payload[..])
88 .map_err(|e| DejaDbError::Serialization(format!("msgpack decode error: {}", e)))?;
89
90 let fields = msgpack_to_json_expanded(&value)?;
92 let fields_map = match fields {
93 serde_json::Value::Object(m) => {
94 let mut map = HashMap::with_capacity(m.len().max(20));
95 map.extend(m);
96 map
97 }
98 _ => return Err(DejaDbError::Format("payload must be a map".into())),
99 };
100
101 let hash = crate::format::header::content_address(inner);
103
104 Ok(DeserializedGrain {
105 header,
106 grain_type,
107 fields: fields_map,
108 hash,
109 })
110}
111
112pub(crate) const MAX_GRAIN_BYTES: usize = 16 * 1024 * 1024;
117
118pub(crate) const MAX_MSGPACK_DEPTH: usize = 256;
123
124pub(crate) fn guard_msgpack_shape(buf: &[u8]) -> Result<()> {
139 fn need_bytes(buf: &[u8], pos: usize, n: usize) -> Result<()> {
140 match pos.checked_add(n) {
141 Some(end) if end <= buf.len() => Ok(()),
142 _ => Err(DejaDbError::Format("truncated msgpack payload".into())),
143 }
144 }
145 fn read_be(buf: &[u8], pos: usize, n: usize) -> u64 {
146 let mut v = 0u64;
147 for &b in &buf[pos..pos + n] {
148 v = (v << 8) | b as u64;
149 }
150 v
151 }
152 fn len_prefixed(buf: &[u8], pos: &mut usize, n: usize) -> Result<usize> {
154 need_bytes(buf, *pos, n)?;
155 let v = read_be(buf, *pos, n);
156 *pos += n;
157 Ok(v as usize)
158 }
159 fn skip(buf: &[u8], pos: &mut usize, n: usize) -> Result<()> {
161 need_bytes(buf, *pos, n)?;
162 *pos += n;
163 Ok(())
164 }
165 fn push_container(stack: &mut Vec<u64>, buf: &[u8], pos: usize, count: u64) -> Result<()> {
168 if stack.len() >= MAX_MSGPACK_DEPTH {
169 return Err(DejaDbError::Format("msgpack nesting too deep".into()));
170 }
171 if count > (buf.len() - pos) as u64 {
172 return Err(DejaDbError::Format(
173 "msgpack container length exceeds payload".into(),
174 ));
175 }
176 stack.push(count);
177 Ok(())
178 }
179
180 let mut pos = 0usize;
181 let mut stack: Vec<u64> = vec![1]; while let Some(&top) = stack.last() {
184 if top == 0 {
185 stack.pop();
186 continue;
187 }
188 *stack.last_mut().unwrap() -= 1;
189
190 need_bytes(buf, pos, 1)?;
191 let marker = buf[pos];
192 pos += 1;
193
194 match marker {
195 0x00..=0x7f | 0xe0..=0xff | 0xc0 | 0xc2 | 0xc3 => {}
197 0xc1 => return Err(DejaDbError::Format("invalid msgpack marker 0xc1".into())),
198 0xa0..=0xbf => {
200 let len = (marker & 0x1f) as usize;
201 skip(buf, &mut pos, len)?;
202 }
203 0x80..=0x9f => {
205 let n = (marker & 0x0f) as u64;
206 let count = if marker < 0x90 { n * 2 } else { n };
207 push_container(&mut stack, buf, pos, count)?;
208 }
209 0xcc | 0xd0 => skip(buf, &mut pos, 1)?,
211 0xcd | 0xd1 => skip(buf, &mut pos, 2)?,
212 0xca | 0xce | 0xd2 => skip(buf, &mut pos, 4)?,
213 0xcb | 0xcf | 0xd3 => skip(buf, &mut pos, 8)?,
214 0xd9 | 0xc4 => {
216 let l = len_prefixed(buf, &mut pos, 1)?;
217 skip(buf, &mut pos, l)?;
218 }
219 0xda | 0xc5 => {
220 let l = len_prefixed(buf, &mut pos, 2)?;
221 skip(buf, &mut pos, l)?;
222 }
223 0xdb | 0xc6 => {
224 let l = len_prefixed(buf, &mut pos, 4)?;
225 skip(buf, &mut pos, l)?;
226 }
227 0xc7 => {
229 let l = len_prefixed(buf, &mut pos, 1)?;
230 skip(buf, &mut pos, l + 1)?;
231 }
232 0xc8 => {
233 let l = len_prefixed(buf, &mut pos, 2)?;
234 skip(buf, &mut pos, l + 1)?;
235 }
236 0xc9 => {
237 let l = len_prefixed(buf, &mut pos, 4)?;
238 skip(buf, &mut pos, l + 1)?;
239 }
240 0xd4 => skip(buf, &mut pos, 1 + 1)?,
242 0xd5 => skip(buf, &mut pos, 1 + 2)?,
243 0xd6 => skip(buf, &mut pos, 1 + 4)?,
244 0xd7 => skip(buf, &mut pos, 1 + 8)?,
245 0xd8 => skip(buf, &mut pos, 1 + 16)?,
246 0xdc => {
248 let c = len_prefixed(buf, &mut pos, 2)? as u64;
249 push_container(&mut stack, buf, pos, c)?;
250 }
251 0xdd => {
252 let c = len_prefixed(buf, &mut pos, 4)? as u64;
253 push_container(&mut stack, buf, pos, c)?;
254 }
255 0xde => {
257 let c = len_prefixed(buf, &mut pos, 2)? as u64;
258 push_container(&mut stack, buf, pos, c * 2)?;
259 }
260 0xdf => {
261 let c = len_prefixed(buf, &mut pos, 4)? as u64;
262 push_container(&mut stack, buf, pos, c * 2)?;
263 }
264 }
265 }
266 Ok(())
267}
268
269#[cfg(test)]
270mod msgpack_guard_tests {
271 use super::guard_msgpack_shape;
272
273 #[test]
274 fn accepts_small_valid_values() {
275 assert!(guard_msgpack_shape(&[0x80]).is_ok()); assert!(guard_msgpack_shape(&[0x90]).is_ok()); assert!(guard_msgpack_shape(&[0xc0]).is_ok()); assert!(guard_msgpack_shape(&[0x81, 0xa1, b'a', 0xa1, b'b']).is_ok()); }
280
281 #[test]
282 fn rejects_excessive_depth() {
283 let mut deep = vec![0x91u8; 300];
285 deep.push(0xc0);
286 assert!(guard_msgpack_shape(&deep).is_err());
287 }
288
289 #[test]
290 fn rejects_oversized_container_count() {
291 assert!(guard_msgpack_shape(&[0xdd, 0xff, 0xff, 0xff, 0xff]).is_err());
293 assert!(guard_msgpack_shape(&[0xdf, 0xff, 0xff, 0xff, 0xff]).is_err());
294 }
295
296 #[test]
297 fn rejects_truncated_input() {
298 assert!(guard_msgpack_shape(&[0x92]).is_err()); assert!(guard_msgpack_shape(&[0xa5, b'h', b'i']).is_err()); assert!(guard_msgpack_shape(&[]).is_err()); }
302
303 #[test]
304 fn rejects_reserved_marker() {
305 assert!(guard_msgpack_shape(&[0xc1]).is_err());
306 }
307
308 #[test]
309 fn map_element_count_is_pairs_times_two() {
310 assert!(guard_msgpack_shape(&[0xde, 0x00, 0x03, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0]).is_err());
313 assert!(guard_msgpack_shape(
315 &[0xdf, 0x00, 0x00, 0x00, 0x03, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0]
316 )
317 .is_err());
318 assert!(
320 guard_msgpack_shape(&[0xde, 0x00, 0x02, 0xa1, b'k', 0xc0, 0xa1, b'j', 0xc3]).is_ok()
321 );
322 }
323
324 #[test]
325 fn array_count_is_exact() {
326 assert!(guard_msgpack_shape(&[0xdc, 0x00, 0x03, 0xc0, 0xc0, 0xc0]).is_ok());
328 assert!(guard_msgpack_shape(&[0xdc, 0x00, 0x03, 0xc0, 0xc0]).is_err());
329 }
330
331 #[test]
332 fn multibyte_string_lengths_are_read_exactly() {
333 assert!(guard_msgpack_shape(&[0x92, 0xda, 0x00, 0x03, b'a', b'b', b'c', 0xc0]).is_ok());
336 assert!(guard_msgpack_shape(&[0xda, 0x00, 0x05, b'a', b'b', b'c']).is_err());
338 }
339}
340
341fn msgpack_to_json_expanded(value: &Value) -> Result<serde_json::Value> {
343 match value {
344 Value::Nil => Ok(serde_json::Value::Null),
345 Value::Boolean(b) => Ok(serde_json::Value::Bool(*b)),
346 Value::Integer(i) => {
347 if let Some(n) = i.as_i64() {
348 Ok(serde_json::Value::Number(n.into()))
349 } else if let Some(n) = i.as_u64() {
350 Ok(serde_json::Value::Number(n.into()))
351 } else {
352 Err(DejaDbError::Format("integer out of range".into()))
353 }
354 }
355 Value::F32(f) => Ok(serde_json::Value::Number(
356 serde_json::Number::from_f64(*f as f64)
357 .ok_or_else(|| DejaDbError::Format("invalid float".into()))?,
358 )),
359 Value::F64(f) => Ok(serde_json::Value::Number(
360 serde_json::Number::from_f64(*f)
361 .ok_or_else(|| DejaDbError::Format("invalid float (NaN/Inf)".into()))?,
362 )),
363 Value::String(s) => {
364 let str_val = s.as_str().unwrap_or("");
365 Ok(serde_json::Value::String(str_val.to_string()))
366 }
367 Value::Binary(b) => Ok(serde_json::Value::String(hex::encode(b))),
368 Value::Array(arr) => {
369 let items: Result<Vec<serde_json::Value>> =
370 arr.iter().map(msgpack_to_json_expanded).collect();
371 Ok(serde_json::Value::Array(items?))
372 }
373 Value::Map(pairs) => {
374 let mut map = serde_json::Map::new();
375 for (k, v) in pairs {
376 let key = match k {
377 Value::String(s) => {
378 let short = s.as_str().unwrap_or("");
379 expand_field(short).to_string()
380 }
381 _ => return Err(DejaDbError::Format("map key must be string".into())),
382 };
383 let val = msgpack_to_json_expanded(v)?;
384 map.insert(key, val);
385 }
386 Ok(serde_json::Value::Object(map))
387 }
388 Value::Ext(_, _) => Err(DejaDbError::Format("ext type not supported".into())),
389 }
390}
391
392impl DeserializedGrain {
393 pub fn get_str(&self, field: &str) -> Option<&str> {
395 self.fields.get(field)?.as_str()
396 }
397
398 pub fn base_text(&self) -> String {
411 match self.grain_type {
412 GrainType::Fact => {
413 let s = self.get_str("subject").unwrap_or("");
414 let r = self.get_str("relation").unwrap_or("");
415 let o = self.get_str("object").unwrap_or("");
416 format!("{} {} {}", s, r, o).trim().to_string()
417 }
418 _ => {
419 for field in &[
420 "content",
421 "description",
422 "title",
423 "goal",
424 "query",
425 "result",
426 "output",
427 ] {
428 if let Some(v) = self.get_str(field) {
429 if !v.trim().is_empty() {
430 return v.to_string();
431 }
432 }
433 }
434 self.grain_type.as_str().to_string()
435 }
436 }
437 }
438
439 pub fn embedding_text(&self) -> String {
454 self.reconstruct_for_embedding().embedding_text()
455 }
456
457 fn fill_embedding_common(&self, common: &mut GrainCommon) {
467 if let Some(et) = self.get_str("embedding_text") {
468 common.embedding_text = Some(et.to_string());
469 }
470 if let Some(ns) = self.get_str("namespace") {
471 common.namespace = Some(ns.to_string());
472 }
473 if let Some(arr) = self
474 .fields
475 .get("structural_tags")
476 .and_then(|v| v.as_array())
477 {
478 common.tags = arr
479 .iter()
480 .filter_map(|t| t.as_str().map(str::to_string))
481 .collect();
482 }
483 }
484
485 fn reconstruct_for_embedding(&self) -> Box<dyn Grain> {
491 let s = |f: &str| self.get_str(f).unwrap_or("").to_string();
492 let mut grain: Box<dyn Grain> = match self.grain_type {
493 GrainType::Fact => Box::new(Fact::new(&s("subject"), &s("relation"), &s("object"))),
494 GrainType::Event => Box::new(Event::new(&s("content"))),
495 GrainType::State => {
496 let context = self
497 .fields
498 .get("context")
499 .cloned()
500 .unwrap_or(serde_json::Value::Null);
501 Box::new(State::new(context))
502 }
503 GrainType::Workflow => {
504 let nodes = self
505 .fields
506 .get("nodes")
507 .and_then(|v| v.as_array())
508 .map(|arr| {
509 arr.iter()
510 .filter_map(|n| n.as_str().map(str::to_string))
511 .collect()
512 })
513 .unwrap_or_default();
514 let mut wf = Workflow::new(nodes);
515 if let Some(t) = self.get_str("trigger") {
516 wf = wf.trigger(t);
517 }
518 Box::new(wf)
519 }
520 GrainType::Tool => {
521 let mut tool = Tool::new(&s("tool_name"));
522 if let Some(c) = self
524 .get_str("tool_content")
525 .or_else(|| self.get_str("content"))
526 {
527 tool.content = Some(c.to_string());
528 }
529 Box::new(tool)
530 }
531 GrainType::Observation => {
532 let mut obs = Observation::new(&s("observer_id"), &s("observer_type"));
533 if let Some(subj) = self.get_str("subject") {
534 obs = obs.subject(subj);
535 }
536 if let Some(obj) = self.get_str("object") {
537 obs = obs.object(obj);
538 }
539 Box::new(obs)
540 }
541 GrainType::Goal => {
542 let mut goal = Goal::new(&s("description"));
543 if let Some(c) = self.get_str("criteria") {
544 goal.criteria = Some(c.to_string());
545 }
546 Box::new(goal)
547 }
548 GrainType::Reasoning => {
549 let mut r = Reasoning::new();
550 if let Some(c) = self.get_str("conclusion") {
551 r.conclusion = Some(c.to_string());
552 }
553 if let Some(t) = self.get_str("thinking_content") {
554 r.thinking_content = Some(t.to_string());
555 }
556 Box::new(r)
557 }
558 GrainType::Consensus => {
559 let mut c = Consensus::new();
560 if let Some(ac) = self.get_str("agreed_content") {
561 c.agreed_content = Some(ac.to_string());
562 }
563 c.agreement_count = self.get_i64("agreement_count");
564 c.threshold = self.get_f64("threshold");
565 Box::new(c)
566 }
567 GrainType::Consent => {
568 let mut c = Consent::new(&s("subject_did"));
569 if let Some(g) = self.get_str("grantee_did") {
570 c.grantee_did = Some(g.to_string());
571 }
572 c.is_withdrawal = self.get_bool("is_withdrawal");
573 Box::new(c)
574 }
575 GrainType::Skill => {
576 let mut sk = Skill::new(&s("name"), &s("description"));
577 if let Some(w) = self.get_str("when_to_use") {
578 sk.when_to_use = Some(w.to_string());
579 }
580 if let Some(d) = self.get_str("domain") {
581 sk.domain = Some(d.to_string());
582 }
583 Box::new(sk)
584 }
585 };
586 self.fill_embedding_common(grain.common_mut());
587 grain
588 }
589
590 pub fn get_i64(&self, field: &str) -> Option<i64> {
592 self.fields.get(field)?.as_i64()
593 }
594
595 pub fn get_f64(&self, field: &str) -> Option<f64> {
597 self.fields.get(field)?.as_f64()
598 }
599
600 pub fn get_bool(&self, field: &str) -> Option<bool> {
602 self.fields.get(field)?.as_bool()
603 }
604
605 pub fn get_u64(&self, field: &str) -> Option<u64> {
607 self.fields.get(field)?.as_u64()
608 }
609
610 pub fn to_fact(&self) -> Result<Fact> {
612 if self.grain_type != GrainType::Fact {
613 return Err(DejaDbError::Validation("not a Fact grain".into()));
614 }
615 let subject = self
616 .get_str("subject")
617 .ok_or_else(|| DejaDbError::Validation("missing subject".into()))?;
618 let relation = self
619 .get_str("relation")
620 .ok_or_else(|| DejaDbError::Validation("missing relation".into()))?;
621 let object = self
622 .get_str("object")
623 .ok_or_else(|| DejaDbError::Validation("missing object".into()))?;
624
625 let mut fact = Fact::new(subject, relation, object);
626
627 if let Some(c) = self.get_f64("confidence") {
628 fact.common.confidence = c;
629 }
630 if let Some(ns) = self.get_str("namespace") {
631 fact.common.namespace = Some(ns.to_string());
632 }
633 if let Some(uid) = self.get_str("user_id") {
634 fact.common.user_id = Some(uid.to_string());
635 }
636 if let Some(st) = self.get_str("source_type") {
637 fact.common.source_type = Some(st.to_string());
638 }
639 if let Some(ca) = self.get_i64("created_at") {
640 fact.common.created_at = Some(ca);
641 }
642 if let Some(adid) = self.get_str("author_did") {
643 fact.common.author_did = Some(adid.to_string());
644 }
645 if let Some(et) = self.get_str("embedding_text") {
646 fact.common.embedding_text = Some(et.to_string());
647 }
648
649 Ok(fact)
650 }
651
652 pub fn to_tool(&self) -> Result<Tool> {
664 if self.grain_type != GrainType::Tool {
665 return Err(DejaDbError::Validation("not an Tool grain".into()));
666 }
667 let tool_name = self.get_str("tool_name").unwrap_or("").to_string();
668 let mut a = Tool::new(&tool_name);
669
670 if let Some(k) = self.get_str("kind").and_then(ToolKind::parse) {
671 a.kind = k;
672 }
673 if let Some(v) = self.fields.get("input") {
674 a.input = Some(v.clone());
675 }
676 if let Some(s) = self
678 .get_str("tool_content")
679 .or_else(|| self.get_str("content"))
680 {
681 a.content = Some(s.to_string());
682 }
683 if let Some(b) = self.get_bool("is_error") {
684 a.is_error = Some(b);
685 }
686 if let Some(s) = self.get_str("error") {
687 a.error = Some(s.to_string());
688 }
689 if let Some(n) = self.get_u64("duration_ms") {
690 a.duration_ms = Some(n);
691 }
692 if let Some(s) = self.get_str("parent_task_id") {
693 a.parent_task_id = Some(s.to_string());
694 }
695 if let Some(s) = self.get_str("tool_call_id") {
696 a.tool_call_id = Some(s.to_string());
697 }
698 if let Some(s) = self.get_str("call_batch_id") {
699 a.call_batch_id = Some(s.to_string());
700 }
701 if let Some(s) = self.get_str("tool_description") {
702 a.tool_description = Some(s.to_string());
703 }
704 if let Some(v) = self.fields.get("input_schema") {
705 a.input_schema = Some(v.clone());
706 }
707 if let Some(v) = self.fields.get("output_schema") {
708 a.output_schema = Some(v.clone());
709 }
710 if let Some(b) = self.get_bool("strict") {
711 a.strict = Some(b);
712 }
713 if let Some(b) = self.get_bool("async_mode") {
714 a.async_mode = Some(b);
715 }
716 if let Some(s) = self.get_str("executor_uri") {
717 a.executor_uri = Some(s.to_string());
718 }
719 if let Some(v) = self.fields.get("locked_params") {
720 a.locked_params = Some(v.clone());
721 }
722 if let Some(arr) = self.fields.get("examples").and_then(|v| v.as_array()) {
723 a.examples = Some(arr.clone());
724 }
725 if let Some(v) = self.fields.get("annotations") {
726 if let Ok(anno) = serde_json::from_value::<ToolAnnotations>(v.clone()) {
727 a.annotations = Some(anno);
728 }
729 }
730 if let Some(s) = self.get_str("spec_hash") {
731 a.spec_hash = Some(s.to_string());
732 }
733 if let Some(k) = self
738 .get_str("executor_kind")
739 .and_then(crate::types::executor_kind::ExecutorKind::parse)
740 {
741 a.executor_kind = Some(k);
742 }
743 if let Some(s) = self.get_str("status").and_then(ExecutionStatus::parse) {
748 a.status = Some(s);
749 }
750 if let Some(s) = self.get_str("correlation_id") {
751 a.correlation_id = Some(s.to_string());
752 }
753 if let Some(n) = self.get_i64("expires_at_sec") {
754 a.expires_at_sec = Some(n);
755 }
756 if let Some(hex_str) = self.get_str("transient_definition_hash") {
757 if let Ok(bytes) = hex::decode(hex_str) {
758 if let Ok(arr) = <[u8; 32]>::try_from(bytes.as_slice()) {
759 a.transient_definition_hash = Some(arr);
760 }
761 }
762 }
763 if let Some(fc) = self.get_str("failure_cause").and_then(FailureCause::parse) {
764 a.failure_cause = Some(fc);
765 }
766 if let Some(s) = self.get_str("failure_detail") {
767 a.failure_detail = Some(s.to_string());
768 }
769 if let Some(aex) = self
770 .get_str("actor_execution_environment")
771 .and_then(ActorExecutionEnvironment::parse)
772 {
773 a.actor_execution_environment = Some(aex);
774 }
775
776 if let Some(c) = self.get_f64("confidence") {
778 a.common.confidence = c;
779 }
780 if let Some(ns) = self.get_str("namespace") {
781 a.common.namespace = Some(ns.to_string());
782 }
783 if let Some(uid) = self.get_str("user_id") {
784 a.common.user_id = Some(uid.to_string());
785 }
786 if let Some(st) = self.get_str("source_type") {
787 a.common.source_type = Some(st.to_string());
788 }
789 if let Some(ca) = self.get_i64("created_at") {
790 a.common.created_at = Some(ca);
791 }
792 if let Some(adid) = self.get_str("author_did") {
793 a.common.author_did = Some(adid.to_string());
794 }
795
796 Ok(a)
797 }
798
799 pub fn to_event(&self) -> Result<Event> {
804 if self.grain_type != GrainType::Event {
805 return Err(DejaDbError::Validation("not an Event grain".into()));
806 }
807 let content = self.get_str("content").unwrap_or("").to_string();
808 let mut ev = Event::new(&content);
809
810 if let Some(s) = self.get_str("subject") {
811 ev.subject = Some(s.to_string());
812 }
813 if let Some(o) = self.get_str("object") {
814 ev.object = Some(o.to_string());
815 }
816 if let Some(r) = self.get_str("role").and_then(Role::from_str) {
817 ev.role = Some(r);
818 }
819 if let Some(s) = self.get_str("session_id") {
820 ev.session_id = Some(s.to_string());
821 }
822 if let Some(p) = self.get_str("parent_message_id") {
823 ev.parent_message_id = Some(p.to_string());
824 }
825 if let Some(m) = self.get_str("model_id") {
826 ev.model_id = Some(m.to_string());
827 }
828 if let Some(sr) = self.get_str("stop_reason") {
829 ev.stop_reason = Some(sr.to_string());
830 }
831 if let Some(rid) = self.get_str("run_id") {
832 ev.run_id = Some(rid.to_string());
833 }
834 if let Some(blocks_json) = self.fields.get("content_blocks") {
835 if let Ok(blocks) = serde_json::from_value(blocks_json.clone()) {
836 ev.content_blocks = Some(blocks);
837 }
838 }
839 if let Some(tu_json) = self.fields.get("token_usage") {
840 if let Ok(tu) = serde_json::from_value(tu_json.clone()) {
841 ev.token_usage = Some(tu);
842 }
843 }
844
845 if let Some(c) = self.get_f64("confidence") {
847 ev.common.confidence = c;
848 }
849 if let Some(ns) = self.get_str("namespace") {
850 ev.common.namespace = Some(ns.to_string());
851 }
852 if let Some(uid) = self.get_str("user_id") {
853 ev.common.user_id = Some(uid.to_string());
854 }
855 if let Some(st) = self.get_str("source_type") {
856 ev.common.source_type = Some(st.to_string());
857 }
858 if let Some(ca) = self.get_i64("created_at") {
859 ev.common.created_at = Some(ca);
860 }
861 if let Some(adid) = self.get_str("author_did") {
862 ev.common.author_did = Some(adid.to_string());
863 }
864 if let Some(et) = self.get_str("embedding_text") {
865 ev.common.embedding_text = Some(et.to_string());
866 }
867
868 Ok(ev)
869 }
870
871 pub fn to_skill(&self) -> Result<Skill> {
879 if self.grain_type != GrainType::Skill {
880 return Err(DejaDbError::Validation("not a Skill grain".into()));
881 }
882 let name = self.get_str("name").unwrap_or("").to_string();
883 let description = self.get_str("description").unwrap_or("").to_string();
884 let mut s = Skill::new(&name, &description);
885
886 if let Some(instr) = self.get_str("instructions") {
887 s.instructions = Some(instr.to_string());
888 }
889 if let Some(wtu) = self.get_str("when_to_use") {
890 s.when_to_use = Some(wtu.to_string());
891 }
892 if let Some(v) = self.get_str("version") {
893 s.version = Some(v.to_string());
894 }
895 if let Some(arr) = self.fields.get("allowed_tools").and_then(|v| v.as_array()) {
896 s.allowed_tools = arr
897 .iter()
898 .filter_map(|v| v.as_str().map(str::to_string))
899 .collect();
900 }
901 if let Some(arr) = self.fields.get("resources").and_then(|v| v.as_array()) {
902 s.resources = arr
903 .iter()
904 .filter_map(|v| v.as_str().map(str::to_string))
905 .collect();
906 }
907 if let Some(arr) = self.fields.get("dependencies").and_then(|v| v.as_array()) {
908 s.dependencies = arr
909 .iter()
910 .filter_map(|v| v.as_str().map(str::to_string))
911 .collect();
912 }
913 if let Some(arr) = self
914 .fields
915 .get("input_modalities")
916 .and_then(|v| v.as_array())
917 {
918 s.input_modalities = arr
919 .iter()
920 .filter_map(|v| v.as_str().map(str::to_string))
921 .collect();
922 }
923 if let Some(arr) = self
924 .fields
925 .get("output_modalities")
926 .and_then(|v| v.as_array())
927 {
928 s.output_modalities = arr
929 .iter()
930 .filter_map(|v| v.as_str().map(str::to_string))
931 .collect();
932 }
933 if let Some(dom) = self.get_str("domain") {
934 s.domain = Some(dom.to_string());
935 }
936 if let Some(hdid) = self.get_str("holder_did") {
937 s.holder_did = Some(hdid.to_string());
938 }
939 if let Some(pc) = self.get_u64("practice_count") {
940 s.practice_count = Some(pc as u32);
941 }
942 if let Some(lpa) = self.get_i64("last_practiced_at") {
943 s.last_practiced_at = Some(lpa);
944 }
945 if let Some(strat_json) = self.fields.get("strategies") {
946 if let Ok(strats) = serde_json::from_value(strat_json.clone()) {
947 s.strategies = strats;
948 }
949 }
950 if let Some(xfer) = self.get_bool("transferable") {
951 s.transferable = Some(xfer);
952 }
953
954 if let Some(ns) = self.get_str("namespace") {
956 s.common.namespace = Some(ns.to_string());
957 }
958 if let Some(uid) = self.get_str("user_id") {
959 s.common.user_id = Some(uid.to_string());
960 }
961 if let Some(st) = self.get_str("source_type") {
962 s.common.source_type = Some(st.to_string());
963 }
964 if let Some(ca) = self.get_i64("created_at") {
965 s.common.created_at = Some(ca);
966 }
967 if let Some(adid) = self.get_str("author_did") {
968 s.common.author_did = Some(adid.to_string());
969 }
970 if let Some(odid) = self.get_str("origin_did") {
971 s.common.origin_did = Some(odid.to_string());
972 }
973 if let Some(df) = self.get_str("derived_from") {
974 s.common.derived_from = Some(df.to_string());
975 }
976 if let Some(et) = self.get_str("embedding_text") {
977 s.common.embedding_text = Some(et.to_string());
978 }
979
980 let cf = self.get_f64("confidence");
983 let prof = self.get_f64("proficiency");
984 match (prof, cf) {
985 (Some(p), Some(c)) if (p - c).abs() > f64::EPSILON => {
986 tracing::debug!(
987 proficiency = p,
988 confidence = c,
989 "skill prof != cf on deserialize; taking prof as authoritative (D3)"
990 );
991 s.common.confidence = p;
992 }
993 (Some(p), _) => s.common.confidence = p,
994 (None, Some(c)) => s.common.confidence = c,
995 (None, None) => {}
996 }
997
998 Ok(s)
999 }
1000}
1001
1002#[cfg(test)]
1003mod tests {
1004 use super::*;
1005 use crate::format::serialize::serialize_grain;
1006
1007 #[test]
1021 fn test_nfc_composition_forms_collapse_to_same_hash() {
1022 let composed = Fact::new("u", "likes", "caf\u{00e9}").created_at(1_700_000_000_000); let decomposed = Fact::new("u", "likes", "cafe\u{0301}").created_at(1_700_000_000_000); let (b1, h1) = serialize_grain(&composed).unwrap();
1027 let (b2, h2) = serialize_grain(&decomposed).unwrap();
1028 assert_eq!(
1029 h1, h2,
1030 "serializer NFC-normalizes field bytes pre-hash, so composition \
1031 variants collapse to one content address (design §R-6 claims the \
1032 opposite — flagged)"
1033 );
1034 assert_eq!(b1, b2, "canonical bytes are byte-identical after NFC");
1035 }
1036
1037 #[test]
1038 fn test_roundtrip_fact() {
1039 let fact = Fact::new("john", "likes", "coffee")
1040 .confidence(0.95)
1041 .namespace("test")
1042 .source_type("user_explicit")
1043 .created_at(1768471200000);
1044
1045 let (blob, _hash) = serialize_grain(&fact).unwrap();
1046 let deserialized = deserialize_blob(&blob).unwrap();
1047
1048 assert_eq!(deserialized.grain_type, GrainType::Fact);
1049 assert_eq!(deserialized.get_str("subject"), Some("john"));
1050 assert_eq!(deserialized.get_str("relation"), Some("likes"));
1051 assert_eq!(deserialized.get_str("object"), Some("coffee"));
1052 assert_eq!(deserialized.get_f64("confidence"), Some(0.95));
1053 assert_eq!(deserialized.get_str("namespace"), Some("test"));
1054 }
1055
1056 #[test]
1062 fn test_embedding_text_matches_grain_trait_all_types() {
1063 use crate::types::{
1064 Consensus, Consent, Event, Goal, Grain, Observation, Reasoning, Skill, State, Tool,
1065 Workflow,
1066 };
1067
1068 fn assert_parity<G: Grain + 'static>(g: G) {
1073 let expected = g.embedding_text();
1074 let (blob, _h) = serialize_grain(&g).unwrap();
1075 let dg = deserialize_blob(&blob).unwrap();
1076 assert_eq!(
1077 dg.embedding_text(),
1078 expected,
1079 "embedding_text parity failed for {:?}",
1080 g.grain_type()
1081 );
1082 assert!(
1083 !expected.trim().is_empty(),
1084 "test grain for {:?} produced empty embedding text",
1085 g.grain_type()
1086 );
1087 }
1088
1089 assert_parity(Fact::new("john", "likes", "coffee").namespace("prefs"));
1091 assert_parity(Event::new("user discussed vacation plans").tags(["travel"]));
1093 assert_parity(State::new(
1095 serde_json::json!({ "label": "checkpoint-7", "extra": 42 }),
1096 ));
1097 assert_parity(Workflow::new(vec!["plan".into(), "execute".into()]).trigger("on_request"));
1099 assert_parity(Tool::new("calculator").content("computed 42"));
1101 assert_parity(
1103 Observation::new("agent-1", "vision")
1104 .subject("scene")
1105 .object("cat"),
1106 );
1107 let mut goal = Goal::new("ship trilingual recall");
1109 goal.criteria = Some("all 11 types embed".into());
1110 assert_parity(goal);
1111 assert_parity(Reasoning::new().conclusion("therefore X holds"));
1113 let mut consensus = Consensus::new();
1115 consensus.agreed_content = Some("ratified the plan".into());
1116 assert_parity(consensus);
1117 let mut consent = Consent::new("did:key:subjectA");
1119 consent.grantee_did = Some("did:key:granteeB".into());
1120 assert_parity(consent);
1121 assert_parity(
1123 Skill::new("code_review", "review code for defects")
1124 .when_to_use("before merge")
1125 .domain("swe"),
1126 );
1127 }
1128
1129 #[test]
1133 fn test_embedding_text_enrichment_and_override() {
1134 use crate::types::Grain;
1135
1136 let fact = Fact::new("john", "likes", "coffee")
1138 .namespace("prefs")
1139 .tags(["a", "b"]);
1140 let (blob, _h) = serialize_grain(&fact).unwrap();
1141 let dg = deserialize_blob(&blob).unwrap();
1142 assert_eq!(dg.embedding_text(), fact.embedding_text());
1143 assert_eq!(dg.embedding_text(), "john likes coffee [a, b] (prefs)");
1144
1145 let mut fact2 = Fact::new("a", "b", "c");
1147 fact2.common_mut().embedding_text = Some("rich custom context".to_string());
1148 let (blob2, _h2) = serialize_grain(&fact2).unwrap();
1149 let dg2 = deserialize_blob(&blob2).unwrap();
1150 assert_eq!(dg2.embedding_text(), "rich custom context");
1151 assert_eq!(dg2.embedding_text(), fact2.embedding_text());
1152 }
1153
1154 #[test]
1159 fn test_base_text_reranker_projection_unchanged() {
1160 let tool = Tool::new("calculator").content("computed 42");
1168 let (blob, _h) = serialize_grain(&tool).unwrap();
1169 let dg = deserialize_blob(&blob).unwrap();
1170 assert_eq!(dg.base_text(), "tool");
1171 assert!(dg.embedding_text().contains("calculator"));
1172 assert!(dg.embedding_text().contains("computed 42"));
1173 assert_ne!(dg.base_text(), dg.embedding_text());
1174
1175 let state = State::new(serde_json::json!({ "label": "checkpoint-7" }));
1179 let (sblob, _sh) = serialize_grain(&state).unwrap();
1180 let sdg = deserialize_blob(&sblob).unwrap();
1181 assert_eq!(sdg.base_text(), "state");
1182 assert_eq!(sdg.embedding_text(), "checkpoint-7");
1183
1184 let fact = Fact::new("john", "likes", "coffee");
1186 let (fblob, _fh) = serialize_grain(&fact).unwrap();
1187 let fdg = deserialize_blob(&fblob).unwrap();
1188 assert_eq!(fdg.base_text(), "john likes coffee");
1189 }
1190
1191 #[test]
1195 fn test_tool_executor_kind_round_trip() {
1196 use crate::types::executor_kind::ExecutorKind;
1197 let tool = Tool::new("slack.post").executor_kind(ExecutorKind::Client);
1198 let (blob, _hash) = serialize_grain(&tool).unwrap();
1199 let dg = deserialize_blob(&blob).unwrap();
1200 assert_eq!(dg.get_str("executor_kind"), Some("client"));
1201 let back = dg.to_tool().unwrap();
1202 assert_eq!(back.executor_kind, Some(ExecutorKind::Client));
1203 }
1204
1205 #[test]
1211 fn test_action_executor_kind_default_host_omits_field() {
1212 use crate::types::executor_kind::ExecutorKind;
1213 let tool = Tool::new("slack.post").executor_kind(ExecutorKind::Host);
1214 let (blob, _hash) = serialize_grain(&tool).unwrap();
1215 let dg = deserialize_blob(&blob).unwrap();
1216 assert_eq!(dg.get_str("executor_kind"), None);
1217 let back = dg.to_tool().unwrap();
1218 assert_eq!(back.executor_kind, None);
1219 }
1220
1221 #[test]
1222 fn test_get_bool_roundtrip_tool() {
1223 let tool = Tool::new("calculator")
1224 .is_error(true)
1225 .duration_ms(250)
1226 .error("divide by zero");
1227
1228 let (blob, _hash) = serialize_grain(&tool).unwrap();
1229 let deserialized = deserialize_blob(&blob).unwrap();
1230
1231 assert_eq!(deserialized.grain_type, GrainType::Tool);
1232 assert_eq!(deserialized.get_bool("is_error"), Some(true));
1233 assert_eq!(deserialized.get_u64("duration_ms"), Some(250));
1234 assert_eq!(deserialized.get_str("error"), Some("divide by zero"));
1235 assert_eq!(deserialized.get_str("tool_name"), Some("calculator"));
1236 }
1237
1238 #[test]
1239 fn test_get_bool_returns_none_for_missing_field() {
1240 let fact = Fact::new("john", "likes", "coffee");
1241 let (blob, _hash) = serialize_grain(&fact).unwrap();
1242 let deserialized = deserialize_blob(&blob).unwrap();
1243
1244 assert_eq!(deserialized.get_bool("is_error"), None);
1246 assert_eq!(deserialized.get_u64("duration_ms"), None);
1247 }
1248
1249 #[test]
1250 fn test_get_bool_false_roundtrip() {
1251 let tool = Tool::new("browser").is_error(false);
1252 let (blob, _hash) = serialize_grain(&tool).unwrap();
1253 let deserialized = deserialize_blob(&blob).unwrap();
1254
1255 assert_eq!(deserialized.get_bool("is_error"), Some(false));
1256 }
1257}