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