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