1use std::{cmp::Ordering, collections::HashSet};
12
13use khive_types::VerbPresentationPolicy;
14use serde::{Deserialize, Serialize};
15use serde_json::{Map, Value};
16
17#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
20pub enum NoteContentScope {
21 #[default]
22 None,
23 Record,
24 Items,
25 Notes,
26}
27
28impl NoteContentScope {
29 pub fn protect(self, mut value: Value, transform: impl FnOnce(Value) -> Value) -> Value {
33 if self == Self::None {
34 return transform(value);
35 }
36 fn take_content(record: &mut Value) -> Option<Value> {
37 record.as_object_mut()?.remove("content")
38 }
39 fn restore_content(record: &mut Value, content: Option<Value>) {
40 if let (Some(record), Some(content)) = (record.as_object_mut(), content) {
41 record.insert("content".to_string(), content);
42 }
43 }
44 if self == Self::Record {
45 let content = take_content(&mut value);
46 let mut value = transform(value);
47 restore_content(&mut value, content);
48 return value;
49 }
50 let key = if self == Self::Items {
51 "items"
52 } else {
53 "notes"
54 };
55 let contents: Vec<_> = value
56 .get_mut(key)
57 .and_then(Value::as_array_mut)
58 .into_iter()
59 .flatten()
60 .map(take_content)
61 .collect();
62 let mut value = transform(value);
63 if let Some(records) = value.get_mut(key).and_then(Value::as_array_mut) {
64 for (record, content) in records.iter_mut().zip(contents) {
65 restore_content(record, content);
66 }
67 }
68 value
69 }
70
71 fn stringify_table_content(self, value: &mut Value) {
72 fn stringify(record: &mut Value) {
73 if let Some(content) = record.get_mut("content") {
74 if content.is_object() || content.is_array() {
75 *content = Value::String(content.to_string());
76 }
77 }
78 }
79 match self {
80 Self::None => {}
81 Self::Record => stringify(value),
82 Self::Items | Self::Notes => {
83 let key = if self == Self::Items {
84 "items"
85 } else {
86 "notes"
87 };
88 if let Some(records) = value.get_mut(key).and_then(Value::as_array_mut) {
89 records.iter_mut().for_each(stringify);
90 }
91 }
92 }
93 }
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
115#[serde(rename_all = "snake_case")]
116pub enum OutputFormat {
117 #[default]
121 Json,
122 Auto,
126 Table,
130}
131
132const CELL_TRUNCATE: usize = 120;
134
135const PROPERTY_HOIST_FIELDS: &[&str] = &["trigger_at", "due", "status"];
141
142pub fn render_format(value: Value, format: OutputFormat, presentation: PresentationMode) -> String {
160 render_format_with_note_content(value, format, presentation, NoteContentScope::None)
161}
162
163pub fn render_format_with_note_content(
167 value: Value,
168 format: OutputFormat,
169 presentation: PresentationMode,
170 content_scope: NoteContentScope,
171) -> String {
172 let value = prepare_format_value_with_note_content(value, format, presentation, content_scope);
173 match format {
174 OutputFormat::Json => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
175 OutputFormat::Auto if content_scope != NoteContentScope::None => {
176 serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string())
177 }
178 OutputFormat::Auto | OutputFormat::Table => {
179 render_auto(value, presentation != PresentationMode::Verbose)
180 }
181 }
182}
183
184pub fn prepare_format_value(
191 value: Value,
192 format: OutputFormat,
193 presentation: PresentationMode,
194) -> Value {
195 let scope = match format {
196 OutputFormat::Json if presentation == PresentationMode::Agent => {
197 Some(RedundancyScope::Machine)
198 }
199 OutputFormat::Json => None,
200 OutputFormat::Auto | OutputFormat::Table if presentation != PresentationMode::Verbose => {
201 Some(RedundancyScope::View)
202 }
203 OutputFormat::Auto | OutputFormat::Table => None,
204 };
205 match scope {
206 Some(scope) => apply_redundancy_drop(value, scope),
207 None => value,
208 }
209}
210
211pub fn prepare_format_value_with_note_content(
213 value: Value,
214 format: OutputFormat,
215 presentation: PresentationMode,
216 content_scope: NoteContentScope,
217) -> Value {
218 let mut value = content_scope.protect(value, |value| {
219 prepare_format_value(value, format, presentation)
220 });
221 if format == OutputFormat::Table {
222 content_scope.stringify_table_content(&mut value);
223 }
224 value
225}
226
227#[derive(Debug, Clone, Copy, PartialEq, Eq)]
246pub enum RedundancyScope {
247 View,
248 Machine,
249}
250
251pub fn apply_redundancy_drop(value: Value, scope: RedundancyScope) -> Value {
262 match value {
263 Value::Object(_) => drop_record(value, scope),
264 Value::Array(arr) => Value::Array(
265 arr.into_iter()
266 .map(|v| {
267 if v.is_object() {
268 drop_record(v, scope)
269 } else {
270 v
271 }
272 })
273 .collect(),
274 ),
275 other => other,
276 }
277}
278
279fn drop_record(value: Value, scope: RedundancyScope) -> Value {
281 let Value::Object(mut map) = value else {
282 return value;
283 };
284
285 if scope == RedundancyScope::View {
289 map.remove("full_id");
290 }
291
292 if map.get("namespace").and_then(Value::as_str) == Some("local") {
294 map.remove("namespace");
295 }
296
297 let props_val = map.remove("properties");
309 if let Some(Value::Object(props)) = props_val {
310 let mut new_props = Map::new();
311 for (k, v) in props {
312 let strict_round_trip = scope == RedundancyScope::Machine
313 && (k == "full_id" || ROUND_TRIP_FULL_UUID_FIELDS.contains(&k.as_str()));
314 if !strict_round_trip && map.get(&k) == Some(&v) {
315 continue;
316 }
317 new_props.insert(k, v);
318 }
319 if !new_props.is_empty() {
320 map.insert("properties".to_string(), Value::Object(new_props));
321 }
322 } else if let Some(other) = props_val {
323 map.insert("properties".to_string(), other);
324 }
325
326 let stream_entry = is_stream_entry(&map);
329 let out: Map<String, Value> = map
331 .into_iter()
332 .map(|(k, v)| {
333 if stream_entry && k == "record" {
334 return (k, v);
335 }
336 let v = match v {
337 Value::Array(arr) => Value::Array(
338 arr.into_iter()
339 .map(|item| {
340 if item.is_object() {
341 drop_record(item, scope)
342 } else {
343 item
344 }
345 })
346 .collect(),
347 ),
348 other => other,
349 };
350 (k, v)
351 })
352 .collect();
353 Value::Object(out)
354}
355
356struct RecordArray {
362 key: Option<String>,
363 records: Vec<Value>,
364 columns: Vec<String>,
365}
366
367fn render_auto(value: Value, truncate: bool) -> String {
375 match locate_record_array(&value) {
376 Some(found) => render_table_with_siblings(&value, &found, truncate),
377 None => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
378 }
379}
380
381fn locate_record_array(value: &Value) -> Option<RecordArray> {
387 let build = |key: Option<String>, arr: &[Value]| {
388 let records: Vec<Value> = arr.iter().cloned().map(hoist_table_scalars).collect();
389 let columns = collect_keys(&records);
390 RecordArray {
391 key,
392 records,
393 columns,
394 }
395 };
396 match value {
397 Value::Array(arr) if is_record_array(arr) => Some(build(None, arr)),
398 Value::Object(map) => map.iter().find_map(|(k, v)| match v {
399 Value::Array(arr) if is_record_array(arr) => Some(build(Some(k.clone()), arr)),
400 _ => None,
401 }),
402 _ => None,
403 }
404}
405
406fn hoist_table_scalars(record: Value) -> Value {
413 let Value::Object(mut map) = record else {
414 return record;
415 };
416 let mut props = match map.remove("properties") {
417 Some(Value::Object(props)) => props,
418 Some(other) => {
419 map.insert("properties".to_string(), other);
421 return Value::Object(map);
422 }
423 None => return Value::Object(map),
424 };
425 for field in PROPERTY_HOIST_FIELDS {
426 if map.contains_key(*field) {
427 continue;
428 }
429 if props
430 .get(*field)
431 .is_some_and(|v| !v.is_object() && !v.is_array())
432 {
433 let v = props.remove(*field).expect("checked above");
434 map.insert((*field).to_string(), v);
435 }
436 }
437 if !props.is_empty() {
438 map.insert("properties".to_string(), Value::Object(props));
439 }
440 Value::Object(map)
441}
442
443fn is_record_array(arr: &[Value]) -> bool {
445 arr.len() >= 2 && arr.iter().all(Value::is_object)
446}
447
448fn render_table_with_siblings(value: &Value, found: &RecordArray, truncate: bool) -> String {
453 let mut out = render_table(&found.records, &found.columns, truncate);
454 let (Value::Object(map), Some(array_key)) = (value, &found.key) else {
455 return out;
456 };
457 for (k, v) in map {
458 if k == array_key {
459 continue;
460 }
461 let text = match v {
462 Value::String(s) if !s.contains(['\n', '\r']) => s.clone(),
466 other => serde_json::to_string(other).unwrap_or_default(),
469 };
470 out.push_str(&format!("{k}: {text}\n"));
471 }
472 out
473}
474
475fn collect_keys(records: &[Value]) -> Vec<String> {
477 let mut seen = HashSet::new();
478 let mut keys = Vec::new();
479 for record in records {
480 if let Value::Object(map) = record {
481 for k in map.keys() {
482 if seen.insert(k.clone()) {
483 keys.push(k.clone());
484 }
485 }
486 }
487 }
488 keys
489}
490
491fn render_table(records: &[Value], keys: &[String], truncate: bool) -> String {
495 let mut out = String::new();
496
497 out.push('|');
498 for k in keys {
499 out.push(' ');
500 out.push_str(k);
501 out.push_str(" |");
502 }
503 out.push('\n');
504
505 out.push('|');
506 for _ in keys {
507 out.push_str("---|");
508 }
509 out.push('\n');
510
511 for record in records {
512 out.push('|');
513 for k in keys {
514 let cell = record.get(k).unwrap_or(&Value::Null);
515 let text = cell_text(k, cell, truncate);
516 out.push(' ');
517 out.push_str(&text);
518 out.push_str(" |");
519 }
520 out.push('\n');
521 }
522
523 out
524}
525
526fn truncation_exempt(key: &str) -> bool {
530 matches!(
531 key,
532 "id" | "kind"
533 | "status"
534 | "priority"
535 | "relation"
536 | "title"
537 | "name"
538 | "signature"
539 | "slug"
540 | "assignee"
541 | "from"
542 | "to"
543 | "due"
544 ) || key.ends_with("_id")
545 || key.ends_with("_at")
546 || key.starts_with("due")
547}
548
549fn cell_text(key: &str, value: &Value, truncate: bool) -> String {
556 let raw = match value {
557 Value::Null => String::new(),
558 Value::Bool(b) => b.to_string(),
559 Value::Number(n) => n.to_string(),
560 Value::String(s) => s.clone(),
561 Value::Object(_) => return "{…}".to_string(),
562 Value::Array(arr) => {
563 if arr.iter().any(|v| v.is_object() || v.is_array()) {
564 return "[…]".to_string();
565 }
566 serde_json::to_string(value).unwrap_or_default()
568 }
569 };
570
571 let escaped = raw.replace('|', "\\|").replace(['\n', '\r'], " ");
573
574 if !truncate || truncation_exempt(key) {
575 return escaped;
576 }
577
578 let char_count = escaped.chars().count();
581 if char_count > CELL_TRUNCATE {
582 let truncated: String = escaped.chars().take(CELL_TRUNCATE).collect();
583 format!("{truncated}...")
584 } else {
585 escaped
586 }
587}
588
589pub fn format_utc_rfc3339_seconds<Tz: chrono::TimeZone>(value: &chrono::DateTime<Tz>) -> String {
595 value
596 .with_timezone(&chrono::Utc)
597 .to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
598}
599
600pub fn micros_to_iso(micros: i64) -> String {
606 if let Some(dt) = chrono::DateTime::<chrono::Utc>::from_timestamp_micros(micros) {
607 return dt.to_rfc3339_opts(chrono::SecondsFormat::Micros, true);
608 }
609
610 const CYCLE_MICROS: i64 = 146_097 * 86_400 * 1_000_000;
613 let cycles = micros.div_euclid(CYCLE_MICROS);
614 let reduced =
615 chrono::DateTime::<chrono::Utc>::from_timestamp_micros(micros.rem_euclid(CYCLE_MICROS))
616 .expect("400-year remainder is within Chrono's range");
617 let year = i64::from(chrono::Datelike::year(&reduced)) + cycles * 400;
618 let rendered = reduced.to_rfc3339_opts(chrono::SecondsFormat::Micros, true);
619 format!("{year:+05}{}", &rendered[4..])
620}
621
622pub fn rfc3339_to_utc_micros(raw: &str) -> Result<i64, chrono::ParseError> {
631 chrono::DateTime::parse_from_rfc3339(raw.trim()).map(|dt| dt.timestamp_micros())
632}
633
634#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
636#[serde(rename_all = "snake_case")]
637pub enum PresentationMode {
638 #[default]
644 Agent,
645 Verbose,
649 Human,
656}
657
658const LIFECYCLE_NULL_PRESERVE: &[&str] = &[
664 "completed_at",
665 "deleted_at",
666 "due_at",
667 "read_at",
668 "started_at",
669 "superseded_at",
670 "applied_at",
671 "withdrawn_at",
672 "reviewed_at",
673 "parent_id",
674 "superseded_by",
675 "replaced_by",
676 "poll_interval_secs",
677 "stalled",
678 "last_success_at",
679 "last_poll_attempt_at",
680 "last_failure_at",
681 "last_error",
682 "consecutive_failures",
683];
684
685const EMPTY_ARRAY_PRESERVE: &[&str] = &[
690 "items",
691 "entities",
692 "notes",
693 "edges",
694 "results",
695 "entries",
696 "neighbors",
697];
698
699fn is_stable_list_envelope(map: &Map<String, Value>) -> bool {
700 map.contains_key("requested_limit")
701 && map.contains_key("effective_limit")
702 && map.contains_key("limit_clamped")
703 && EMPTY_ARRAY_PRESERVE
704 .iter()
705 .any(|field| map.contains_key(*field))
706}
707
708fn is_keyset_cursor_envelope(map: &Map<String, Value>) -> bool {
715 map.contains_key("next_after")
716 && (map.get("results").is_some_and(Value::is_array)
717 || (map.get("head_seq").is_some_and(Value::is_number)
718 && map.get("entries").is_some_and(Value::is_array)))
719}
720
721fn is_stream_entry(map: &Map<String, Value>) -> bool {
722 map.get("seq").is_some_and(Value::is_number)
723 && map.contains_key("id")
724 && map.contains_key("created_at")
725 && map.contains_key("record")
726}
727
728const PAYLOAD_TIMESTAMP_FIELDS: &[&str] = &["trigger_at", "due"];
744
745const ROUND_TRIP_FULL_UUID_FIELDS: &[&str] = &[
755 "context_entity_id",
756 "thread_id",
757 "outbound_ref",
758 "parent_id",
759 "session_id",
760 "project_id",
761];
762
763const SCORE_FIELDS: &[&str] = &[
765 "score",
766 "rank_score",
767 "vector_similarity",
768 "keyword_score",
769 "salience",
770 "decay_factor",
771 "rrf_score",
772 "similarity",
773 "cross_encoder_score",
774 "graph_proximity_score",
775];
776
777const UUID_CANONICAL_LEN: usize = 36;
779
780fn should_shorten_uuid_field(key: &str) -> bool {
788 if key == "full_id" || key == "bridge_instance_id" || ROUND_TRIP_FULL_UUID_FIELDS.contains(&key)
789 {
790 return false;
791 }
792 key == "id" || key.ends_with("_id") || matches!(key, "superseded_by" | "replaced_by")
793}
794
795pub fn present(value: Value, mode: PresentationMode, now_unix_seconds: i64) -> Value {
805 present_with_policy(
806 value,
807 mode,
808 now_unix_seconds,
809 VerbPresentationPolicy::Standard,
810 )
811}
812
813pub fn present_with_policy(
816 value: Value,
817 mode: PresentationMode,
818 now_unix_seconds: i64,
819 policy: VerbPresentationPolicy,
820) -> Value {
821 present_with_policy_at(value, mode, now_unix_seconds.into(), policy)
822}
823
824#[derive(Clone, Copy)]
827pub struct PresentationNow {
828 seconds: i64,
829 nanoseconds: u32,
830}
831
832impl From<i64> for PresentationNow {
833 fn from(seconds: i64) -> Self {
834 Self {
835 seconds,
836 nanoseconds: 0,
837 }
838 }
839}
840
841impl From<i32> for PresentationNow {
842 fn from(seconds: i32) -> Self {
843 i64::from(seconds).into()
844 }
845}
846
847impl From<chrono::DateTime<chrono::Utc>> for PresentationNow {
848 fn from(now: chrono::DateTime<chrono::Utc>) -> Self {
849 Self {
850 seconds: now.timestamp(),
851 nanoseconds: now.timestamp_subsec_nanos(),
852 }
853 }
854}
855
856pub fn present_with_policy_at(
858 value: Value,
859 mode: PresentationMode,
860 now: PresentationNow,
861 policy: VerbPresentationPolicy,
862) -> Value {
863 if policy == VerbPresentationPolicy::AlwaysVerbose {
864 return value;
865 }
866 match mode {
867 PresentationMode::Verbose | PresentationMode::Human => value,
868 PresentationMode::Agent => {
869 let mut value = value;
873 let agenda_next = (policy == VerbPresentationPolicy::AgendaContinuation)
874 .then(|| value.as_object_mut().and_then(|map| map.remove("next")))
875 .flatten();
876 let agenda_empty = policy == VerbPresentationPolicy::AgendaContinuation
877 && value
878 .get("events")
879 .and_then(Value::as_array)
880 .is_some_and(Vec::is_empty);
881 let config = AgentTransformConfig {
882 preserved_nulls: LIFECYCLE_NULL_PRESERVE.iter().copied().collect(),
883 scores: SCORE_FIELDS.iter().copied().collect(),
884 payload_timestamps: PAYLOAD_TIMESTAMP_FIELDS.iter().copied().collect(),
885 now,
886 };
887 let mut value = transform_agent(
888 value,
889 &config,
890 AgentTreeContext {
891 inside_properties: false,
892 object_properties: false,
893 array_member: false,
894 receipt_context: match policy {
895 VerbPresentationPolicy::StreamAppendReceipt => ReceiptContext::AppendRoot,
896 VerbPresentationPolicy::StreamBatchReceipts => ReceiptContext::BatchRoot,
897 _ => ReceiptContext::None,
898 },
899 },
900 );
901 if let Some(map) = value.as_object_mut() {
902 if let Some(next) = agenda_next {
903 map.insert("next".into(), next);
904 }
905 if agenda_empty {
906 map.insert("events".into(), Value::Array(Vec::new()));
907 }
908 }
909 value
910 }
911 }
912}
913
914#[derive(Clone, Copy, PartialEq, Eq)]
915enum ReceiptContext {
916 None,
917 AppendRoot,
918 BatchRoot,
919 BatchResults,
920 BatchMember,
921}
922
923struct AgentTransformConfig {
924 preserved_nulls: HashSet<&'static str>,
925 scores: HashSet<&'static str>,
926 payload_timestamps: HashSet<&'static str>,
927 now: PresentationNow,
928}
929
930#[derive(Clone, Copy)]
931struct AgentTreeContext {
932 inside_properties: bool,
933 object_properties: bool,
934 array_member: bool,
935 receipt_context: ReceiptContext,
936}
937
938fn transform_agent(
944 value: Value,
945 config: &AgentTransformConfig,
946 context: AgentTreeContext,
947) -> Value {
948 match value {
949 Value::Object(map) => {
950 let preserve_list_envelope =
951 is_stable_list_envelope(&map) || is_keyset_cursor_envelope(&map);
952 let stream_entry = is_stream_entry(&map);
953 let relative_created_at = (context.array_member
954 && !context.inside_properties
955 && !map.contains_key("created_at_relative")
956 && !matches!(
957 context.receipt_context,
958 ReceiptContext::AppendRoot | ReceiptContext::BatchMember
959 ))
960 .then(|| map.get("created_at").and_then(Value::as_str))
961 .flatten()
962 .and_then(|raw| {
963 let (_, created_at) = exact_timestamp(raw)?;
964 let age = config.now.seconds.checked_sub(created_at)?;
965 age.checked_sub(i64::from(
968 timestamp_fraction_cmp(raw, config.now.nanoseconds) == Ordering::Greater,
969 ))
970 })
971 .filter(|difference| *difference >= 0)
972 .map(relative_time);
973 let mut out = Map::new();
974 for (k, v) in map {
975 if stream_entry && k == "record" {
976 out.insert(k, v);
977 continue;
978 }
979 if v.is_string()
980 && ((context.receipt_context == ReceiptContext::AppendRoot
981 && k == "created_at")
982 || (context.receipt_context == ReceiptContext::BatchMember
983 && matches!(k.as_str(), "created_at" | "updated_at")))
984 {
985 out.insert(k, v);
986 continue;
987 }
988 let child_receipt_context = if context.receipt_context == ReceiptContext::BatchRoot
989 && k == "results"
990 && v.is_array()
991 {
992 ReceiptContext::BatchResults
993 } else {
994 ReceiptContext::None
995 };
996 let child_inside_properties = context.inside_properties || k == "properties";
1000 let child_object_properties =
1001 context.object_properties || (k == "properties" && v.is_object());
1002 let transformed = transform_field_agent(
1003 &k,
1004 v,
1005 config,
1006 AgentFieldContext {
1007 inside_properties: child_inside_properties,
1008 object_properties: child_object_properties,
1009 preserve_list_envelope,
1010 receipt_context: child_receipt_context,
1011 },
1012 );
1013 match transformed {
1014 None => {} Some(tv) => {
1016 out.insert(k, tv);
1017 }
1018 }
1019 }
1020 if let Some(relative) = relative_created_at {
1021 out.insert("created_at_relative".to_string(), Value::String(relative));
1022 }
1023 Value::Object(out)
1024 }
1025 Value::Array(arr) => {
1026 let items: Vec<Value> = arr
1027 .into_iter()
1028 .map(|v| {
1029 transform_agent(
1030 v,
1031 config,
1032 AgentTreeContext {
1033 inside_properties: context.inside_properties,
1034 object_properties: context.object_properties,
1035 array_member: true,
1036 receipt_context: if context.receipt_context
1037 == ReceiptContext::BatchResults
1038 {
1039 ReceiptContext::BatchMember
1040 } else {
1041 ReceiptContext::None
1042 },
1043 },
1044 )
1045 })
1046 .collect();
1047 Value::Array(items)
1048 }
1049 other => other,
1050 }
1051}
1052
1053#[derive(Clone, Copy)]
1065struct AgentFieldContext {
1066 inside_properties: bool,
1067 object_properties: bool,
1068 preserve_list_envelope: bool,
1069 receipt_context: ReceiptContext,
1070}
1071
1072fn transform_field_agent(
1073 key: &str,
1074 value: Value,
1075 config: &AgentTransformConfig,
1076 context: AgentFieldContext,
1077) -> Option<Value> {
1078 match &value {
1079 Value::Null => {
1081 if config.preserved_nulls.contains(key)
1082 || (context.preserve_list_envelope && key == "next_after")
1083 {
1084 Some(value)
1085 } else {
1086 None
1087 }
1088 }
1089 Value::Array(a)
1091 if context.preserve_list_envelope
1092 && a.is_empty()
1093 && EMPTY_ARRAY_PRESERVE.contains(&key) =>
1094 {
1095 Some(value)
1096 }
1097 Value::String(s) if s.is_empty() && context.object_properties => Some(value),
1103 Value::String(s) if s.is_empty() => None,
1105 Value::Array(a) if a.is_empty() => None,
1106 Value::Object(o) if o.is_empty() => None,
1107 Value::Number(_) if config.scores.contains(key) => {
1109 if let Some(f) = value.as_f64() {
1110 Some(truncate_to_3_sig_figs(f))
1111 } else {
1112 Some(value)
1113 }
1114 }
1115 Value::String(s) if is_canonical_uuid(s) && should_shorten_uuid_field(key) => {
1117 Some(Value::String(s[..8].to_string()))
1118 }
1119 Value::String(s)
1122 if !context.inside_properties
1123 && !config.payload_timestamps.contains(key)
1124 && looks_like_iso8601(s) =>
1125 {
1126 Some(Value::String(
1127 exact_timestamp(s).map_or_else(|| s.clone(), |(rendered, _)| rendered),
1128 ))
1129 }
1130 Value::Object(_) | Value::Array(_) => Some(transform_agent(
1132 value,
1133 config,
1134 AgentTreeContext {
1135 inside_properties: context.inside_properties,
1136 object_properties: context.object_properties,
1137 array_member: false,
1138 receipt_context: context.receipt_context,
1139 },
1140 )),
1141 _ => Some(value),
1143 }
1144}
1145
1146fn is_canonical_uuid(s: &str) -> bool {
1148 if s.len() != UUID_CANONICAL_LEN {
1149 return false;
1150 }
1151 let b = s.as_bytes();
1152 b[8] == b'-'
1154 && b[13] == b'-'
1155 && b[18] == b'-'
1156 && b[23] == b'-'
1157 && b[..8].iter().all(|c| c.is_ascii_hexdigit())
1158 && b[9..13].iter().all(|c| c.is_ascii_hexdigit())
1159 && b[14..18].iter().all(|c| c.is_ascii_hexdigit())
1160 && b[19..23].iter().all(|c| c.is_ascii_hexdigit())
1161 && b[24..].iter().all(|c| c.is_ascii_hexdigit())
1162}
1163
1164fn looks_like_iso8601(s: &str) -> bool {
1168 if s.len() < 16 {
1169 return false;
1170 }
1171 let b = s.as_bytes();
1172 b[4] == b'-'
1173 && b[7] == b'-'
1174 && b[10] == b'T'
1175 && b[13] == b':'
1176 && b[..4].iter().all(|c| c.is_ascii_digit())
1177 && b[5..7].iter().all(|c| c.is_ascii_digit())
1178 && b[8..10].iter().all(|c| c.is_ascii_digit())
1179 && b[11..13].iter().all(|c| c.is_ascii_digit())
1180}
1181
1182fn exact_timestamp(s: &str) -> Option<(String, i64)> {
1184 let unix = parse_iso8601_unix(s)?;
1185 let tail = s.get(19..)?;
1186 let fraction = if let Some(after_dot) = tail.strip_prefix('.') {
1187 let digits = after_dot.bytes().take_while(u8::is_ascii_digit).count();
1188 tail.get(..digits + 1)?
1189 } else {
1190 ""
1191 };
1192 let utc = chrono::DateTime::<chrono::Utc>::from_timestamp(unix, 0)?;
1193 Some((
1194 format!("{}{fraction}Z", utc.format("%Y-%m-%dT%H:%M:%S")),
1195 unix,
1196 ))
1197}
1198
1199fn timestamp_fraction_cmp(timestamp: &str, now_nanoseconds: u32) -> Ordering {
1202 let fraction = if timestamp.as_bytes().get(19) == Some(&b'.') {
1203 timestamp.as_bytes()[20..]
1204 .split(|byte| !byte.is_ascii_digit())
1205 .next()
1206 .unwrap_or_default()
1207 } else {
1208 &[]
1209 };
1210 let now_fraction = format!("{now_nanoseconds:09}");
1211 for index in 0..fraction.len().max(9) {
1212 let created_digit = fraction.get(index).copied().unwrap_or(b'0');
1213 let now_digit = now_fraction.as_bytes().get(index).copied().unwrap_or(b'0');
1214 match created_digit.cmp(&now_digit) {
1215 Ordering::Equal => {}
1216 difference => return difference,
1217 }
1218 }
1219 Ordering::Equal
1220}
1221
1222fn parse_iso8601_unix(s: &str) -> Option<i64> {
1227 if s.len() < 20 {
1228 return None;
1229 }
1230 let b = s.as_bytes();
1231 if b[4] != b'-' || b[7] != b'-' || b[10] != b'T' || b[13] != b':' || b[16] != b':' {
1232 return None;
1233 }
1234 let year: i64 = parse_digits(&b[0..4])?;
1235 let month: i64 = parse_digits(&b[5..7])?;
1236 let day: i64 = parse_digits(&b[8..10])?;
1237 let hour: i64 = parse_digits(&b[11..13])?;
1238 let minute: i64 = parse_digits(&b[14..16])?;
1239 let second: i64 = parse_digits(&b[17..19])?;
1240
1241 let local = chrono::NaiveDate::from_ymd_opt(
1242 year.try_into().ok()?,
1243 month.try_into().ok()?,
1244 day.try_into().ok()?,
1245 )?
1246 .and_hms_opt(
1247 hour.try_into().ok()?,
1248 minute.try_into().ok()?,
1249 second.try_into().ok()?,
1250 )?
1251 .and_utc()
1252 .timestamp();
1253 let offset_secs = parse_tz_offset_secs(s.get(19..)?)?;
1254 local.checked_sub(offset_secs)
1255}
1256
1257fn parse_tz_offset_secs(tail: &str) -> Option<i64> {
1267 let mut rest = tail;
1268 if let Some(after_dot) = rest.strip_prefix('.') {
1269 let frac_len = after_dot.bytes().take_while(u8::is_ascii_digit).count();
1270 if frac_len == 0 {
1271 return None;
1272 }
1273 rest = &after_dot[frac_len..];
1274 }
1275
1276 if rest == "Z" {
1277 return Some(0);
1278 }
1279
1280 let sign: i64 = match rest.as_bytes().first()? {
1281 b'+' => 1,
1282 b'-' => -1,
1283 _ => return None,
1284 };
1285 let digits = &rest[1..];
1286 let (hh, mm) = match digits.len() {
1287 5 if digits.as_bytes()[2] == b':' => (
1289 parse_digits(&digits.as_bytes()[0..2])?,
1290 parse_digits(&digits.as_bytes()[3..5])?,
1291 ),
1292 4 => (
1294 parse_digits(&digits.as_bytes()[0..2])?,
1295 parse_digits(&digits.as_bytes()[2..4])?,
1296 ),
1297 _ => return None,
1298 };
1299 if hh > 23 || mm > 59 {
1300 return None;
1301 }
1302 Some(sign * (hh * 3600 + mm * 60))
1303}
1304
1305fn parse_digits(b: &[u8]) -> Option<i64> {
1306 let s = std::str::from_utf8(b).ok()?;
1307 s.parse().ok()
1308}
1309
1310fn relative_time(diff_secs: i64) -> String {
1312 if diff_secs < 60 {
1313 format!("{diff_secs}s ago")
1314 } else if diff_secs < 3600 {
1315 format!("{}m ago", diff_secs / 60)
1316 } else if diff_secs < 86400 {
1317 format!("{}h ago", diff_secs / 3600)
1318 } else {
1319 format!("{}d ago", diff_secs / 86400)
1320 }
1321}
1322
1323fn truncate_to_3_sig_figs(f: f64) -> Value {
1325 if f == 0.0 || !f.is_finite() {
1326 return Value::from(f);
1327 }
1328 let magnitude = f.abs().log10().floor() as i32;
1329 let factor = 10f64.powi(2 - magnitude);
1330 let rounded = (f * factor).round() / factor;
1331 serde_json::Number::from_f64(rounded)
1333 .map(Value::Number)
1334 .unwrap_or(Value::from(rounded))
1335}
1336
1337#[cfg(test)]
1338mod tests {
1339 use super::*;
1340 use serde_json::json;
1341
1342 const NOW: i64 = 1_748_016_480;
1344
1345 #[test]
1346 fn utc_rfc3339_seconds_normalizes_offsets_and_omits_fractions() {
1347 for (raw, expected) in [
1348 (
1349 "2026-01-01T00:15:30.987654321+01:00",
1350 "2025-12-31T23:15:30Z",
1351 ),
1352 ("1969-12-31T23:59:59.999999999Z", "1969-12-31T23:59:59Z"),
1353 ] {
1354 let instant = chrono::DateTime::parse_from_rfc3339(raw).unwrap();
1355 assert_eq!(format_utc_rfc3339_seconds(&instant), expected, "{raw}");
1356 }
1357 }
1358
1359 #[test]
1360 fn micros_to_iso_preserves_chrono_range_spelling() {
1361 for (micros, expected) in [
1362 (0, "1970-01-01T00:00:00.000000Z"),
1363 (-1, "1969-12-31T23:59:59.999999Z"),
1364 (1_748_016_480_123_456, "2025-05-23T16:08:00.123456Z"),
1365 (-62_198_755_200_000_000, "-0001-01-01T00:00:00.000000Z"),
1366 (253_402_300_800_000_000, "+10000-01-01T00:00:00.000000Z"),
1367 (-8_334_601_228_800_000_000, "-262143-01-01T00:00:00.000000Z"),
1368 (8_210_266_876_799_999_999, "+262142-12-31T23:59:59.999999Z"),
1369 ] {
1370 assert!(
1371 chrono::DateTime::<chrono::Utc>::from_timestamp_micros(micros).is_some(),
1372 "{micros}"
1373 );
1374 assert_eq!(micros_to_iso(micros), expected, "{micros}");
1375 }
1376 }
1377
1378 #[test]
1379 fn micros_to_iso_formats_outside_chrono_range_exactly() {
1380 for (micros, expected) in [
1381 (i64::MIN, "-290308-12-21T19:59:05.224192Z"),
1382 (i64::MAX, "+294247-01-10T04:00:54.775807Z"),
1383 (-8_334_601_228_800_000_001, "-262144-12-31T23:59:59.999999Z"),
1384 (8_210_266_876_800_000_000, "+262143-01-01T00:00:00.000000Z"),
1385 (-8_898_108_636_303_876_544, "-280000-02-29T12:34:56.123456Z"),
1386 (8_776_940_198_400_000_001, "+280100-03-01T00:00:00.000001Z"),
1387 ] {
1388 assert!(
1389 chrono::DateTime::<chrono::Utc>::from_timestamp_micros(micros).is_none(),
1390 "{micros}"
1391 );
1392 assert_eq!(micros_to_iso(micros), expected, "{micros}");
1393 }
1394 }
1395
1396 #[test]
1397 fn expanded_micros_timestamps_pass_through_agent_without_relative_labels() {
1398 for (micros, expected) in [
1399 (i64::MIN, "-290308-12-21T19:59:05.224192Z"),
1400 (i64::MAX, "+294247-01-10T04:00:54.775807Z"),
1401 ] {
1402 let shown = present(
1403 json!({"items": [{"created_at": micros_to_iso(micros)}]}),
1404 PresentationMode::Agent,
1405 NOW,
1406 );
1407 assert_eq!(shown, json!({"items": [{"created_at": expected}]}));
1408 assert!(rfc3339_to_utc_micros(expected).is_err());
1409 }
1410 }
1411
1412 #[test]
1413 fn rfc3339_to_utc_micros_round_trips_and_rejects_partial_forms() {
1414 let micros = 1_748_016_480_000_000_i64;
1415 assert_eq!(rfc3339_to_utc_micros(µs_to_iso(micros)), Ok(micros));
1416 assert_eq!(
1419 rfc3339_to_utc_micros(" 2025-05-23T12:08:00-04:00 "),
1420 Ok(micros)
1421 );
1422 assert!(rfc3339_to_utc_micros("2026-05-23").is_err());
1423 assert!(rfc3339_to_utc_micros("2026-05-23T16:18:00").is_err());
1424 }
1425
1426 fn agent(v: Value) -> Value {
1427 present(v, PresentationMode::Agent, NOW)
1428 }
1429
1430 #[test]
1431 fn agent_preserves_empty_string_under_properties() {
1432 let v = json!({
1437 "id": "a1b2c3d4",
1438 "summary": "",
1439 "properties": {"k": "", "nested": {"deep": ""}},
1440 });
1441 let out = agent(v);
1442 assert_eq!(out["properties"]["k"], json!(""));
1443 assert_eq!(out["properties"]["nested"]["deep"], json!(""));
1444 assert!(out.get("summary").is_none());
1446 }
1447
1448 #[test]
1449 fn agent_drops_scalar_empty_properties_value() {
1450 let v = json!({
1454 "id": "a1b2c3d4",
1455 "properties": "",
1456 });
1457 let out = agent(v);
1458 assert!(out.get("properties").is_none());
1459 }
1460
1461 #[test]
1462 fn agent_still_drops_empty_containers_under_properties() {
1463 let v = json!({
1466 "id": "a1b2c3d4",
1467 "properties": {"tags": [], "meta": {}, "kept": "x"},
1468 });
1469 let out = agent(v);
1470 assert!(out["properties"].get("tags").is_none());
1471 assert!(out["properties"].get("meta").is_none());
1472 assert_eq!(out["properties"]["kept"], json!("x"));
1473 }
1474
1475 #[test]
1476 fn verbose_passthrough() {
1477 let v = json!({"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "title": "X"});
1478 let out = present(v.clone(), PresentationMode::Verbose, NOW);
1479 assert_eq!(out, v);
1480 }
1481
1482 #[test]
1483 fn agent_shortens_uuid() {
1484 let v = json!({"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"});
1485 let out = agent(v);
1486 assert_eq!(out["id"], json!("a1b2c3d4"));
1487 }
1488
1489 #[test]
1490 fn agent_drops_empty_string() {
1491 let v = json!({"title": "ok", "description": ""});
1492 let out = agent(v);
1493 assert!(out.get("description").is_none());
1494 assert_eq!(out["title"], json!("ok"));
1495 }
1496
1497 #[test]
1498 fn agent_drops_empty_array() {
1499 let v = json!({"tags": [], "title": "ok"});
1500 let out = agent(v);
1501 assert!(out.get("tags").is_none());
1502 }
1503
1504 #[test]
1505 fn agent_preserves_empty_list_page_arrays() {
1506 let v = json!({
1507 "items": [],
1508 "entities": [],
1509 "notes": [],
1510 "edges": [],
1511 "results": [],
1512 "entries": [],
1513 "neighbors": [],
1514 "next_after": null,
1515 "requested_limit": 10,
1516 "effective_limit": 10,
1517 "limit_clamped": false,
1518 });
1519 let out = agent(v);
1520 for key in EMPTY_ARRAY_PRESERVE {
1521 assert_eq!(out[*key], json!([]), "missing structural key {key}");
1522 }
1523 assert_eq!(out["next_after"], json!(null));
1524 }
1525
1526 #[test]
1527 fn agent_preserves_keyset_cursor_envelope_completion_signals() {
1528 let out = agent(json!({
1530 "results": [],
1531 "limit": 500,
1532 "order": "created_at ASC, id ASC",
1533 "next_after": null,
1534 }));
1535 assert_eq!(out["results"], json!([]));
1536 assert!(
1537 out.get("next_after").is_some_and(Value::is_null),
1538 "terminal cursor page must keep next_after:null: {out}"
1539 );
1540 }
1541
1542 #[test]
1543 fn agent_results_without_next_after_is_not_a_cursor_envelope() {
1544 let out = agent(json!({"results": [], "limit": 500, "title": "ordinary response"}));
1545 assert!(out.get("results").is_none());
1546 }
1547
1548 #[test]
1549 fn agent_still_drops_empty_arrays_outside_list_envelopes() {
1550 let out = agent(json!({"items": [], "entities": [], "title": "ordinary response"}));
1551 assert!(out.get("items").is_none());
1552 assert!(out.get("entities").is_none());
1553 }
1554
1555 #[test]
1556 fn agent_drops_empty_object() {
1557 let v = json!({"properties": {}, "title": "ok"});
1558 let out = agent(v);
1559 assert!(out.get("properties").is_none());
1560 }
1561
1562 #[test]
1563 fn agent_drops_non_lifecycle_null() {
1564 let v = json!({"result": null, "title": "ok"});
1565 let out = agent(v);
1566 assert!(out.get("result").is_none());
1567 }
1568
1569 #[test]
1570 fn agent_preserves_lifecycle_null() {
1571 let v = json!({"completed_at": null, "due_at": null, "title": "ok"});
1572 let out = agent(v);
1573 assert_eq!(out["completed_at"], json!(null));
1574 assert_eq!(out["due_at"], json!(null));
1575 }
1576
1577 #[test]
1578 fn agent_preserves_relationship_null() {
1579 let v = json!({"parent_id": null, "superseded_by": null});
1580 let out = agent(v);
1581 assert_eq!(out["parent_id"], json!(null));
1582 assert_eq!(out["superseded_by"], json!(null));
1583 }
1584
1585 #[test]
1586 fn agent_preserves_unknown_channel_heartbeat_nulls() {
1587 let v = json!({
1588 "channels": [{
1589 "poll_interval_secs": null,
1590 "stalled": null,
1591 "last_success_at": null,
1592 "last_poll_attempt_at": null,
1593 "last_failure_at": null,
1594 "last_error": null,
1595 "consecutive_failures": null,
1596 "quarantined_count": 1,
1597 }]
1598 });
1599 let out = agent(v);
1600 let channel = out["channels"][0].as_object().expect("channel object");
1601 for field in [
1602 "poll_interval_secs",
1603 "stalled",
1604 "last_success_at",
1605 "last_poll_attempt_at",
1606 "last_failure_at",
1607 "last_error",
1608 "consecutive_failures",
1609 ] {
1610 assert_eq!(
1611 channel.get(field),
1612 Some(&Value::Null),
1613 "Agent presentation must preserve unknown heartbeat fact `{field}`"
1614 );
1615 }
1616 }
1617
1618 #[test]
1619 fn agent_truncates_score_field() {
1620 let v = json!({"score": 0.12345678});
1621 let out = agent(v);
1622 let s = out["score"].as_f64().unwrap();
1623 assert!((s - 0.123).abs() < 1e-9, "expected ~0.123, got {s}");
1624 }
1625
1626 #[test]
1627 fn agent_presents_ranking_alias_and_nested_evidence() {
1628 let canonical = json!([
1629 {
1630 "rank_score": 0.0325224749,
1631 "score": 0.0325224749,
1632 "rank_score_kind": "rrf",
1633 "signals": {"vector_similarity": 0.842567, "keyword_score": 18.375}
1634 },
1635 {"rank_score": 0.0, "score": 0.0, "rank_score_kind": "keyword", "signals": {}}
1636 ]);
1637 for mode in [PresentationMode::Verbose, PresentationMode::Human] {
1638 assert_eq!(present(canonical.clone(), mode, NOW), canonical);
1639 }
1640 let shown = agent(canonical);
1641 assert_eq!(shown[0]["rank_score"], json!(0.0325));
1642 assert_eq!(shown[0]["score"], shown[0]["rank_score"]);
1643 assert_eq!(shown[0]["rank_score_kind"], "rrf");
1644 assert_eq!(shown[0]["signals"]["vector_similarity"], json!(0.843));
1645 assert_eq!(shown[0]["signals"]["keyword_score"], json!(18.4));
1646 assert_eq!(shown[1]["rank_score"], json!(0.0));
1647 assert_eq!(shown[1]["score"], shown[1]["rank_score"]);
1648 assert_eq!(shown[1]["rank_score_kind"], "keyword");
1649 assert!(shown[1].get("signals").is_none());
1650 }
1651
1652 #[test]
1653 fn agent_renders_old_timestamp_exactly() {
1654 let v = json!({"created_at": "2020-01-01T10:30:45.123456Z"});
1655 let out = agent(v);
1656 assert_eq!(out["created_at"], json!("2020-01-01T10:30:45.123456Z"));
1657 assert!(out.get("created_at_relative").is_none());
1658 }
1659
1660 #[test]
1661 fn agent_renders_recent_timestamp_exactly() {
1662 let ts_unix = NOW - 180;
1663 let ts = unix_to_iso8601(ts_unix);
1664 let v = json!({"updated_at": ts.clone()});
1665 let out = agent(v);
1666 assert_eq!(out["updated_at"], json!(ts));
1667 }
1668
1669 #[test]
1670 fn agent_does_not_compact_top_level_trigger_at_field() {
1671 let at = "2026-07-11T19:00:00-04:00";
1680 let v = json!({
1681 "id": "a1b2c3d4",
1682 "full_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
1683 "event_type": "remind",
1684 "trigger_at": at,
1685 "repeat": null,
1686 "status": "pending",
1687 });
1688 let out = agent(v);
1689 assert_eq!(out["trigger_at"], json!(at));
1690 }
1691
1692 #[test]
1693 fn agent_does_not_compact_top_level_trigger_at_utc() {
1694 let at = "2026-07-11T23:00:00Z";
1695 let v = json!({"trigger_at": at});
1696 let out = agent(v);
1697 assert_eq!(out["trigger_at"], json!(at));
1698 }
1699
1700 #[test]
1701 fn agent_does_not_compact_top_level_trigger_at_offset_less() {
1702 let at = "2026-07-11T23:00:00";
1703 let v = json!({"trigger_at": at});
1704 let out = agent(v);
1705 assert_eq!(out["trigger_at"], json!(at));
1706 }
1707
1708 #[test]
1709 fn agent_still_renders_other_top_level_timestamps_alongside_trigger_at() {
1710 let v = json!({
1713 "trigger_at": "2026-07-11T19:00:00-04:00",
1714 "created_at": "2020-01-01T10:30:45.123456Z",
1715 });
1716 let out = agent(v);
1717 assert_eq!(out["trigger_at"], json!("2026-07-11T19:00:00-04:00"));
1718 assert_eq!(out["created_at"], json!("2020-01-01T10:30:45.123456Z"));
1719 }
1720
1721 #[test]
1722 fn agent_does_not_compact_top_level_due() {
1723 let due = "2026-08-01T09:30:15-04:00";
1728 let v = json!({"due": due});
1729 let out = agent(v);
1730 assert_eq!(out["due"], json!(due));
1731 }
1732
1733 #[test]
1734 fn agent_still_protects_nested_trigger_at_under_properties() {
1735 let at = "2026-07-11T19:00:00-04:00";
1739 let v = json!({
1740 "id": "a1b2c3d4",
1741 "properties": {"trigger_at": at, "status": "pending"},
1742 });
1743 let out = agent(v);
1744 assert_eq!(out["properties"]["trigger_at"], json!(at));
1745 }
1746
1747 #[test]
1748 fn agent_recurses_into_nested_objects() {
1749 let v = json!({
1750 "items": [
1751 {
1752 "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
1753 "tags": [],
1754 "score": 0.9999
1755 }
1756 ]
1757 });
1758 let out = agent(v);
1759 let item = &out["items"][0];
1760 assert_eq!(item["id"], json!("a1b2c3d4"));
1761 assert!(item.get("tags").is_none());
1762 let s = item["score"].as_f64().unwrap();
1763 assert!((s - 1.0).abs() < 1e-9);
1764 }
1765
1766 #[test]
1769 fn agent_preserves_full_id_as_36_chars() {
1770 let uuid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
1771 let v = json!({"id": uuid, "full_id": uuid, "title": "X"});
1772 let out = agent(v);
1773 assert_eq!(
1775 out["id"],
1776 json!("a1b2c3d4"),
1777 "id should be 8-char short form"
1778 );
1779 assert_eq!(
1781 out["full_id"].as_str().unwrap().len(),
1782 36,
1783 "full_id must be 36 chars in agent mode"
1784 );
1785 assert_eq!(
1786 out["full_id"],
1787 json!(uuid),
1788 "full_id must equal the original UUID"
1789 );
1790 assert!(
1792 out["full_id"]
1793 .as_str()
1794 .unwrap()
1795 .starts_with(out["id"].as_str().unwrap()),
1796 "full_id must start with the short id prefix"
1797 );
1798 }
1799
1800 #[test]
1801 fn is_canonical_uuid_recognizes_valid() {
1802 assert!(is_canonical_uuid("a1b2c3d4-e5f6-7890-abcd-ef1234567890"));
1803 assert!(!is_canonical_uuid("a1b2c3d4"));
1804 assert!(!is_canonical_uuid("not-a-uuid-at-all-here---------"));
1805 }
1806
1807 #[test]
1808 fn looks_like_iso8601_recognizes_valid() {
1809 assert!(looks_like_iso8601("2026-05-23T16:18:15.234567Z"));
1810 assert!(!looks_like_iso8601("not a timestamp"));
1811 assert!(!looks_like_iso8601("2026-05-23"));
1812 }
1813
1814 fn unix_to_iso8601(unix: i64) -> String {
1816 let (y, mo, d, h, mi, s) = unix_to_civil(unix);
1817 format!("{y:04}-{mo:02}-{d:02}T{h:02}:{mi:02}:{s:02}Z")
1818 }
1819
1820 fn unix_to_civil(unix: i64) -> (i64, i64, i64, i64, i64, i64) {
1821 let s = unix % 86400;
1822 let days = unix / 86400;
1823 let h = s / 3600;
1824 let m = (s % 3600) / 60;
1825 let sec = s % 60;
1826 let z = days + 719468;
1828 let era = z.div_euclid(146097);
1829 let doe = z - era * 146097;
1830 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
1831 let y = yoe + era * 400;
1832 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
1833 let mp = (5 * doy + 2) / 153;
1834 let d = doy - (153 * mp + 2) / 5 + 1;
1835 let mo = if mp < 10 { mp + 3 } else { mp - 9 };
1836 let y = if mo <= 2 { y + 1 } else { y };
1837 (y, mo, d, h, m, sec)
1838 }
1839
1840 #[test]
1841 fn agent_does_not_shorten_uuid_shaped_content_fields() {
1842 let uuid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
1843 let out = agent(json!({
1844 "id": uuid,
1845 "full_id": uuid,
1846 "content": uuid,
1847 "description": uuid,
1848 "title": uuid,
1849 "query": uuid,
1850 }));
1851
1852 assert_eq!(out["id"], json!("a1b2c3d4"));
1853 assert_eq!(out["full_id"], json!(uuid));
1854 assert_eq!(out["content"], json!(uuid));
1855 assert_eq!(out["description"], json!(uuid));
1856 assert_eq!(out["title"], json!(uuid));
1857 assert_eq!(out["query"], json!(uuid));
1858 }
1859
1860 #[test]
1861 fn agent_shortens_suffix_id_fields() {
1862 let uuid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
1863 let out = agent(json!({
1864 "note_id": uuid,
1865 "source_id": uuid,
1866 "target_id": uuid,
1867 }));
1868
1869 assert_eq!(out["note_id"], json!("a1b2c3d4"));
1870 assert_eq!(out["source_id"], json!("a1b2c3d4"));
1871 assert_eq!(out["target_id"], json!("a1b2c3d4"));
1872 }
1873
1874 #[test]
1875 fn agent_preserves_strict_round_trip_uuid_fields() {
1876 let uuid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
1877 let out = agent(json!({
1878 "context_entity_id": uuid,
1879 "thread_id": uuid,
1880 "session_id": uuid,
1881 "project_id": uuid,
1882 "properties": {
1883 "context_entity_id": uuid,
1884 "thread_id": uuid,
1885 "outbound_ref": uuid,
1886 "parent_id": uuid,
1887 "session_id": uuid,
1888 "project_id": uuid,
1889 },
1890 "parent_id": uuid,
1891 }));
1892
1893 assert_eq!(out["context_entity_id"], json!(uuid));
1894 assert_eq!(out["thread_id"], json!(uuid));
1895 assert_eq!(out["session_id"], json!(uuid));
1896 assert_eq!(out["project_id"], json!(uuid));
1897 assert_eq!(out["properties"]["context_entity_id"], json!(uuid));
1898 assert_eq!(out["properties"]["thread_id"], json!(uuid));
1899 assert_eq!(out["properties"]["outbound_ref"], json!(uuid));
1900 assert_eq!(out["properties"]["parent_id"], json!(uuid));
1901 assert_eq!(out["properties"]["session_id"], json!(uuid));
1902 assert_eq!(out["properties"]["project_id"], json!(uuid));
1903 assert_eq!(out["parent_id"], json!(uuid));
1904 }
1905
1906 #[test]
1910 fn format_verbose_json_preserves_full_shape() {
1911 let v = json!({
1912 "full_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
1913 "namespace": "local",
1914 "properties": {"k": "v"},
1915 "title": "test"
1916 });
1917 let rendered = render_format(v.clone(), OutputFormat::Json, PresentationMode::Verbose);
1918 let parsed: Value = serde_json::from_str(&rendered).unwrap();
1919 assert!(
1921 parsed.get("full_id").is_some(),
1922 "json mode must keep full_id"
1923 );
1924 assert_eq!(
1926 parsed.get("namespace").and_then(Value::as_str),
1927 Some("local")
1928 );
1929 assert!(parsed.get("properties").is_some());
1931 }
1932
1933 #[test]
1938 fn format_agent_json_drops_redundancy() {
1939 let v = json!({
1940 "full_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
1941 "namespace": "local",
1942 "title": "test",
1943 "status": "next",
1944 "properties": {"status": "next", "additive": true}
1945 });
1946 let rendered = render_format(v, OutputFormat::Json, PresentationMode::Agent);
1947 let parsed: Value = serde_json::from_str(&rendered).unwrap();
1948 assert!(
1949 parsed.get("full_id").is_some(),
1950 "Agent JSON must keep full_id: it is the machine contract's chaining handle"
1951 );
1952 assert!(parsed.get("namespace").is_none());
1953 assert_eq!(parsed["status"], json!("next"));
1954 assert!(parsed["properties"].get("status").is_none());
1955 assert_eq!(parsed["properties"]["additive"], json!(true));
1956 }
1957
1958 #[test]
1963 fn agent_json_keeps_full_id_while_auto_still_drops_it() {
1964 let v = json!({
1965 "full_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
1966 "namespace": "local",
1967 "title": "test",
1968 "properties": {"title": "test", "additive": true}
1969 });
1970
1971 let json_rendered = render_format(v.clone(), OutputFormat::Json, PresentationMode::Agent);
1972 let json_parsed: Value = serde_json::from_str(&json_rendered).unwrap();
1973 assert!(
1974 json_parsed.get("full_id").is_some(),
1975 "Agent JSON must keep full_id"
1976 );
1977 assert!(
1978 json_parsed.get("namespace").is_none(),
1979 "Agent JSON must elide namespace=\"local\""
1980 );
1981 assert!(
1982 json_parsed["properties"].get("title").is_none(),
1983 "Agent JSON must dedupe a duplicate properties entry"
1984 );
1985 assert_eq!(json_parsed["properties"]["additive"], json!(true));
1986
1987 let auto_rendered = render_format(v, OutputFormat::Auto, PresentationMode::Agent);
1988 assert!(
1989 !auto_rendered.contains("full_id"),
1990 "Auto (view scope) must still drop full_id: {auto_rendered}"
1991 );
1992 assert!(
1993 !auto_rendered.contains("\"namespace\""),
1994 "Auto must still elide namespace=\"local\": {auto_rendered}"
1995 );
1996 }
1997
1998 #[test]
2000 fn format_auto_homogeneous_array_renders_markdown_table() {
2001 let v = json!([
2002 {"id": "abc", "title": "First"},
2003 {"id": "def", "title": "Second"}
2004 ]);
2005 let rendered = render_format(v, OutputFormat::Auto, PresentationMode::Agent);
2006 assert!(rendered.starts_with('|'), "must start with |");
2007 assert!(
2008 rendered.contains("| id |") || rendered.contains("| id"),
2009 "must have id column"
2010 );
2011 assert!(rendered.contains("title"), "must have title column");
2012 assert!(rendered.contains("|---|"), "must have separator row");
2013 assert!(rendered.contains("abc"), "must have first row data");
2014 assert!(rendered.contains("Second"), "must have second row data");
2015 }
2016
2017 #[test]
2019 fn format_auto_single_record_renders_compact_json() {
2020 let v = json!({"id": "abc", "title": "Hello World"});
2021 let rendered = render_format(v.clone(), OutputFormat::Auto, PresentationMode::Agent);
2022 let parsed: Value = serde_json::from_str(&rendered).expect("must be valid JSON");
2023 assert_eq!(parsed, v, "single record must round-trip losslessly");
2024 assert!(
2025 !rendered.starts_with('|'),
2026 "single record must not be a markdown table"
2027 );
2028 }
2029
2030 #[test]
2034 fn format_auto_single_record_large_payload_survives_whole() {
2035 let briefing = "line one\nline two\n".repeat(500); let v = json!({"id": "abc", "markdown": briefing.clone()});
2037 let rendered = render_format(v, OutputFormat::Auto, PresentationMode::Agent);
2038 let parsed: Value = serde_json::from_str(&rendered).expect("must be valid JSON");
2039 assert_eq!(
2040 parsed.get("markdown").and_then(Value::as_str),
2041 Some(briefing.as_str()),
2042 "large payload field must survive untruncated"
2043 );
2044 }
2045
2046 #[test]
2049 fn format_auto_table_preserves_sibling_scalars() {
2050 let v = json!({
2051 "results": [
2052 {"id": "abc", "title": "First"},
2053 {"id": "def", "title": "Second"}
2054 ],
2055 "has_more": true,
2056 "offset": 20,
2057 "page_size": 2,
2058 "truncated": false
2059 });
2060 let rendered = render_format(v, OutputFormat::Auto, PresentationMode::Agent);
2061 assert!(rendered.contains("| id"), "records must render as a table");
2062 assert!(
2063 rendered.contains("has_more: true"),
2064 "has_more sibling must survive: {rendered}"
2065 );
2066 assert!(
2067 rendered.contains("offset: 20"),
2068 "offset sibling must survive"
2069 );
2070 assert!(
2071 rendered.contains("page_size: 2"),
2072 "page_size sibling must survive"
2073 );
2074 assert!(
2075 rendered.contains("truncated: false"),
2076 "truncated sibling must survive"
2077 );
2078 }
2079
2080 #[test]
2083 fn format_auto_nested_cell_renders_elision_marker() {
2084 let big_nested: Value = json!({"k": "v".repeat(300), "other": {"deep": true}});
2085 let v = json!([
2086 {"id": "abc", "properties": big_nested},
2087 {"id": "def", "properties": {"x": 1}}
2088 ]);
2089 let rendered = render_format(v, OutputFormat::Auto, PresentationMode::Agent);
2090 assert!(
2091 rendered.contains("{…}"),
2092 "nested object cell must render the elision marker: {rendered}"
2093 );
2094 assert!(
2095 !rendered.contains("..."),
2096 "no truncated pseudo-JSON in nested cells"
2097 );
2098 }
2099
2100 #[test]
2102 fn format_auto_scalar_array_cell_renders_compact_json() {
2103 let v = json!([
2104 {"id": "abc", "tags": ["lesson", "khive"]},
2105 {"id": "def", "tags": ["adr"]}
2106 ]);
2107 let rendered = render_format(v, OutputFormat::Auto, PresentationMode::Agent);
2108 assert!(
2109 rendered.contains(r#"["lesson","khive"]"#),
2110 "scalar array cell must be compact JSON: {rendered}"
2111 );
2112 }
2113
2114 #[test]
2117 fn format_auto_identity_columns_not_truncated() {
2118 let long_title = "T".repeat(300);
2119 let long_note = "N".repeat(300);
2120 let v = json!([
2121 {"id": "abc", "title": long_title.clone(), "note": long_note.clone()},
2122 {"id": "def", "title": "short", "note": "short"}
2123 ]);
2124 let rendered = render_format(v, OutputFormat::Auto, PresentationMode::Agent);
2125 assert!(
2126 rendered.contains(&long_title),
2127 "title column must not be truncated"
2128 );
2129 assert!(
2130 !rendered.contains(&long_note),
2131 "non-exempt column must still truncate"
2132 );
2133 }
2134
2135 #[test]
2137 fn format_auto_verbose_disables_truncation() {
2138 let long_note = "N".repeat(300);
2139 let v = json!([
2140 {"id": "abc", "note": long_note.clone()},
2141 {"id": "def", "note": "short"}
2142 ]);
2143 let rendered = render_format(v, OutputFormat::Auto, PresentationMode::Verbose);
2144 assert!(
2145 rendered.contains(&long_note),
2146 "verbose must render full cell content"
2147 );
2148 }
2149
2150 #[test]
2154 fn table_path_hoists_payload_scalars_to_columns() {
2155 let record = |n: u32| {
2159 json!({
2160 "id": format!("evt-{n}"),
2161 "properties": {
2162 "trigger_at": "2026-09-01T14:00:00-04:00",
2163 "status": "pending",
2164 "dispatch_receipt": {"state": "succeeded"}
2165 }
2166 })
2167 };
2168 let out = render_format(
2169 json!([record(1), record(2)]),
2170 OutputFormat::Auto,
2171 PresentationMode::Agent,
2172 );
2173 let header = out.lines().next().expect("table header");
2174 assert!(
2175 header.contains("trigger_at") && header.contains("status"),
2176 "hoisted scalars must appear as table columns, got header: {header}"
2177 );
2178 assert!(
2179 out.contains("2026-09-01T14:00:00-04:00") && out.contains("pending"),
2180 "hoisted values must render in cells:\n{out}"
2181 );
2182 }
2183
2184 #[test]
2185 fn redundancy_drop_no_longer_hoists_outside_tables() {
2186 let v = json!({
2190 "id": "abc",
2191 "properties": {
2192 "trigger_at": "2026-09-01T14:00:00-04:00",
2193 "status": "pending"
2194 }
2195 });
2196 let reduced = apply_redundancy_drop(v, RedundancyScope::View);
2197 assert!(
2198 reduced.get("trigger_at").is_none() && reduced.get("status").is_none(),
2199 "the pre-pass must not hoist; the hoist is table-path only"
2200 );
2201 let props = reduced.get("properties").expect("properties must remain");
2202 assert_eq!(
2203 props.get("status").and_then(Value::as_str),
2204 Some("pending"),
2205 "fallback shape keeps payload fields inside properties"
2206 );
2207 }
2208
2209 #[test]
2210 fn sibling_string_with_newline_renders_as_json_literal() {
2211 let v = json!({
2214 "items": [{"id": "a", "kind": "x"}, {"id": "b", "kind": "y"}],
2215 "note": "line one\nforged_key: forged_value",
2216 "has_more": true
2217 });
2218 let out = render_format(v, OutputFormat::Auto, PresentationMode::Agent);
2219 assert!(
2220 !out.contains("\nforged_key: forged_value"),
2221 "raw newline from a sibling string must not start a new line:\n{out}"
2222 );
2223 assert!(
2224 out.contains(r#"note: "line one\nforged_key: forged_value""#),
2225 "newline-bearing sibling renders as its JSON literal:\n{out}"
2226 );
2227 assert!(out.contains("has_more: true"), "siblings still preserved");
2228 }
2229
2230 #[test]
2231 fn carriage_return_in_cell_collapses_like_newline() {
2232 let v = json!([
2236 {"id": "a", "note": "before\rafter"},
2237 {"id": "b", "note": "one\r\ntwo"}
2238 ]);
2239 let out = render_format(v, OutputFormat::Auto, PresentationMode::Agent);
2240 assert!(
2241 !out.contains('\r'),
2242 "no raw carriage return may survive into rendered output:\n{out:?}"
2243 );
2244 assert!(
2245 out.contains("before after") && out.contains("one two"),
2246 "\\r and \\r\\n collapse to spaces inside cells:\n{out}"
2247 );
2248 }
2249
2250 #[test]
2252 fn redundancy_drop_hoist_does_not_overwrite_top_level() {
2253 let v = json!({
2254 "id": "abc",
2255 "status": "active",
2256 "properties": {"status": "pending"}
2257 });
2258 let reduced = apply_redundancy_drop(v, RedundancyScope::View);
2259 assert_eq!(
2260 reduced.get("status").and_then(Value::as_str),
2261 Some("active"),
2262 "existing top-level status must win"
2263 );
2264 assert_eq!(
2265 reduced
2266 .get("properties")
2267 .and_then(|p| p.get("status"))
2268 .and_then(Value::as_str),
2269 Some("pending"),
2270 "conflicting properties value must stay where it was"
2271 );
2272 }
2273
2274 #[test]
2276 fn format_auto_scalar_fallback_compact_json() {
2277 let v = json!(42);
2278 let rendered = render_format(v, OutputFormat::Auto, PresentationMode::Agent);
2279 assert_eq!(rendered, "42");
2280 }
2281
2282 #[test]
2284 fn format_table_forces_markdown_when_array() {
2285 let v = json!({
2286 "items": [
2287 {"name": "A", "score": 1},
2288 {"name": "B", "score": 2}
2289 ]
2290 });
2291 let rendered = render_format(v, OutputFormat::Table, PresentationMode::Agent);
2292 assert!(
2293 rendered.contains("|"),
2294 "table format must produce markdown table"
2295 );
2296 assert!(rendered.contains("name"), "must have name column");
2297 assert!(rendered.contains("score"), "must have score column");
2298 }
2299
2300 #[test]
2302 fn format_table_falls_back_to_json_when_no_array() {
2303 let v = json!({"single": "value"});
2304 let rendered = render_format(v, OutputFormat::Table, PresentationMode::Agent);
2305 let parsed: Value = serde_json::from_str(&rendered).unwrap();
2307 assert_eq!(parsed["single"], json!("value"));
2308 }
2309
2310 #[test]
2312 fn format_auto_verbose_skips_redundancy_drop() {
2313 let v = json!({
2314 "full_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
2315 "namespace": "local",
2316 "title": "test"
2317 });
2318 let rendered = render_format(v, OutputFormat::Auto, PresentationMode::Verbose);
2321 assert!(
2322 rendered.contains("full_id"),
2323 "verbose must preserve full_id"
2324 );
2325 assert!(
2326 rendered.contains("namespace"),
2327 "verbose must preserve namespace"
2328 );
2329 }
2330
2331 #[test]
2335 fn redundancy_drop_does_not_corrupt_error_shape() {
2336 let v = json!({"ok": false, "error": "something failed", "namespace": "local"});
2337 let reduced = apply_redundancy_drop(v.clone(), RedundancyScope::View);
2342 assert!(
2343 reduced.get("error").is_some(),
2344 "redundancy drop must preserve error field"
2345 );
2346 assert_eq!(
2347 reduced.get("ok").and_then(Value::as_bool),
2348 Some(false),
2349 "redundancy drop must preserve ok=false"
2350 );
2351 }
2352
2353 #[test]
2355 fn redundancy_drop_properties_dedup() {
2356 let v = json!({
2357 "id": "abc",
2358 "title": "Same",
2359 "properties": {
2360 "title": "Same", "extra": "unique" }
2363 });
2364 let reduced = apply_redundancy_drop(v, RedundancyScope::View);
2365 let props = reduced.get("properties").expect("properties must remain");
2366 assert!(props.get("extra").is_some(), "unique property must be kept");
2367 assert!(
2368 props.get("title").is_none(),
2369 "duplicate top-level property must be removed"
2370 );
2371 }
2372
2373 #[test]
2375 fn cell_text_truncates_long_values() {
2376 let long = "X".repeat(200);
2377 let v = json!([
2378 {"col": long.clone()},
2379 {"col": "short"}
2380 ]);
2381 let rendered = render_format(v, OutputFormat::Auto, PresentationMode::Agent);
2382 assert!(
2384 rendered.contains("..."),
2385 "long cell must be truncated with ..."
2386 );
2387 assert!(
2388 !rendered.contains(&long),
2389 "full long string must not appear in table"
2390 );
2391 }
2392
2393 #[test]
2399 fn cell_text_truncation_is_utf8_safe() {
2400 let prefix = "a".repeat(119);
2403 let suffix = "中".repeat(10); let long_multibyte = format!("{prefix}{suffix}trailing");
2405 let v = json!([
2406 {"col": long_multibyte.clone()},
2407 {"col": "ok"}
2408 ]);
2409 let rendered = render_format(v, OutputFormat::Auto, PresentationMode::Agent);
2411 assert!(
2412 rendered.contains("..."),
2413 "multibyte cell must be truncated with ..."
2414 );
2415 assert!(
2417 std::str::from_utf8(rendered.as_bytes()).is_ok(),
2418 "rendered output must be valid UTF-8"
2419 );
2420 }
2421
2422 #[test]
2425 fn parse_iso8601_unix_negative_offset_matches_equivalent_utc() {
2426 assert_eq!(
2428 parse_iso8601_unix("2026-07-09T11:55:00-04:00"),
2429 parse_iso8601_unix("2026-07-09T15:55:00Z")
2430 );
2431 }
2432
2433 #[test]
2434 fn parse_iso8601_unix_positive_offset_matches_equivalent_utc() {
2435 assert_eq!(
2437 parse_iso8601_unix("2026-05-23T20:15:00+04:00"),
2438 parse_iso8601_unix("2026-05-23T16:15:00Z")
2439 );
2440 }
2441
2442 #[test]
2443 fn parse_iso8601_unix_zero_offset_matches_z() {
2444 assert_eq!(
2445 parse_iso8601_unix("2026-07-09T15:55:00+00:00"),
2446 parse_iso8601_unix("2026-07-09T15:55:00Z")
2447 );
2448 }
2449
2450 #[test]
2451 fn parse_iso8601_unix_compact_offset_form_matches_colon_form() {
2452 assert_eq!(
2453 parse_iso8601_unix("2026-07-09T11:55:00-0400"),
2454 parse_iso8601_unix("2026-07-09T11:55:00-04:00")
2455 );
2456 }
2457
2458 #[test]
2459 fn parse_iso8601_unix_fractional_seconds_with_offset() {
2460 assert_eq!(
2463 parse_iso8601_unix("2026-07-09T11:55:00.123-04:00"),
2464 parse_iso8601_unix("2026-07-09T15:55:00Z")
2465 );
2466 }
2467
2468 #[test]
2469 fn parse_iso8601_unix_fractional_seconds_with_z() {
2470 assert_eq!(
2471 parse_iso8601_unix("2026-07-09T15:55:00.999Z"),
2472 parse_iso8601_unix("2026-07-09T15:55:00Z")
2473 );
2474 }
2475
2476 #[test]
2477 fn parse_iso8601_unix_rejects_bare_form() {
2478 assert_eq!(parse_iso8601_unix("2026-07-09T15:55:00"), None);
2479 }
2480
2481 #[test]
2482 fn parse_iso8601_unix_malformed_tail_returns_none() {
2483 assert_eq!(parse_iso8601_unix("2026-07-09T15:55:00X"), None);
2484 assert_eq!(parse_iso8601_unix("2026-07-09T15:55:00+04"), None);
2485 assert_eq!(parse_iso8601_unix("2026-07-09T15:55:00."), None);
2486 }
2487
2488 #[test]
2489 fn parse_iso8601_unix_out_of_range_offset_returns_none() {
2490 assert_eq!(parse_iso8601_unix("2026-07-09T15:55:00+24:00"), None);
2492 assert_eq!(parse_iso8601_unix("2026-07-09T15:55:00+2400"), None);
2493 assert_eq!(parse_iso8601_unix("2026-07-09T15:55:00+01:60"), None);
2495 assert_eq!(parse_iso8601_unix("2026-07-09T15:55:00+0160"), None);
2496 }
2497
2498 #[test]
2499 fn parse_iso8601_unix_max_valid_offset_boundary_is_accepted() {
2500 assert!(parse_iso8601_unix("2026-07-09T15:55:00+23:59").is_some());
2502 assert!(parse_iso8601_unix("2026-07-09T15:55:00-23:59").is_some());
2503 assert!(parse_iso8601_unix("2026-07-09T15:55:00+2359").is_some());
2504 }
2505
2506 #[test]
2507 fn exact_timestamp_converts_future_offset_without_relative_form() {
2508 let out = exact_timestamp("2025-05-23T16:08:00-02:00").unwrap();
2509 assert_eq!(out.0, "2025-05-23T18:08:00Z");
2510 assert!(out.1 > NOW);
2511 }
2512
2513 #[test]
2514 fn exact_timestamp_converts_past_offset_and_retains_fraction() {
2515 let out = exact_timestamp("2025-05-23T20:05:00.123+04:00").unwrap();
2516 assert_eq!(out.0, "2025-05-23T16:05:00.123Z");
2517 assert_eq!(out.1, NOW - 180);
2518 }
2519}
2520
2521#[cfg(test)]
2522mod stream_presentation_tests {
2523 use super::*;
2524 use serde_json::json;
2525
2526 #[test]
2527 fn stream_agent_json_preserves_opaque_record_and_empty_page() {
2528 let record = json!([{"id": "aabbccdd-1234-4321-1234-abcdefabcdef", "score": 0.123456789, "created_at": "2026-09-08T00:00:00.123456Z", "empty": [], "null": null, "namespace": "local", "properties": {"namespace": "local"}}]);
2529 for payload in [record, Value::Null, json!([]), json!({}), json!("")] {
2530 let page = json!({"entries": [{"seq": 1, "id": "aabbccdd-1234-4321-1234-abcdefabcdef", "created_at": "2026-09-08T00:00:00.123456Z", "record": payload}], "head_seq": 1, "next_after": null});
2531 let presented = present(page, PresentationMode::Agent, 0);
2532 let out = prepare_format_value(presented, OutputFormat::Json, PresentationMode::Agent);
2533 assert_eq!(out["entries"][0].get("record"), Some(&payload));
2534 assert!(out.get("next_after").is_some_and(Value::is_null));
2535 }
2536 let empty = json!({"entries": [], "head_seq": 0, "next_after": null});
2537 assert_eq!(present(empty.clone(), PresentationMode::Agent, 0), empty);
2538 }
2539}
2540
2541#[cfg(test)]
2542mod issue_2537_standard_tests {
2543 use super::*;
2544 use serde_json::json;
2545
2546 #[test]
2547 fn issue_2537_standard_context_baseline_controls() {
2548 let timestamp = "2026-01-01T00:00:00.123456Z";
2549 let now = 1_767_225_780; let uuid = "aabbccdd-1234-4321-1234-abcdefabcdef";
2551 let payload = json!({"id":uuid,"created_at":timestamp,"empty":[],"null":null});
2552 let value = json!({"tool":"stream.batch","policy":"StreamBatchReceipts","id":uuid,
2553 "updated_at":timestamp,"score":0.123456,"empty":[],"null":null,
2554 "results":[{"id":uuid,"version":1,"updated_at":timestamp,"details":{"updated_at":timestamp}}],
2555 "properties":{"created_at":timestamp,"id":uuid,"empty":""},"trigger_at":timestamp,"due":timestamp,
2556 "entry":{"seq":1,"id":uuid,"created_at":timestamp,"record":payload},
2557 "cursor":{"head_seq":0,"entries":[],"next_after":null}});
2558 let shown = present(value.clone(), PresentationMode::Agent, now);
2559 assert_eq!(shown["updated_at"], timestamp);
2560 assert_eq!(shown["results"][0]["updated_at"], timestamp);
2561 assert_eq!(shown["results"][0]["details"]["updated_at"], timestamp);
2562 assert_eq!(shown["id"], "aabbccdd");
2563 assert_eq!(shown["score"], json!(0.123));
2564 assert!(shown.get("empty").is_none());
2565 assert!(shown.get("null").is_none());
2566 assert_eq!(
2567 shown["properties"],
2568 json!({"created_at":timestamp,"id":"aabbccdd","empty":""})
2569 );
2570 assert_eq!(shown["trigger_at"], timestamp);
2571 assert_eq!(shown["due"], timestamp);
2572 assert_eq!(shown["entry"]["record"], payload);
2573 assert_eq!(shown["entry"]["created_at"], timestamp);
2574 assert_eq!(shown["cursor"], value["cursor"]);
2575 for mode in [PresentationMode::Verbose, PresentationMode::Human] {
2576 assert_eq!(present(value.clone(), mode, now), value);
2577 }
2578 assert_eq!(
2579 present(
2580 json!({"properties":timestamp}),
2581 PresentationMode::Agent,
2582 now
2583 )["properties"],
2584 timestamp
2585 );
2586 assert_eq!(
2587 present(
2588 json!({"properties":[timestamp,{"created_at":timestamp}]}),
2589 PresentationMode::Agent,
2590 now
2591 )["properties"],
2592 json!([timestamp,{"created_at":timestamp}])
2593 );
2594 }
2595}
2596
2597#[cfg(test)]
2598mod issue_2537_receipt_policy_tests {
2599 use super::*;
2600 use serde_json::json;
2601
2602 const TIMESTAMP: &str = "2026-01-01T00:00:00.123456Z";
2603 const NOW: i64 = 1_767_225_780;
2604 const UUID: &str = "aabbccdd-1234-4321-1234-abcdefabcdef";
2605
2606 #[test]
2607 fn issue_2537_receipt_policies_preserve_only_the_named_fields() {
2608 let append = json!({
2609 "id": UUID, "created_at": TIMESTAMP, "updated_at": TIMESTAMP,
2610 "score": 0.123456, "empty": [], "null": null,
2611 "details": {"created_at": TIMESTAMP},
2612 "ticker": {"last_tick_at": TIMESTAMP},
2613 });
2614 let batch = json!({
2615 "id": UUID, "created_at": TIMESTAMP, "updated_at": TIMESTAMP,
2616 "results": [{
2617 "id": UUID, "created_at": TIMESTAMP, "updated_at": TIMESTAMP,
2618 "details": {"created_at": TIMESTAMP, "updated_at": TIMESTAMP},
2619 "record": {"updated_at": TIMESTAMP},
2620 "score": 0.123456, "empty": [], "null": null,
2621 }],
2622 "properties": {"created_at": TIMESTAMP, "id": UUID, "empty": ""},
2623 "entry": {"seq": 1, "id": UUID, "created_at": TIMESTAMP,
2624 "record": {"id": UUID, "updated_at": TIMESTAMP, "empty": [], "null": null}},
2625 "trigger_at": TIMESTAMP, "due": TIMESTAMP,
2626 "ticker": {"last_tick_at": TIMESTAMP},
2627 });
2628 for (value, policy) in [
2629 (append, VerbPresentationPolicy::StreamAppendReceipt),
2630 (batch, VerbPresentationPolicy::StreamBatchReceipts),
2631 ] {
2632 let mut expected = present(value.clone(), PresentationMode::Agent, NOW);
2633 assert_eq!(expected["created_at"], TIMESTAMP);
2634 assert_eq!(expected["updated_at"], TIMESTAMP);
2635 assert_eq!(expected["id"], "aabbccdd");
2636 assert_eq!(expected["ticker"]["last_tick_at"], TIMESTAMP);
2637 if policy == VerbPresentationPolicy::StreamAppendReceipt {
2638 expected["created_at"] = json!(TIMESTAMP);
2639 } else {
2640 expected["results"][0]["created_at"] = json!(TIMESTAMP);
2641 expected["results"][0]["updated_at"] = json!(TIMESTAMP);
2642 expected["results"][0]
2643 .as_object_mut()
2644 .unwrap()
2645 .remove("created_at_relative");
2646 }
2647 assert_eq!(
2648 present_with_policy(value.clone(), PresentationMode::Agent, NOW, policy),
2649 expected
2650 );
2651 for mode in [PresentationMode::Verbose, PresentationMode::Human] {
2652 assert_eq!(present_with_policy(value.clone(), mode, NOW, policy), value);
2653 }
2654 assert_eq!(
2655 present_with_policy(
2656 value.clone(),
2657 PresentationMode::Agent,
2658 NOW,
2659 VerbPresentationPolicy::AlwaysVerbose
2660 ),
2661 value
2662 );
2663 }
2664 }
2665
2666 #[test]
2667 fn issue_2537_receipt_paths_do_not_match_other_containers_or_descendants() {
2668 let row = json!({"created_at": TIMESTAMP, "updated_at": TIMESTAMP});
2669 for (policy, cases) in [
2670 (
2671 VerbPresentationPolicy::StreamAppendReceipt,
2672 vec![
2673 json!([row.clone()]),
2674 json!({"details": row.clone()}),
2675 json!({"results": [row.clone()]}),
2676 json!({"created_at": {"created_at": TIMESTAMP}}),
2677 ],
2678 ),
2679 (
2680 VerbPresentationPolicy::StreamBatchReceipts,
2681 vec![
2682 json!([{"results": [row.clone()]}]),
2683 json!({"results": row.clone()}),
2684 json!({"results": [[row.clone()]]}),
2685 json!({"details": {"results": [row.clone()]}}),
2686 json!({"results": [{"details": row.clone(), "record": row.clone(), "results": [row.clone()]}]}),
2687 json!({"results": [{"created_at": {"updated_at": TIMESTAMP}, "updated_at": null}]}),
2688 ],
2689 ),
2690 ] {
2691 for value in cases {
2692 assert_eq!(
2693 present_with_policy(value.clone(), PresentationMode::Agent, NOW, policy),
2694 present(value.clone(), PresentationMode::Agent, NOW),
2695 "container must not acquire receipt protection: {value}"
2696 );
2697 }
2698 }
2699 }
2700
2701 #[test]
2702 fn issue_2537_receipt_strings_are_preserved_without_parsing() {
2703 for text in ["", "not a timestamp", "2026-01-01T05:30:00.123456+05:30"] {
2704 let append = json!({"created_at": text});
2705 assert_eq!(
2706 present_with_policy(
2707 append.clone(),
2708 PresentationMode::Agent,
2709 NOW,
2710 VerbPresentationPolicy::StreamAppendReceipt
2711 ),
2712 append
2713 );
2714 let batch = json!({"results": [{"created_at": text, "updated_at": text}]});
2715 assert_eq!(
2716 present_with_policy(
2717 batch.clone(),
2718 PresentationMode::Agent,
2719 NOW,
2720 VerbPresentationPolicy::StreamBatchReceipts
2721 ),
2722 batch
2723 );
2724 }
2725 for non_string in [Value::Null, json!([]), json!({}), json!(42), json!(true)] {
2726 for (value, policy) in [
2727 (
2728 json!({"created_at": non_string}),
2729 VerbPresentationPolicy::StreamAppendReceipt,
2730 ),
2731 (
2732 json!({"results": [{"created_at": non_string, "updated_at": non_string}]}),
2733 VerbPresentationPolicy::StreamBatchReceipts,
2734 ),
2735 ] {
2736 assert_eq!(
2737 present_with_policy(value.clone(), PresentationMode::Agent, NOW, policy),
2738 present(value, PresentationMode::Agent, NOW)
2739 );
2740 }
2741 }
2742 }
2743}
2744
2745#[cfg(test)]
2746mod parsed_note_content_tests {
2747 use super::*;
2748 use serde_json::json;
2749
2750 #[test]
2751 fn issue2757_content_is_opaque_to_metadata_and_format_reductions() {
2752 let payload = json!([
2753 {"id":"11111111-1111-4111-8111-111111111111", "full_id":"keep", "namespace":"local",
2754 "created_at":"2026-09-15T12:34:56.123456Z", "score":0.123456789,
2755 "nil":null, "empty":"", "array":[], "object":{},
2756 "properties":{"id":"11111111-1111-4111-8111-111111111111"}},
2757 {"status":"pending"}
2758 ]);
2759 for (scope, response, pointer) in [
2760 (
2761 NoteContentScope::Record,
2762 json!({"id":"22222222-2222-4222-8222-222222222222", "content":payload}),
2763 "/content",
2764 ),
2765 (
2766 NoteContentScope::Items,
2767 json!({"items":[{"id":"22222222-2222-4222-8222-222222222222", "content":payload}], "requested_limit":1, "effective_limit":1, "limit_clamped":false}),
2768 "/items/0/content",
2769 ),
2770 (
2771 NoteContentScope::Notes,
2772 json!({"notes":[{"id":"22222222-2222-4222-8222-222222222222", "content":payload}], "next_after":null}),
2773 "/notes/0/content",
2774 ),
2775 ] {
2776 let presented =
2777 scope.protect(response, |value| present(value, PresentationMode::Agent, 0));
2778 for format in [OutputFormat::Json, OutputFormat::Auto, OutputFormat::Table] {
2779 let rendered = render_format_with_note_content(
2780 presented.clone(),
2781 format,
2782 PresentationMode::Agent,
2783 scope,
2784 );
2785 let actual: Value = serde_json::from_str(&rendered)
2786 .expect("single note stays a JSON record, never a table of its body");
2787 let expected = if format == OutputFormat::Table {
2788 Value::String(payload.to_string())
2789 } else {
2790 payload.clone()
2791 };
2792 assert_eq!(actual.pointer(pointer), Some(&expected));
2793 }
2794 }
2795 }
2796}
2797
2798#[cfg(test)]
2799mod adr045_amendment9_tests {
2800 use super::*;
2801 use serde_json::json;
2802
2803 const NOW: i64 = 1_748_016_480; #[test]
2806 fn agent_exact_utc_preserves_fraction_digits_and_instant() {
2807 for (utc_clock, plus_clock, minus_clock) in [
2808 (
2809 "2025-05-23T16:05:00",
2810 "2025-05-23T20:05:00",
2811 "2025-05-23T12:05:00",
2812 ),
2813 (
2814 "2020-01-01T10:30:45",
2815 "2020-01-01T14:30:45",
2816 "2020-01-01T06:30:45",
2817 ),
2818 ] {
2819 for fraction in ["", ".123", ".123456"] {
2820 let expected = format!("{utc_clock}{fraction}Z");
2821 for (clock, offset) in [
2822 (utc_clock, "Z"),
2823 (utc_clock, "+00:00"),
2824 (plus_clock, "+0400"),
2825 (minus_clock, "-04:00"),
2826 ] {
2827 let input = format!("{clock}{fraction}{offset}");
2828 let shown = present(
2829 json!({"updated_at": input.clone()}),
2830 PresentationMode::Agent,
2831 NOW,
2832 );
2833 assert_eq!(shown["updated_at"], expected, "{input}");
2834 assert_eq!(
2835 parse_iso8601_unix(&input),
2836 parse_iso8601_unix(&expected),
2837 "{input}"
2838 );
2839 }
2840 }
2841 }
2842 }
2843
2844 #[test]
2845 fn agent_preserves_timestamp_prefix_body() {
2846 let body = "2025-05-23T16:05:00Z followed by ordinary message text";
2847 let shown = present(json!({"content": body}), PresentationMode::Agent, NOW);
2848 assert_eq!(shown["content"], body);
2849 }
2850
2851 #[test]
2852 fn agent_preserves_offsetless_timestamp() {
2853 let body = "2025-05-23T16:05:00";
2854 let shown = present(json!({"content": body}), PresentationMode::Agent, NOW);
2855 assert_eq!(shown["content"], body);
2856 }
2857
2858 #[test]
2859 fn agent_preserves_malformed_offset() {
2860 let body = "2025-05-23T16:05:00+25:00";
2861 let shown = present(json!({"content": body}), PresentationMode::Agent, NOW);
2862 assert_eq!(shown["content"], body);
2863 }
2864
2865 #[test]
2866 fn agent_list_rows_pair_exact_and_relative_time_in_each_band() {
2867 let bands = [
2868 (42, "42s ago"),
2869 (180, "3m ago"),
2870 (3 * 3600, "3h ago"),
2871 (2 * 86400, "2d ago"),
2872 ];
2873 let rows: Vec<Value> = bands
2874 .into_iter()
2875 .map(|(age, _)| {
2876 let created = chrono::DateTime::<chrono::Utc>::from_timestamp(NOW - age, 0)
2877 .unwrap()
2878 .to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
2879 json!({"created_at": created, "updated_at": created})
2880 })
2881 .collect();
2882 let shown = present(json!({"items": rows.clone()}), PresentationMode::Agent, NOW);
2883 for (index, (_, relative)) in bands.into_iter().enumerate() {
2884 assert_eq!(
2885 shown["items"][index]["created_at"],
2886 rows[index]["created_at"]
2887 );
2888 assert_eq!(
2889 shown["items"][index]["updated_at"],
2890 rows[index]["updated_at"]
2891 );
2892 assert_eq!(shown["items"][index]["created_at_relative"], relative);
2893 assert!(shown["items"][index].get("updated_at_relative").is_none());
2894 }
2895 }
2896
2897 #[test]
2898 fn agent_relative_sibling_obeys_array_properties_existing_and_future_bounds() {
2899 let recent = "2025-05-23T16:05:00+00:00";
2900 let future = "2025-05-23T16:08:10Z";
2901 let shown = present(
2902 json!({
2903 "created_at": recent,
2904 "items": [
2905 {"created_at": future},
2906 {"created_at": recent, "created_at_relative": "canonical"},
2907 {"created_at": "2025-05-23T16:05:00"},
2908 {"updated_at": recent},
2909 {"created_at": "2025-05-23T16:08:00.000001Z"},
2910 {"created_at": "2025-05-23T16:07:59.999999Z"},
2911 {"created_at": "2025-05-23T16:08:00.000000Z"},
2912 {"properties": [{"created_at": recent, "empty": ""}]}
2913 ],
2914 "properties": {"nested": [{"created_at": recent}]}
2915 }),
2916 PresentationMode::Agent,
2917 NOW,
2918 );
2919 assert_eq!(shown["created_at"], "2025-05-23T16:05:00Z");
2920 assert!(shown.get("created_at_relative").is_none());
2921 assert_eq!(shown["items"][0]["created_at"], future);
2922 assert!(shown["items"][0].get("created_at_relative").is_none());
2923 assert_eq!(shown["items"][1]["created_at_relative"], "canonical");
2924 assert_eq!(shown["items"][2]["created_at"], "2025-05-23T16:05:00");
2925 assert!(shown["items"][2].get("created_at_relative").is_none());
2926 assert!(shown["items"][3].get("created_at_relative").is_none());
2927 assert!(shown["items"][4].get("created_at_relative").is_none());
2928 assert_eq!(shown["items"][5]["created_at_relative"], "0s ago");
2929 assert_eq!(shown["items"][6]["created_at_relative"], "0s ago");
2930 assert_eq!(shown["items"][7]["properties"][0]["created_at"], recent);
2931 assert!(shown["items"][7]["properties"][0]
2932 .get("created_at_relative")
2933 .is_none());
2934 assert!(shown["items"][7]["properties"][0].get("empty").is_none());
2935 assert_eq!(shown["properties"]["nested"][0]["created_at"], recent);
2936 assert!(shown["properties"]["nested"][0]
2937 .get("created_at_relative")
2938 .is_none());
2939 }
2940
2941 #[test]
2942 fn agent_relative_sibling_uses_sampled_fraction_within_same_second() {
2943 let sampled = chrono::DateTime::parse_from_rfc3339("2025-05-23T16:08:00.500000000Z")
2944 .unwrap()
2945 .with_timezone(&chrono::Utc);
2946 let shown = present_with_policy_at(
2947 json!({"items": [
2948 {"created_at": "2025-05-23T16:08:00.499999999Z"},
2949 {"created_at": "2025-05-23T16:08:00.500000000Z"},
2950 {"created_at": "2025-05-23T16:08:00.5000000000Z"},
2951 {"created_at": "2025-05-23T16:08:00.5000000001Z"},
2952 {"created_at": "2025-05-23T16:08:00.500000001Z"},
2953 {"created_at": "2025-05-23T16:07:59.500000001Z"},
2954 {"created_at": "2025-05-23T16:07:59.500000000Z"}
2955 ]}),
2956 PresentationMode::Agent,
2957 sampled.into(),
2958 VerbPresentationPolicy::Standard,
2959 );
2960 for index in [0, 1, 2, 5] {
2961 assert_eq!(shown["items"][index]["created_at_relative"], "0s ago");
2962 }
2963 for index in [3, 4] {
2964 assert!(shown["items"][index].get("created_at_relative").is_none());
2965 }
2966 assert_eq!(shown["items"][6]["created_at_relative"], "1s ago");
2967 }
2968}