1use std::collections::{BTreeMap, BTreeSet};
2use std::time::{SystemTime, UNIX_EPOCH};
3
4use kmp_domain::{
5 DECLARED_FROM_RELATE_METHOD, INTENDED_NEW_LABEL_METADATA_KEY, MemoryDimensionIdentity,
6 MemoryRelationType, RelationSemanticClass, SearchSummary, SearchSummaryFault, SourceKind,
7 label_resemblances,
8};
9
10use crate::ApplicationError;
11use crate::commands::{UpdateContextChange, UpdateContextCommand};
12use crate::memory::{
13 LabelPolicy, MemoryAcceptedCounts, MemoryCoordinateData, MemoryData, MemoryDimensionData,
14 MemoryIngestCommand, MemoryIngestOutcome, MemoryRelationData, ResemblingLabelData,
15};
16
17use super::ref_boundary::{
18 validate_ref_token, validate_supplied_entry_ref, validate_supplied_evidence_ref,
19 validate_supplied_member_ref,
20};
21
22#[derive(Debug, Clone, Default, PartialEq, Eq)]
23pub struct ExistingMemoryRefs {
24 pub refs: BTreeSet<String>,
25 pub dimensions: BTreeSet<String>,
26 pub labels: BTreeSet<(String, String)>,
29 pub foreign: BTreeSet<String>,
33 pub max_sequences: BTreeMap<(String, String), u32>,
36}
37
38pub fn translate_memory_ingest(
39 command: &MemoryIngestCommand,
40 existing: &ExistingMemoryRefs,
41) -> Result<(UpdateContextCommand, MemoryIngestOutcome), ApplicationError> {
42 validate_command(command)?;
43 let ingested_at = kernel_ingested_at();
44 let memory = namespaced_memory(&command.about, &command.memory, existing, &ingested_at)?;
45 let created_dimensions = memory
49 .dimensions
50 .iter()
51 .map(|dimension| dimension.id.clone())
52 .filter(|id| !existing.dimensions.contains(id))
53 .collect::<Vec<_>>();
54 let resembling_labels = resembling_labels(&command.about, &command.memory, existing)?;
59 if command.label_policy == LabelPolicy::Refuse && !resembling_labels.is_empty() {
60 return Err(ApplicationError::Validation(format!(
61 "labels resemble ones the about already holds: {}. Reuse the existing label, or set the dimension metadata `{}: \"true\"` to insist on the new one",
62 resembling_labels
63 .iter()
64 .map(|label| label.why.clone())
65 .collect::<Vec<_>>()
66 .join(" "),
67 INTENDED_NEW_LABEL_METADATA_KEY
68 )));
69 }
70 let mut warnings = search_summary_warnings(&command.memory);
71 warnings.extend(resembling_labels.iter().map(|label| label.why.clone()));
72
73 let changes = memory_changes(&memory)?;
74 let outcome = MemoryIngestOutcome {
75 about: command.about.clone(),
76 memory_id: memory_id_from_idempotency_key(&command.idempotency_key),
77 accepted: MemoryAcceptedCounts {
78 entries: command.memory.entries.len(),
79 relations: command.memory.relations.len(),
80 evidence: command.memory.evidence.len(),
81 },
82 read_after_write_ready: false,
83 warnings,
84 created_dimensions,
85 resembling_labels,
86 };
87
88 Ok((
89 UpdateContextCommand {
90 root_node_id: command.about.clone(),
91 role: "memory".to_string(),
92 work_item_id: command.idempotency_key.clone(),
93 changes,
94 expected_revision: None,
95 expected_content_hash: None,
96 idempotency_key: Some(command.idempotency_key.clone()),
97 logical_digest: Some(logical_digest(command)),
98 requested_by: command
99 .provenance
100 .as_ref()
101 .map(|provenance| provenance.source_agent.clone()),
102 },
103 outcome,
104 ))
105}
106
107fn resembling_labels(
112 about: &str,
113 memory: &MemoryData,
114 existing: &ExistingMemoryRefs,
115) -> Result<Vec<ResemblingLabelData>, ApplicationError> {
116 let catalogue = existing
117 .labels
118 .iter()
119 .map(|(kind, value)| (kind.as_str(), value.as_str()))
120 .collect::<Vec<_>>();
121 let mut found = Vec::new();
122 for dimension in &memory.dimensions {
123 let identity = dimension_identity(about, &dimension.id)?;
124 if existing.dimensions.contains(&identity.node_id()) {
125 continue;
126 }
127 if dimension
128 .metadata
129 .get(INTENDED_NEW_LABEL_METADATA_KEY)
130 .is_some_and(|value| value == "true")
131 {
132 continue;
133 }
134 for resemblance in label_resemblances(
135 &dimension.kind,
136 identity.dimension_id(),
137 catalogue.iter().copied(),
138 ) {
139 found.push(ResemblingLabelData {
140 key: resemblance.key().to_string(),
141 value: resemblance.value().to_string(),
142 existing_key: resemblance.existing_key().to_string(),
143 existing_value: resemblance.existing_value().to_string(),
144 kind: resemblance.kind().name().to_string(),
145 why: resemblance.why(),
146 });
147 }
148 }
149 Ok(found)
150}
151
152fn validate_command(command: &MemoryIngestCommand) -> Result<(), ApplicationError> {
153 require_non_empty(&command.about, "about")?;
154 validate_ref_token("about", &command.about).map_err(ApplicationError::Validation)?;
155 require_non_empty(&command.idempotency_key, "idempotency_key")?;
156 if let Some(provenance) = command.provenance.as_ref() {
157 SourceKind::parse(&provenance.source_kind).map_err(|error| {
158 ApplicationError::Validation(format!(
159 "memory provenance source_kind is invalid: {error}"
160 ))
161 })?;
162 require_non_empty(&provenance.source_agent, "provenance.source_agent")?;
163 require_non_empty(&provenance.observed_at, "provenance.observed_at")?;
164 }
165
166 Ok(())
167}
168
169fn namespaced_memory(
170 about: &str,
171 memory: &MemoryData,
172 existing: &ExistingMemoryRefs,
173 ingested_at: &str,
174) -> Result<MemoryData, ApplicationError> {
175 if memory.dimensions.is_empty() && existing.dimensions.is_empty() {
176 return Err(ApplicationError::Validation(
177 "memory.dimensions must not be empty when no existing memory dimensions are available"
178 .to_string(),
179 ));
180 }
181 if memory.entries.is_empty() {
182 return Err(ApplicationError::Validation(
183 "memory.entries must not be empty".to_string(),
184 ));
185 }
186
187 let mut known_refs = existing.refs.clone();
188 known_refs.extend(existing.dimensions.iter().cloned());
189 known_refs.insert(about.to_string());
197 let mut dimension_ids = existing.dimensions.clone();
198 let mut dimension_aliases = existing_dimension_aliases(about, existing);
199 let mut declared_dimension_kinds = BTreeMap::new();
200 let mut declared_dimension_refs = BTreeSet::new();
201 let mut max_sequences = existing.max_sequences.clone();
202 let mut dimensions = Vec::new();
203 for dimension in &memory.dimensions {
204 require_non_empty(&dimension.id, "memory.dimensions[].id")?;
205 validate_ref_token("memory.dimensions[].id", &dimension.id)
206 .map_err(ApplicationError::Validation)?;
207 require_non_empty(&dimension.kind, "memory.dimensions[].kind")?;
208 let dimension_identity = dimension_identity(about, &dimension.id)?;
209 let dimension_ref = dimension_identity.node_id();
210 declared_dimension_kinds.insert(dimension_ref.clone(), dimension.kind.clone());
211 insert_unique(
212 &mut declared_dimension_refs,
213 &dimension_ref,
214 "memory dimension",
215 )?;
216 if existing.dimensions.contains(&dimension_ref) {
217 dimension_aliases
218 .entry(dimension.id.clone())
219 .or_insert_with(|| dimension_ref.clone());
220 known_refs.insert(dimension_ref);
221 continue;
222 }
223 insert_unique(&mut dimension_ids, &dimension_ref, "memory dimension")?;
224 if dimension_aliases
225 .insert(dimension.id.clone(), dimension_ref.clone())
226 .is_some()
227 {
228 return Err(ApplicationError::Validation(format!(
229 "duplicate memory dimension `{}`",
230 dimension.id
231 )));
232 }
233 known_refs.insert(dimension_ref.clone());
234
235 let mut metadata = dimension.metadata.clone();
236 metadata.remove(INTENDED_NEW_LABEL_METADATA_KEY);
238 metadata
239 .entry("memory_about".to_string())
240 .or_insert_with(|| about.to_string());
241 metadata
242 .entry("memory_dimension_id".to_string())
243 .or_insert_with(|| dimension.id.clone());
244 dimensions.push(MemoryDimensionData {
245 id: dimension_ref,
246 kind: dimension.kind.clone(),
247 title: dimension.title.clone(),
248 metadata,
249 });
250 }
251
252 let mut entry_ids = BTreeSet::new();
253 let mut entries = Vec::new();
254 for entry in &memory.entries {
255 require_non_empty(&entry.id, "memory.entries[].id")?;
256 validate_supplied_entry_ref(about, "memory.entries[].id", &entry.id)
257 .map_err(ApplicationError::Validation)?;
258 require_non_empty(&entry.kind, "memory.entries[].kind")?;
259 require_non_empty(&entry.text, "memory.entries[].text")?;
260 if entry.coordinates.is_empty() {
261 return Err(ApplicationError::Validation(format!(
262 "memory entry `{}` must include at least one coordinate",
263 entry.id
264 )));
265 }
266 insert_unique(&mut entry_ids, &entry.id, "memory entry")?;
267 known_refs.insert(entry.id.clone());
268
269 let mut coordinates = Vec::new();
270 for coordinate in &entry.coordinates {
271 let mut coordinate = normalize_coordinate(
272 coordinate,
273 "memory.entries[].coordinates[]",
274 "memory entry",
275 &dimension_aliases,
276 &dimension_ids,
277 &declared_dimension_kinds,
278 )?;
279 coordinate
280 .ingested_at
281 .get_or_insert_with(|| ingested_at.to_string());
282 let sequence_key = (coordinate.dimension.clone(), coordinate.scope_id.clone());
283 let frontier = max_sequences.entry(sequence_key).or_default();
284 match coordinate.sequence {
285 Some(sequence) => *frontier = (*frontier).max(sequence),
286 None => {
287 *frontier = frontier.checked_add(1).ok_or_else(|| {
288 ApplicationError::Validation(
289 "memory coordinate sequence space is exhausted".to_string(),
290 )
291 })?;
292 coordinate.sequence = Some(*frontier);
293 }
294 }
295 coordinates.push(coordinate);
296 }
297 let mut entry = entry.clone();
298 entry.coordinates = coordinates;
299 entries.push(entry);
300 }
301
302 let mut relations = Vec::new();
303 for relation in &memory.relations {
304 require_non_empty(&relation.source_ref, "memory.relations[].source_ref")?;
305 require_non_empty(&relation.target_ref, "memory.relations[].target_ref")?;
306 require_non_empty(&relation.rel, "memory.relations[].rel")?;
307 let relation_type = MemoryRelationType::new(&relation.rel).map_err(|error| {
308 ApplicationError::Validation(format!("memory relation type is invalid: {error}"))
309 })?;
310 let semantic_class =
311 RelationSemanticClass::parse(&relation.semantic_class).map_err(|error| {
312 ApplicationError::Validation(format!("memory relation class is invalid: {error}"))
313 })?;
314 let source_ref = normalize_ref(&relation.source_ref, &dimension_aliases);
315 let target_ref = normalize_ref(&relation.target_ref, &dimension_aliases);
316 validate_supplied_member_ref(about, "memory.relations[].from", &source_ref)
317 .map_err(ApplicationError::Validation)?;
318 let crosses_abouts = crosses_abouts(about, relation, &relation_type, &target_ref);
323 if crosses_abouts {
324 validate_ref_token("memory.relations[].to", &target_ref)
325 .map_err(ApplicationError::Validation)?;
326 if !existing.foreign.contains(&target_ref) {
327 return Err(ApplicationError::Validation(format!(
328 "memory relation `{}` -> `{}` declares an equivalence with a ref no about holds",
329 relation.source_ref, relation.target_ref
330 )));
331 }
332 } else {
333 validate_supplied_member_ref(about, "memory.relations[].to", &target_ref)
334 .map_err(ApplicationError::Validation)?;
335 }
336 if !known_refs.contains(&source_ref)
337 || (!crosses_abouts && !known_refs.contains(&target_ref))
338 {
339 return Err(ApplicationError::Validation(format!(
340 "memory relation `{}` -> `{}` references unknown refs",
341 relation.source_ref, relation.target_ref
342 )));
343 }
344 if semantic_class != RelationSemanticClass::Structural {
345 if relation
346 .confidence
347 .as_deref()
348 .unwrap_or("")
349 .trim()
350 .is_empty()
351 {
352 return Err(ApplicationError::Validation(
353 "non-structural memory relations require confidence".to_string(),
354 ));
355 }
356 if relation.why.as_deref().unwrap_or("").trim().is_empty()
357 && relation.evidence.as_deref().unwrap_or("").trim().is_empty()
358 {
359 return Err(ApplicationError::Validation(
360 "non-structural memory relations require why or evidence".to_string(),
361 ));
362 }
363 }
364 validate_positive_optional(relation.sequence, "memory.relations[].sequence")?;
365 let coordinate = relation
366 .coordinate
367 .as_ref()
368 .map(|coordinate| {
369 normalize_coordinate(
370 coordinate,
371 "memory.relations[].coordinate",
372 "memory relation",
373 &dimension_aliases,
374 &dimension_ids,
375 &declared_dimension_kinds,
376 )
377 })
378 .transpose()?
379 .map(|mut coordinate| {
380 coordinate
381 .ingested_at
382 .get_or_insert_with(|| ingested_at.to_string());
383 coordinate
384 });
385 let mut relation = relation.clone();
386 relation.source_ref = source_ref;
387 relation.target_ref = target_ref;
388 relation.decision_id = normalize_optional_member_ref(
389 about,
390 "memory.relations[].decision_id",
391 relation.decision_id.as_deref(),
392 &dimension_aliases,
393 )?;
394 relation.caused_by_node_id = normalize_optional_member_ref(
395 about,
396 "memory.relations[].caused_by_node_id",
397 relation.caused_by_node_id.as_deref(),
398 &dimension_aliases,
399 )?;
400 relation.rel = relation_type.as_str().to_string();
401 relation.coordinate = coordinate;
402 relations.push(relation);
403 }
404
405 let mut evidence_ids = BTreeSet::new();
406 let mut evidence_items = Vec::new();
407 for evidence in &memory.evidence {
408 require_non_empty(&evidence.id, "memory.evidence[].id")?;
409 validate_supplied_evidence_ref(about, "memory.evidence[].id", &evidence.id)
410 .map_err(ApplicationError::Validation)?;
411 require_non_empty(&evidence.text, "memory.evidence[].text")?;
412 insert_unique(&mut evidence_ids, &evidence.id, "memory evidence")?;
413 known_refs.insert(evidence.id.clone());
414 let mut supports = Vec::new();
415 for supported in &evidence.supports {
416 require_non_empty(supported, "memory.evidence[].supports[]")?;
417 let supported_ref = normalize_ref(supported, &dimension_aliases);
418 validate_supplied_member_ref(about, "memory.evidence[].supports[]", &supported_ref)
419 .map_err(ApplicationError::Validation)?;
420 if !known_refs.contains(&supported_ref) {
421 return Err(ApplicationError::Validation(format!(
422 "memory evidence `{}` supports unknown ref `{supported}`",
423 evidence.id
424 )));
425 }
426 supports.push(supported_ref);
427 }
428 let mut evidence = evidence.clone();
429 evidence.supports = supports;
430 evidence_items.push(evidence);
431 }
432
433 Ok(MemoryData {
434 dimensions,
435 entries,
436 relations,
437 evidence: evidence_items,
438 })
439}
440
441fn search_summary_warnings(memory: &MemoryData) -> Vec<String> {
448 memory
449 .entries
450 .iter()
451 .filter_map(|entry| {
452 let summary = entry.metadata.get(SearchSummary::METADATA_KEY)?;
453 SearchSummary::lint(&entry.text, summary)
454 .err()
455 .map(|faults| {
456 format!(
457 "memory entry `{}` carries a {} that will not carry retrieval: {}",
458 entry.id,
459 SearchSummary::METADATA_KEY,
460 SearchSummaryFault::describe(&faults)
461 )
462 })
463 })
464 .collect()
465}
466
467fn kernel_ingested_at() -> String {
472 let since_epoch = SystemTime::now()
473 .duration_since(UNIX_EPOCH)
474 .unwrap_or_default();
475 format!(
476 "unix:{:012}:{:09}",
477 since_epoch.as_secs() + 100_000_000_000,
478 since_epoch.subsec_nanos()
479 )
480}
481
482fn existing_dimension_aliases(
483 about: &str,
484 existing: &ExistingMemoryRefs,
485) -> BTreeMap<String, String> {
486 existing
487 .dimensions
488 .iter()
489 .filter_map(|dimension_ref| {
490 let identity = MemoryDimensionIdentity::parse(dimension_ref)?;
491 (identity.about() == about)
492 .then(|| (identity.dimension_id().to_string(), dimension_ref.clone()))
493 })
494 .collect()
495}
496
497fn dimension_identity(
498 about: &str,
499 dimension_id: &str,
500) -> Result<MemoryDimensionIdentity, ApplicationError> {
501 MemoryDimensionIdentity::resolve(about, dimension_id).ok_or_else(|| {
502 ApplicationError::Validation(format!(
503 "memory dimension `{dimension_id}` belongs to another about; declare it bare or \
504 namespaced for `{about}`"
505 ))
506 })
507}
508
509pub fn crosses_abouts(
513 about: &str,
514 relation: &MemoryRelationData,
515 relation_type: &MemoryRelationType,
516 target_ref: &str,
517) -> bool {
518 relation_type.may_cross_abouts()
519 && validate_supplied_member_ref(about, "memory.relations[].to", target_ref).is_err()
520 && relation.semantic_class.trim() == "evidential"
521 && !relation
522 .why
523 .as_deref()
524 .unwrap_or_default()
525 .trim()
526 .is_empty()
527 && !relation
528 .evidence
529 .as_deref()
530 .unwrap_or_default()
531 .trim()
532 .is_empty()
533 && relation
534 .method
535 .as_deref()
536 .is_some_and(|method| method.starts_with(DECLARED_FROM_RELATE_METHOD))
537}
538
539fn normalize_ref(value: &str, dimension_aliases: &BTreeMap<String, String>) -> String {
540 dimension_aliases
541 .get(value)
542 .cloned()
543 .unwrap_or_else(|| value.to_string())
544}
545
546fn normalize_optional_member_ref(
547 about: &str,
548 path: &str,
549 value: Option<&str>,
550 dimension_aliases: &BTreeMap<String, String>,
551) -> Result<Option<String>, ApplicationError> {
552 value
553 .map(|value| {
554 let normalized = normalize_ref(value, dimension_aliases);
555 validate_supplied_member_ref(about, path, &normalized)
556 .map_err(ApplicationError::Validation)?;
557 Ok(normalized)
558 })
559 .transpose()
560}
561
562fn normalize_coordinate(
563 coordinate: &MemoryCoordinateData,
564 field: &str,
565 label: &str,
566 dimension_aliases: &BTreeMap<String, String>,
567 dimension_ids: &BTreeSet<String>,
568 declared_dimension_kinds: &BTreeMap<String, String>,
569) -> Result<MemoryCoordinateData, ApplicationError> {
570 require_non_empty(&coordinate.dimension, &format!("{field}.dimension"))?;
571 require_non_empty(&coordinate.scope_id, &format!("{field}.scope_id"))?;
572 let scope_id = normalize_ref(&coordinate.scope_id, dimension_aliases);
573 if !dimension_ids.contains(&scope_id) {
574 return Err(ApplicationError::Validation(format!(
575 "{label} coordinate references unknown dimension scope `{}`",
576 coordinate.scope_id
577 )));
578 }
579 if let Some(expected_kind) = declared_dimension_kinds.get(&scope_id)
580 && coordinate.dimension != *expected_kind
581 {
582 return Err(ApplicationError::Validation(format!(
583 "{label} coordinate dimension `{}` does not match declared kind `{expected_kind}` for scope `{}`",
584 coordinate.dimension, coordinate.scope_id
585 )));
586 }
587 validate_positive_optional(coordinate.sequence, &format!("{field}.sequence"))?;
588 validate_positive_optional(coordinate.rank, &format!("{field}.rank"))?;
589
590 let mut coordinate = coordinate.clone();
591 coordinate.scope_id = scope_id;
592 Ok(coordinate)
593}
594
595fn memory_changes(memory: &MemoryData) -> Result<Vec<UpdateContextChange>, ApplicationError> {
596 let mut changes = Vec::new();
597 for dimension in &memory.dimensions {
598 changes.push(change(
599 "memory_dimension",
600 &dimension.id,
601 serde_json::to_string(dimension),
602 "KMP memory dimension ingest",
603 vec![dimension.id.clone()],
604 )?);
605 }
606 for entry in &memory.entries {
607 let scopes = entry
608 .coordinates
609 .iter()
610 .map(|coordinate| coordinate.scope_id.clone())
611 .collect();
612 changes.push(change(
613 "memory_entry",
614 &entry.id,
615 serde_json::to_string(entry),
616 "KMP memory entry ingest",
617 scopes,
618 )?);
619 }
620 for relation in &memory.relations {
621 changes.push(change(
622 "memory_relation",
623 &format!(
624 "relation:{}:{}:{}",
625 relation.source_ref, relation.rel, relation.target_ref
626 ),
627 serde_json::to_string(relation),
628 relation
629 .why
630 .as_deref()
631 .filter(|value| !value.trim().is_empty())
632 .unwrap_or("KMP memory relation ingest"),
633 vec![relation.source_ref.clone(), relation.target_ref.clone()],
634 )?);
635 }
636 for evidence in &memory.evidence {
637 changes.push(change(
638 "memory_evidence",
639 &evidence.id,
640 serde_json::to_string(evidence),
641 evidence
642 .source
643 .as_deref()
644 .filter(|value| !value.trim().is_empty())
645 .unwrap_or("KMP memory evidence ingest"),
646 evidence.supports.clone(),
647 )?);
648 }
649
650 Ok(changes)
651}
652
653fn change(
654 entity_kind: &str,
655 entity_id: &str,
656 payload: Result<String, serde_json::Error>,
657 reason: &str,
658 scopes: Vec<String>,
659) -> Result<UpdateContextChange, ApplicationError> {
660 Ok(UpdateContextChange {
661 operation: "UPSERT".to_string(),
662 entity_kind: entity_kind.to_string(),
663 entity_id: entity_id.to_string(),
664 payload_json: payload.map_err(|error| {
665 ApplicationError::Validation(format!("memory payload could not serialize: {error}"))
666 })?,
667 reason: reason.to_string(),
668 scopes,
669 })
670}
671
672fn require_non_empty(value: &str, field: &str) -> Result<(), ApplicationError> {
673 if value.trim().is_empty() {
674 Err(ApplicationError::Validation(format!(
675 "{field} cannot be empty"
676 )))
677 } else {
678 Ok(())
679 }
680}
681
682fn insert_unique(
683 values: &mut BTreeSet<String>,
684 value: &str,
685 label: &str,
686) -> Result<(), ApplicationError> {
687 if !values.insert(value.to_string()) {
688 Err(ApplicationError::Validation(format!(
689 "duplicate {label} `{value}`"
690 )))
691 } else {
692 Ok(())
693 }
694}
695
696fn validate_positive_optional(value: Option<u32>, field: &str) -> Result<(), ApplicationError> {
697 if value == Some(0) {
698 Err(ApplicationError::Validation(format!(
699 "{field} must be greater than zero when set"
700 )))
701 } else {
702 Ok(())
703 }
704}
705
706fn logical_digest(command: &MemoryIngestCommand) -> String {
713 use sha2::{Digest, Sha256};
714 let mut hasher = Sha256::new();
715 hasher.update(command.about.as_bytes());
716 hasher.update([0]);
717 let memory = serde_json::to_vec(&command.memory)
718 .expect("memory data serializes: it holds only strings, maps and integers");
719 hasher.update(&memory);
720 hasher.update([0]);
721 if let Some(provenance) = &command.provenance {
722 let provenance =
723 serde_json::to_vec(provenance).expect("provenance serializes: it holds only strings");
724 hasher.update(&provenance);
725 }
726 format!("{:x}", hasher.finalize())
727}
728
729fn memory_id_from_idempotency_key(idempotency_key: &str) -> String {
730 idempotency_key
731 .strip_prefix("ingest:")
732 .map(|suffix| format!("memory:{suffix}"))
733 .unwrap_or_else(|| format!("memory:{idempotency_key}"))
734}
735
736#[cfg(test)]
737mod tests {
738 use std::collections::{BTreeMap, BTreeSet};
739
740 use crate::ApplicationError;
741 use crate::memory::{
742 ExistingMemoryRefs, MemoryCoordinateData, MemoryData, MemoryDimensionData, MemoryEntryData,
743 MemoryEvidenceData, MemoryIngestCommand, MemoryRelationData,
744 };
745
746 use super::translate_memory_ingest;
747
748 #[test]
749 fn translate_memory_ingest_creates_internal_memory_update_command() {
750 let command = sample_command();
751
752 let (update, outcome) = translate_memory_ingest(&command, &ExistingMemoryRefs::default())
753 .expect("valid memory should translate");
754
755 assert_eq!(update.root_node_id, "question:830ce83f");
756 assert_eq!(update.role, "memory");
757 assert_eq!(update.idempotency_key.as_deref(), Some("ingest:app-test"));
758 assert_eq!(outcome.memory_id, "memory:app-test");
759 assert_eq!(outcome.accepted.entries, 1);
760 assert_eq!(outcome.accepted.relations, 1);
761 assert_eq!(outcome.accepted.evidence, 1);
762 assert_eq!(
763 update
764 .changes
765 .iter()
766 .map(|change| change.entity_kind.as_str())
767 .collect::<Vec<_>>(),
768 vec![
769 "memory_dimension",
770 "memory_entry",
771 "memory_relation",
772 "memory_evidence"
773 ]
774 );
775 assert_eq!(
776 update.changes[0].entity_id,
777 "about:question:830ce83f:dimension:conversation:rachel-2026-04-12"
778 );
779 assert_eq!(
780 update.changes[1].scopes,
781 ["about:question:830ce83f:dimension:conversation:rachel-2026-04-12"]
782 );
783 assert_eq!(
784 update.changes[2].entity_id,
785 "relation:about:question:830ce83f:dimension:conversation:rachel-2026-04-12:contains_entry:question:830ce83f:claim:rachel-denver"
786 );
787 let entry_payload: serde_json::Value =
788 serde_json::from_str(&update.changes[1].payload_json).expect("entry payload json");
789 assert_eq!(
790 entry_payload["coordinates"][0]["scope_id"],
791 "about:question:830ce83f:dimension:conversation:rachel-2026-04-12"
792 );
793 assert!(
794 entry_payload["coordinates"][0]["ingested_at"]
795 .as_str()
796 .is_some_and(|value| value.starts_with("unix:")),
797 "the kernel must stamp when it learned every coordinate: {entry_payload}"
798 );
799 }
800
801 #[test]
806 fn translate_memory_ingest_warns_about_a_search_summary_that_will_not_carry() {
807 let mut command = sample_command();
808 command.memory.entries[0].text =
809 "Rachel dijo que se mudaba a Denver por el ticket #469.".to_string();
810 command.memory.entries[0].metadata.insert(
811 "summary_en".to_string(),
812 "Rachel said she was moving to Denver.".to_string(),
813 );
814
815 let (_, outcome) = translate_memory_ingest(&command, &ExistingMemoryRefs::default())
816 .expect("a degraded summary is a warning, not a refusal");
817
818 assert_eq!(
819 outcome.warnings,
820 [
821 "memory entry `question:830ce83f:claim:rachel-denver` carries a summary_en that will \
822 not carry retrieval: drops identifiers the text carries: #469"
823 ]
824 );
825 assert_eq!(outcome.accepted.entries, 1);
826 }
827
828 #[test]
829 fn translate_memory_ingest_is_silent_about_a_search_summary_that_carries() {
830 let mut command = sample_command();
831 command.memory.entries[0].text =
832 "Rachel dijo que se mudaba a Denver por el ticket #469.".to_string();
833 command.memory.entries[0].metadata.insert(
834 "summary_en".to_string(),
835 "Rachel said she was moving to Denver because of ticket #469.".to_string(),
836 );
837
838 let (update, outcome) = translate_memory_ingest(&command, &ExistingMemoryRefs::default())
839 .expect("a faithful summary translates");
840
841 assert!(outcome.warnings.is_empty(), "{:?}", outcome.warnings);
842 let entry_payload: serde_json::Value =
843 serde_json::from_str(&update.changes[1].payload_json).expect("entry payload json");
844 assert_eq!(
845 entry_payload["metadata"]["summary_en"],
846 "Rachel said she was moving to Denver because of ticket #469.",
847 "the summary is stored as written, beside the text"
848 );
849 }
850
851 #[test]
852 fn translate_memory_ingest_preserves_a_replayed_ingest_clock() {
853 let mut command = sample_command();
854 command.memory.entries[0].coordinates[0].ingested_at =
855 Some("2026-04-12T15:01:00Z".to_string());
856
857 let (update, _) = translate_memory_ingest(&command, &ExistingMemoryRefs::default())
858 .expect("caller-supplied ingest clock should survive replay");
859 let entry_payload: serde_json::Value =
860 serde_json::from_str(&update.changes[1].payload_json).expect("entry payload json");
861
862 assert_eq!(
863 entry_payload["coordinates"][0]["ingested_at"],
864 "2026-04-12T15:01:00Z"
865 );
866 }
867
868 #[test]
869 fn translate_memory_ingest_accepts_an_already_namespaced_dimension_id() {
870 let namespaced = "about:question:830ce83f:dimension:conversation:rachel-2026-04-12";
874 let mut command = sample_command();
875 command.memory.dimensions[0].id = namespaced.to_string();
876 command.memory.entries[0].coordinates[0].scope_id = namespaced.to_string();
877 command.memory.relations[0].source_ref = namespaced.to_string();
878
879 let (update, _) = translate_memory_ingest(&command, &ExistingMemoryRefs::default())
880 .expect("a namespaced dimension id belongs to this about");
881
882 assert_eq!(update.changes[0].entity_id, namespaced);
883 let entry_payload: serde_json::Value =
884 serde_json::from_str(&update.changes[1].payload_json).expect("entry payload json");
885 assert_eq!(entry_payload["coordinates"][0]["scope_id"], namespaced);
886 }
887
888 #[test]
889 fn translate_memory_ingest_rejects_a_dimension_owned_by_another_about() {
890 let mut command = sample_command();
891 command.memory.dimensions[0].id =
892 "about:question:other:dimension:conversation:rachel-2026-04-12".to_string();
893
894 let error = translate_memory_ingest(&command, &ExistingMemoryRefs::default())
895 .expect_err("a foreign about's dimension is not ours to write");
896
897 assert_validation_contains(error, "belongs to another about");
898 }
899
900 #[test]
901 fn translate_memory_ingest_fails_fast_for_unknown_coordinate_dimension() {
902 let mut command = sample_command();
903 command.memory.entries[0].coordinates[0].scope_id = "conversation:missing".to_string();
904
905 let error = translate_memory_ingest(&command, &ExistingMemoryRefs::default())
906 .expect_err("unknown scope should fail");
907
908 assert_validation_contains(error, "unknown dimension scope");
909 }
910
911 #[test]
912 fn translate_memory_ingest_rejects_coordinate_kind_mismatch() {
913 let mut command = sample_command();
914 command.memory.entries[0].coordinates[0].dimension = "ceremony".to_string();
915
916 let error = translate_memory_ingest(&command, &ExistingMemoryRefs::default())
917 .expect_err("coordinate kind mismatch should fail");
918
919 assert_validation_contains(error, "does not match declared kind `conversation`");
920 }
921
922 #[test]
923 fn translate_memory_ingest_rejects_relation_coordinate_kind_mismatch() {
924 let mut command = sample_command();
925 let mut coordinate = command.memory.entries[0].coordinates[0].clone();
926 coordinate.dimension = "ceremony".to_string();
927 command.memory.relations[0].coordinate = Some(coordinate);
928
929 let error = translate_memory_ingest(&command, &ExistingMemoryRefs::default())
930 .expect_err("relation coordinate kind mismatch should fail");
931
932 assert_validation_contains(error, "does not match declared kind `conversation`");
933 }
934
935 #[test]
936 fn translate_memory_ingest_fails_fast_for_unknown_relation_endpoint() {
937 let mut command = sample_command();
938 command.memory.relations[0].target_ref = "question:830ce83f:claim:missing".to_string();
939
940 let error = translate_memory_ingest(&command, &ExistingMemoryRefs::default())
941 .expect_err("unknown ref should fail");
942
943 assert_validation_contains(error, "references unknown refs");
944 }
945
946 #[test]
955 fn translate_memory_ingest_accepts_a_relation_to_the_abouts_own_anchor() {
956 let mut command = sample_command();
957 command.memory.relations[0].target_ref = command.about.clone();
958
959 let (update, _) = translate_memory_ingest(&command, &ExistingMemoryRefs::default())
960 .expect("an entry may relate to the about it belongs to");
961
962 assert!(
963 update
964 .changes
965 .iter()
966 .any(|change| change.entity_id.ends_with(&command.about)),
967 "the relation to the anchor must survive translation, got {:?}",
968 update
969 .changes
970 .iter()
971 .map(|change| change.entity_id.as_str())
972 .collect::<Vec<_>>()
973 );
974 }
975
976 #[test]
977 fn translate_memory_ingest_canonicalizes_known_relation_types() {
978 let mut command = sample_command();
979 command.memory.relations[0].rel = " CONTAINS-ENTRY ".to_string();
980
981 let (update, _) = translate_memory_ingest(&command, &ExistingMemoryRefs::default())
982 .expect("known relation aliases should canonicalize");
983
984 assert_eq!(
985 update.changes[2].entity_id,
986 "relation:about:question:830ce83f:dimension:conversation:rachel-2026-04-12:contains_entry:question:830ce83f:claim:rachel-denver"
987 );
988 }
989
990 #[test]
991 fn translate_memory_ingest_requires_non_structural_relation_proof() {
992 let mut command = sample_command();
993 command.memory.relations[0].semantic_class = "causal".to_string();
994 command.memory.relations[0].why = None;
995 command.memory.relations[0].evidence = None;
996 command.memory.relations[0].confidence = None;
997
998 let error = translate_memory_ingest(&command, &ExistingMemoryRefs::default())
999 .expect_err("missing proof should fail");
1000
1001 assert_validation_contains(error, "require confidence");
1002 }
1003
1004 #[test]
1005 fn translate_memory_ingest_accepts_existing_materialized_refs() {
1006 let mut command = sample_command();
1007 command.memory.dimensions.clear();
1008 command.memory.entries[0].coordinates[0].scope_id = "conversation:existing".to_string();
1009 command.memory.relations[0].source_ref = "conversation:existing".to_string();
1010 command.memory.relations[0].target_ref = "question:830ce83f:claim:existing".to_string();
1011 command.memory.evidence[0].supports = vec!["question:830ce83f:claim:existing".to_string()];
1012 let dimension_ref = "about:question:830ce83f:dimension:conversation:existing".to_string();
1013 let existing = ExistingMemoryRefs {
1014 refs: [
1015 dimension_ref.clone(),
1016 "question:830ce83f:claim:existing".to_string(),
1017 ]
1018 .into_iter()
1019 .collect(),
1020 dimensions: [dimension_ref].into_iter().collect(),
1021 labels: BTreeSet::new(),
1022 ..ExistingMemoryRefs::default()
1023 };
1024
1025 let (update, outcome) =
1026 translate_memory_ingest(&command, &existing).expect("existing refs should validate");
1027
1028 assert_eq!(outcome.accepted.entries, 1);
1029 assert_eq!(update.changes.len(), 3);
1030 }
1031
1032 #[test]
1033 fn translate_memory_ingest_treats_existing_namespaced_dimension_as_idempotent() {
1034 let command = sample_command();
1035 let dimension_ref =
1036 "about:question:830ce83f:dimension:conversation:rachel-2026-04-12".to_string();
1037 let existing = ExistingMemoryRefs {
1038 refs: [dimension_ref.clone()].into_iter().collect(),
1039 dimensions: [dimension_ref.clone()].into_iter().collect(),
1040 labels: BTreeSet::new(),
1041 ..ExistingMemoryRefs::default()
1042 };
1043
1044 let (update, outcome) = translate_memory_ingest(&command, &existing)
1045 .expect("existing dimension declaration should be idempotent");
1046
1047 assert_eq!(outcome.accepted.entries, 1);
1048 assert_eq!(
1049 update
1050 .changes
1051 .iter()
1052 .map(|change| change.entity_kind.as_str())
1053 .collect::<Vec<_>>(),
1054 vec!["memory_entry", "memory_relation", "memory_evidence"]
1055 );
1056 assert_eq!(
1057 update.changes[0].scopes,
1058 std::slice::from_ref(&dimension_ref)
1059 );
1060 assert_eq!(
1061 update.changes[1].entity_id,
1062 "relation:about:question:830ce83f:dimension:conversation:rachel-2026-04-12:contains_entry:question:830ce83f:claim:rachel-denver"
1063 );
1064 }
1065
1066 #[test]
1067 fn translate_memory_ingest_keeps_existing_dimensions_as_known_relation_refs() {
1068 let mut command = sample_command();
1069 command.memory.dimensions.clear();
1070 let dimension_ref =
1071 "about:question:830ce83f:dimension:conversation:rachel-2026-04-12".to_string();
1072 command.memory.relations[0].source_ref = dimension_ref.clone();
1073 let existing = ExistingMemoryRefs {
1074 refs: BTreeSet::new(),
1075 dimensions: [dimension_ref].into_iter().collect(),
1076 labels: BTreeSet::new(),
1077 ..ExistingMemoryRefs::default()
1078 };
1079
1080 translate_memory_ingest(&command, &existing)
1081 .expect("existing dimensions should also be valid relation refs");
1082 }
1083
1084 #[test]
1085 fn translate_memory_ingest_rejects_zero_coordinates_when_set() {
1086 let mut command = sample_command();
1087 command.memory.entries[0].coordinates[0].sequence = Some(0);
1088
1089 let error = translate_memory_ingest(&command, &ExistingMemoryRefs::default())
1090 .expect_err("zero coordinate sequence should fail");
1091
1092 assert_validation_contains(error, "sequence must be greater than zero");
1093 }
1094
1095 #[test]
1096 fn translate_memory_ingest_assigns_next_sequence_when_writer_omits_it() {
1097 let mut command = sample_command();
1098 command.memory.entries[0].coordinates[0].sequence = None;
1099 let scope = "about:question:830ce83f:dimension:conversation:rachel-2026-04-12".to_string();
1100 let existing = ExistingMemoryRefs {
1101 max_sequences: BTreeMap::from([(("conversation".to_string(), scope), 7)]),
1102 ..ExistingMemoryRefs::default()
1103 };
1104
1105 let (update, _) = translate_memory_ingest(&command, &existing)
1106 .expect("kernel should assign the next coordinate sequence");
1107 let entry = update
1108 .changes
1109 .iter()
1110 .find(|change| change.entity_kind == "memory_entry")
1111 .expect("entry change");
1112 let payload: serde_json::Value =
1113 serde_json::from_str(&entry.payload_json).expect("entry payload");
1114
1115 assert_eq!(payload["coordinates"][0]["sequence"], 8);
1116 }
1117
1118 #[test]
1119 fn translate_memory_ingest_bounds_every_caller_supplied_ref_field() {
1120 const HOSTILE_REFS: &[&str] = &[
1121 "incident:gamma:entry:observation:foreign",
1122 "incident:beta",
1123 "incident:alfa:entry:x\nincident:beta:entry:y",
1124 "../../incident:beta:entry:x",
1125 ];
1126 const REF_FIELDS: &[&str] = &[
1127 "entry.id",
1128 "relation.from",
1129 "relation.to",
1130 "relation.decision_id",
1131 "relation.caused_by_node_id",
1132 "evidence.id",
1133 "evidence.supports",
1134 ];
1135
1136 for field in REF_FIELDS {
1137 for hostile in HOSTILE_REFS {
1138 let mut command = sample_command();
1139 command.about = "incident:alfa".to_string();
1140 command.memory.entries[0].id = "incident:alfa:entry:observation:local".to_string();
1141 command.memory.relations[0].target_ref = command.memory.entries[0].id.clone();
1142 command.memory.evidence[0].id =
1143 "evidence:incident:alfa:entry:observation:local:current".to_string();
1144 command.memory.evidence[0].supports = vec![command.memory.entries[0].id.clone()];
1145
1146 match *field {
1147 "entry.id" => command.memory.entries[0].id = (*hostile).to_string(),
1148 "relation.from" => {
1149 command.memory.relations[0].source_ref = (*hostile).to_string()
1150 }
1151 "relation.to" => {
1152 command.memory.relations[0].target_ref = (*hostile).to_string()
1153 }
1154 "relation.decision_id" => {
1155 command.memory.relations[0].decision_id = Some((*hostile).to_string())
1156 }
1157 "relation.caused_by_node_id" => {
1158 command.memory.relations[0].caused_by_node_id = Some((*hostile).to_string())
1159 }
1160 "evidence.id" => command.memory.evidence[0].id = (*hostile).to_string(),
1161 "evidence.supports" => {
1162 command.memory.evidence[0].supports[0] = (*hostile).to_string()
1163 }
1164 unexpected => panic!("unknown test field {unexpected}"),
1165 }
1166
1167 let error = translate_memory_ingest(&command, &ExistingMemoryRefs::default())
1168 .expect_err("an ingest ref outside the about must be refused");
1169 assert_validation_contains(
1170 error,
1171 if hostile.contains('/') || hostile.contains('\n') {
1172 "memory refs cannot contain"
1173 } else {
1174 "does not belong to about"
1175 },
1176 );
1177 }
1178 }
1179 }
1180
1181 fn catalogue_with(kind: &str, value: &str) -> ExistingMemoryRefs {
1182 ExistingMemoryRefs {
1183 labels: BTreeSet::from([(kind.to_string(), value.to_string())]),
1184 ..ExistingMemoryRefs::default()
1185 }
1186 }
1187
1188 #[test]
1189 fn a_lax_ingest_writes_a_resembling_label_and_says_so() {
1190 let command = sample_command();
1191 let existing = catalogue_with("conversation", "conversation-rachel-2026-04-12");
1192
1193 let (_, outcome) =
1194 translate_memory_ingest(&command, &existing).expect("warn policy writes");
1195
1196 assert_eq!(outcome.resembling_labels.len(), 1);
1197 let resembling = &outcome.resembling_labels[0];
1198 assert_eq!(resembling.key, "conversation");
1199 assert_eq!(resembling.value, "conversation:rachel-2026-04-12");
1200 assert_eq!(resembling.existing_value, "conversation-rachel-2026-04-12");
1201 assert_eq!(resembling.kind, "same_label_spelled_differently");
1202 assert!(
1203 outcome
1204 .warnings
1205 .iter()
1206 .any(|warning| warning == &resembling.why),
1207 "the why is also a warning: {:?}",
1208 outcome.warnings
1209 );
1210 }
1211
1212 #[test]
1213 fn a_refusing_ingest_names_both_labels_and_the_way_to_insist() {
1214 let mut command = sample_command();
1215 command.label_policy = crate::memory::LabelPolicy::Refuse;
1216 let existing = catalogue_with("release", "conversation-rachel-2026-04-12");
1217
1218 let error = translate_memory_ingest(&command, &existing).expect_err("refused");
1219
1220 let message = match error {
1221 ApplicationError::Validation(message) => message,
1222 other => panic!("expected a validation error, got {other:?}"),
1223 };
1224 assert!(
1225 message.contains("`conversation=conversation:rachel-2026-04-12` resembles `release=conversation-rachel-2026-04-12`"),
1226 "{message}"
1227 );
1228 assert!(
1229 message.contains("the same value under another key"),
1230 "{message}"
1231 );
1232 assert!(message.contains("writer_intended_new"), "{message}");
1233 }
1234
1235 #[test]
1236 fn an_insisted_label_is_left_alone_and_the_insistence_is_not_stored() {
1237 let mut command = sample_command();
1238 command.label_policy = crate::memory::LabelPolicy::Refuse;
1239 command.memory.dimensions[0]
1240 .metadata
1241 .insert("writer_intended_new".to_string(), "true".to_string());
1242 let existing = catalogue_with("conversation", "conversation-rachel-2026-04-12");
1243
1244 let (update, outcome) =
1245 translate_memory_ingest(&command, &existing).expect("insisted label writes");
1246
1247 assert!(outcome.resembling_labels.is_empty());
1248 assert!(
1249 !update.changes[0]
1250 .payload_json
1251 .contains("writer_intended_new"),
1252 "the marker is read at translation and never stored: {}",
1253 update.changes[0].payload_json
1254 );
1255 }
1256
1257 #[test]
1258 fn a_label_the_about_already_holds_resembles_nothing() {
1259 let command = sample_command();
1260 let mut existing = catalogue_with("conversation", "conversation:rachel-2026-04-12");
1261 existing
1262 .dimensions
1263 .insert("about:question:830ce83f:dimension:conversation:rachel-2026-04-12".to_string());
1264
1265 let (_, outcome) = translate_memory_ingest(&command, &existing).expect("reuse");
1266
1267 assert!(outcome.resembling_labels.is_empty());
1268 assert!(outcome.created_dimensions.is_empty());
1269 }
1270
1271 fn sample_command() -> MemoryIngestCommand {
1272 MemoryIngestCommand {
1273 about: "question:830ce83f".to_string(),
1274 memory: MemoryData {
1275 dimensions: vec![MemoryDimensionData {
1276 id: "conversation:rachel-2026-04-12".to_string(),
1277 kind: "conversation".to_string(),
1278 title: Some("Rachel relocation discussion".to_string()),
1279 metadata: Default::default(),
1280 }],
1281 entries: vec![MemoryEntryData {
1282 id: "question:830ce83f:claim:rachel-denver".to_string(),
1283 kind: "claim".to_string(),
1284 text: "Rachel said she was moving to Denver.".to_string(),
1285 coordinates: vec![MemoryCoordinateData {
1286 dimension: "conversation".to_string(),
1287 scope_id: "conversation:rachel-2026-04-12".to_string(),
1288 occurred_at: Some("2026-04-12T15:00:00Z".to_string()),
1289 observed_at: None,
1290 ingested_at: None,
1291 valid_from: None,
1292 valid_until: None,
1293 sequence: Some(1),
1294 rank: None,
1295 metadata: Default::default(),
1296 }],
1297 metadata: Default::default(),
1298 }],
1299 relations: vec![MemoryRelationData {
1300 source_ref: "conversation:rachel-2026-04-12".to_string(),
1301 target_ref: "question:830ce83f:claim:rachel-denver".to_string(),
1302 rel: "contains_entry".to_string(),
1303 semantic_class: "structural".to_string(),
1304 why: None,
1305 evidence: None,
1306 confidence: None,
1307 sequence: Some(1),
1308 motivation: None,
1309 method: None,
1310 decision_id: None,
1311 caused_by_node_id: None,
1312 coordinate: None,
1313 }],
1314 evidence: vec![MemoryEvidenceData {
1315 id: "evidence:question:830ce83f:claim:rachel-denver".to_string(),
1316 supports: vec!["question:830ce83f:claim:rachel-denver".to_string()],
1317 text: "Conversation transcript line 1".to_string(),
1318 source: Some("transcript:1".to_string()),
1319 time: Some("2026-04-12T15:00:00Z".to_string()),
1320 metadata: Default::default(),
1321 }],
1322 },
1323 provenance: None,
1324 idempotency_key: "ingest:app-test".to_string(),
1325 dry_run: false,
1326 label_policy: Default::default(),
1327 }
1328 }
1329
1330 fn assert_validation_contains(error: ApplicationError, expected: &str) {
1331 match error {
1332 ApplicationError::Validation(message) => assert!(
1333 message.contains(expected),
1334 "expected `{message}` to contain `{expected}`"
1335 ),
1336 other => panic!("expected validation error, got {other:?}"),
1337 }
1338 }
1339
1340 fn cross_about_relation(rel: &str, method: Option<&str>) -> MemoryRelationData {
1341 MemoryRelationData {
1342 source_ref: "question:830ce83f:claim:rachel-denver".to_string(),
1343 target_ref: "incident:platform:outcome:freeze".to_string(),
1344 rel: rel.to_string(),
1345 semantic_class: "evidential".to_string(),
1346 why: Some("Both record the same freeze.".to_string()),
1347 evidence: Some("kmp_relate proposal by identifier.".to_string()),
1348 confidence: Some("high".to_string()),
1349 sequence: None,
1350 motivation: None,
1351 method: method.map(str::to_string),
1352 decision_id: None,
1353 caused_by_node_id: None,
1354 coordinate: None,
1355 }
1356 }
1357
1358 #[test]
1363 fn a_declared_equivalence_crosses_the_about_and_nothing_else_does() {
1364 let mut command = sample_command();
1365 command.memory.relations.push(cross_about_relation(
1366 "same_event_as",
1367 Some("kmp_relate:identifier"),
1368 ));
1369 let mut existing = ExistingMemoryRefs::default();
1370 existing
1371 .foreign
1372 .insert("incident:platform:outcome:freeze".to_string());
1373 let (update, _) = translate_memory_ingest(&command, &existing)
1374 .expect("a declared equivalence is written");
1375 assert!(
1376 update.changes.iter().any(|change| {
1377 change.payload_json.contains("same_event_as")
1378 && change
1379 .payload_json
1380 .contains("incident:platform:outcome:freeze")
1381 }),
1382 "the equivalence is among the changes"
1383 );
1384
1385 let unverified = translate_memory_ingest(&command, &ExistingMemoryRefs::default())
1386 .expect_err("a ref no about holds is refused");
1387 assert!(
1388 unverified.to_string().contains("a ref no about holds"),
1389 "{unverified}"
1390 );
1391
1392 let mut unstamped = sample_command();
1393 unstamped
1394 .memory
1395 .relations
1396 .push(cross_about_relation("same_event_as", None));
1397 let error = translate_memory_ingest(&unstamped, &existing)
1398 .expect_err("without the proposal stamp the boundary holds");
1399 assert!(
1400 error.to_string().contains("does not belong to about"),
1401 "{error}"
1402 );
1403
1404 let mut follows = sample_command();
1405 follows.memory.relations.push(cross_about_relation(
1406 "follows",
1407 Some("kmp_relate:identifier"),
1408 ));
1409 let error = translate_memory_ingest(&follows, &existing)
1410 .expect_err("no other relation crosses an about");
1411 assert!(
1412 error.to_string().contains("does not belong to about"),
1413 "{error}"
1414 );
1415 }
1416}