1use std::collections::{HashMap, HashSet};
4use std::sync::{Arc, LazyLock};
5
6#[cfg(test)]
7use std::cell::Cell;
8
9use arrow::array::{
10 Array, FixedSizeBinaryArray, FixedSizeBinaryBuilder, StringArray, StringBuilder,
11 TimestampMicrosecondArray, TimestampMicrosecondBuilder, UInt32Array, UInt32Builder,
12};
13use arrow::datatypes::{DataType, Field, Schema, SchemaRef, TimeUnit};
14use arrow::record_batch::RecordBatch;
15use graphforge_core::canonical::{
16 CANONICAL_CONTRACT_VERSION, CanonicalDomain, CanonicalWriter, fingerprint,
17};
18use unicode_normalization::UnicodeNormalization;
19use uuid::{Uuid, Version};
20
21use crate::{
22 EPISTEMIC_CAPABILITY_VERSION, KnowledgeError, MAX_KNOWLEDGE_ROWS, SchemaRegistryEntry,
23};
24
25pub const HYPOTHESIS_GROUP_CONTRACT_VERSION: u32 = 1;
27pub const HYPOTHESIS_MEMBERSHIP_CONTRACT_VERSION: u32 = 1;
29pub const HYPOTHESIS_SELECTION_CONTRACT_VERSION: u32 = 1;
31pub const HYPOTHESIS_KEY_POLICY_VERSION: u32 = 1;
33pub const HYPOTHESIS_STATE_POLICY_VERSION: u32 = 1;
35pub const MAX_HYPOTHESIS_QUESTION_KEY_BYTES: usize = 1_024;
37
38pub static HYPOTHESIS_GROUP_SCHEMA: LazyLock<SchemaRef> = LazyLock::new(|| {
40 Arc::new(Schema::new(vec![
41 uuid_field("group_uuid", false),
42 Field::new("question_key", DataType::Utf8, false),
43 uuid_field("provenance_uuid", false),
44 timestamp_field("recorded_at"),
45 Field::new("contract_version", DataType::UInt32, false),
46 ]))
47});
48
49pub static HYPOTHESIS_MEMBERSHIP_SCHEMA: LazyLock<SchemaRef> = LazyLock::new(|| {
51 Arc::new(Schema::new(vec![
52 uuid_field("membership_event_uuid", false),
53 uuid_field("operation_uuid", false),
54 uuid_field("group_uuid", false),
55 uuid_field("assertion_uuid", false),
56 Field::new("action", DataType::Utf8, false),
57 uuid_field("reasoning_uuid", false),
58 uuid_field("provenance_uuid", false),
59 timestamp_field("recorded_at"),
60 Field::new("contract_version", DataType::UInt32, false),
61 ]))
62});
63
64pub static HYPOTHESIS_SELECTION_SCHEMA: LazyLock<SchemaRef> = LazyLock::new(|| {
66 Arc::new(Schema::new(vec![
67 uuid_field("selection_event_uuid", false),
68 uuid_field("operation_uuid", false),
69 uuid_field("group_uuid", false),
70 uuid_field("selected_assertion_uuid", true),
71 uuid_field("reasoning_uuid", false),
72 uuid_field("provenance_uuid", false),
73 timestamp_field("recorded_at"),
74 Field::new("contract_version", DataType::UInt32, false),
75 ]))
76});
77
78static GROUP_SCHEMA_FINGERPRINT: LazyLock<[u8; 32]> = LazyLock::new(|| {
79 fingerprint(
80 CanonicalDomain::Schema,
81 CANONICAL_CONTRACT_VERSION,
82 b"hypothesis_group/1|group_uuid:fixed[16]:required|question_key:utf8:required|provenance_uuid:fixed[16]:required|recorded_at:timestamp_us_utc:required|contract_version:u32:required",
83 )
84 .expect("registered hypothesis-group schema is within canonical bounds")
85});
86
87static MEMBERSHIP_SCHEMA_FINGERPRINT: LazyLock<[u8; 32]> = LazyLock::new(|| {
88 fingerprint(
89 CanonicalDomain::Schema,
90 CANONICAL_CONTRACT_VERSION,
91 b"hypothesis_membership/1|membership_event_uuid:fixed[16]:required|operation_uuid:fixed[16]:required|group_uuid:fixed[16]:required|assertion_uuid:fixed[16]:required|action:utf8:required|reasoning_uuid:fixed[16]:required|provenance_uuid:fixed[16]:required|recorded_at:timestamp_us_utc:required|contract_version:u32:required",
92 )
93 .expect("registered hypothesis-membership schema is within canonical bounds")
94});
95
96static SELECTION_SCHEMA_FINGERPRINT: LazyLock<[u8; 32]> = LazyLock::new(|| {
97 fingerprint(
98 CanonicalDomain::Schema,
99 CANONICAL_CONTRACT_VERSION,
100 b"hypothesis_selection/1|selection_event_uuid:fixed[16]:required|operation_uuid:fixed[16]:required|group_uuid:fixed[16]:required|selected_assertion_uuid:fixed[16]:nullable|reasoning_uuid:fixed[16]:required|provenance_uuid:fixed[16]:required|recorded_at:timestamp_us_utc:required|contract_version:u32:required",
101 )
102 .expect("registered hypothesis-selection schema is within canonical bounds")
103});
104
105#[cfg(test)]
106thread_local! {
107 static ROW_VALIDATION_CALLS: Cell<Option<usize>> = const { Cell::new(None) };
108}
109
110#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
112pub enum HypothesisMembershipAction {
113 Added,
115 Removed,
117}
118
119impl HypothesisMembershipAction {
120 #[must_use]
122 pub const fn as_str(self) -> &'static str {
123 match self {
124 Self::Added => "added",
125 Self::Removed => "removed",
126 }
127 }
128
129 fn parse(value: &str) -> Result<Self, KnowledgeError> {
130 match value {
131 "added" => Ok(Self::Added),
132 "removed" => Ok(Self::Removed),
133 _ => Err(invalid(
134 "hypothesis_membership.action",
135 "unknown registry value",
136 )),
137 }
138 }
139}
140
141#[derive(Clone, Debug, Eq, PartialEq)]
143pub struct HypothesisGroup {
144 pub group_uuid: Uuid,
146 pub question_key: String,
148 pub provenance_uuid: Uuid,
150 pub recorded_at_micros: i64,
152 pub contract_version: u32,
154}
155
156impl HypothesisGroup {
157 pub fn new(
159 group_uuid: Uuid,
160 question_key: String,
161 provenance_uuid: Uuid,
162 recorded_at_micros: i64,
163 ) -> Result<Self, KnowledgeError> {
164 require_v7(group_uuid, "group_uuid")?;
165 require_uuid(provenance_uuid, "provenance_uuid")?;
166 validate_question_key(&question_key)?;
167 Ok(Self {
168 group_uuid,
169 question_key,
170 provenance_uuid,
171 recorded_at_micros,
172 contract_version: HYPOTHESIS_GROUP_CONTRACT_VERSION,
173 })
174 }
175}
176
177#[derive(Clone, Debug, Eq, PartialEq)]
179pub struct HypothesisMembershipEvent {
180 pub membership_event_uuid: Uuid,
182 pub operation_uuid: Uuid,
184 pub group_uuid: Uuid,
186 pub assertion_uuid: Uuid,
188 pub action: HypothesisMembershipAction,
190 pub reasoning_uuid: Uuid,
192 pub provenance_uuid: Uuid,
194 pub recorded_at_micros: i64,
196 pub contract_version: u32,
198}
199
200impl HypothesisMembershipEvent {
201 #[allow(clippy::too_many_arguments)]
203 pub fn new(
204 membership_event_uuid: Uuid,
205 operation_uuid: Uuid,
206 group_uuid: Uuid,
207 assertion_uuid: Uuid,
208 action: HypothesisMembershipAction,
209 reasoning_uuid: Uuid,
210 provenance_uuid: Uuid,
211 recorded_at_micros: i64,
212 ) -> Result<Self, KnowledgeError> {
213 for (uuid, field) in [
214 (membership_event_uuid, "membership_event_uuid"),
215 (operation_uuid, "operation_uuid"),
216 (group_uuid, "group_uuid"),
217 (assertion_uuid, "assertion_uuid"),
218 (reasoning_uuid, "reasoning_uuid"),
219 ] {
220 require_v7(uuid, field)?;
221 }
222 require_uuid(provenance_uuid, "provenance_uuid")?;
223 Ok(Self {
224 membership_event_uuid,
225 operation_uuid,
226 group_uuid,
227 assertion_uuid,
228 action,
229 reasoning_uuid,
230 provenance_uuid,
231 recorded_at_micros,
232 contract_version: HYPOTHESIS_MEMBERSHIP_CONTRACT_VERSION,
233 })
234 }
235}
236
237#[derive(Clone, Debug, Eq, PartialEq)]
239pub struct HypothesisSelectionEvent {
240 pub selection_event_uuid: Uuid,
242 pub operation_uuid: Uuid,
244 pub group_uuid: Uuid,
246 pub selected_assertion_uuid: Option<Uuid>,
248 pub reasoning_uuid: Uuid,
250 pub provenance_uuid: Uuid,
252 pub recorded_at_micros: i64,
254 pub contract_version: u32,
256}
257
258impl HypothesisSelectionEvent {
259 #[allow(clippy::too_many_arguments)]
261 pub fn new(
262 selection_event_uuid: Uuid,
263 operation_uuid: Uuid,
264 group_uuid: Uuid,
265 selected_assertion_uuid: Option<Uuid>,
266 reasoning_uuid: Uuid,
267 provenance_uuid: Uuid,
268 recorded_at_micros: i64,
269 ) -> Result<Self, KnowledgeError> {
270 for (uuid, field) in [
271 (selection_event_uuid, "selection_event_uuid"),
272 (operation_uuid, "operation_uuid"),
273 (group_uuid, "group_uuid"),
274 (reasoning_uuid, "reasoning_uuid"),
275 ] {
276 require_v7(uuid, field)?;
277 }
278 if let Some(assertion_uuid) = selected_assertion_uuid {
279 require_v7(assertion_uuid, "selected_assertion_uuid")?;
280 }
281 require_uuid(provenance_uuid, "provenance_uuid")?;
282 Ok(Self {
283 selection_event_uuid,
284 operation_uuid,
285 group_uuid,
286 selected_assertion_uuid,
287 reasoning_uuid,
288 provenance_uuid,
289 recorded_at_micros,
290 contract_version: HYPOTHESIS_SELECTION_CONTRACT_VERSION,
291 })
292 }
293}
294
295#[derive(Clone, Debug, Default, Eq, PartialEq)]
297pub struct HypothesisLedger {
298 groups: Vec<HypothesisGroup>,
299 membership_events: Vec<HypothesisMembershipEvent>,
300 selection_events: Vec<HypothesisSelectionEvent>,
301}
302
303impl HypothesisLedger {
304 pub fn new(
306 groups: Vec<HypothesisGroup>,
307 membership_events: Vec<HypothesisMembershipEvent>,
308 selection_events: Vec<HypothesisSelectionEvent>,
309 ) -> Result<Self, KnowledgeError> {
310 Self::new_with_limit(
311 groups,
312 membership_events,
313 selection_events,
314 MAX_KNOWLEDGE_ROWS,
315 )
316 }
317
318 fn new_with_limit(
319 mut groups: Vec<HypothesisGroup>,
320 mut membership_events: Vec<HypothesisMembershipEvent>,
321 mut selection_events: Vec<HypothesisSelectionEvent>,
322 row_limit: usize,
323 ) -> Result<Self, KnowledgeError> {
324 for (participant, observed) in [
325 ("hypothesis_groups", groups.len()),
326 ("hypothesis_membership_events", membership_events.len()),
327 ("hypothesis_selection_events", selection_events.len()),
328 ] {
329 validate_row_count(participant, observed, row_limit)?;
330 }
331 validate_groups(&groups)?;
332 validate_event_ids(&membership_events, &selection_events)?;
333 groups.sort_by_key(|row| (row.recorded_at_micros, row.group_uuid));
334 membership_events.sort_by_key(|row| (row.recorded_at_micros, row.membership_event_uuid));
335 selection_events.sort_by_key(|row| (row.recorded_at_micros, row.selection_event_uuid));
336 validate_state(&groups, &membership_events, &selection_events)?;
337 Ok(Self {
338 groups,
339 membership_events,
340 selection_events,
341 })
342 }
343
344 #[must_use]
345 pub fn groups(&self) -> &[HypothesisGroup] {
347 &self.groups
348 }
349
350 #[must_use]
351 pub fn membership_events(&self) -> &[HypothesisMembershipEvent] {
353 &self.membership_events
354 }
355
356 #[must_use]
357 pub fn selection_events(&self) -> &[HypothesisSelectionEvent] {
359 &self.selection_events
360 }
361
362 pub fn group_fingerprint(&self, group_uuid: Uuid) -> Result<[u8; 32], KnowledgeError> {
364 let row = self
365 .groups
366 .iter()
367 .find(|row| row.group_uuid == group_uuid)
368 .ok_or(KnowledgeError::Dangling("group_uuid"))?;
369 group_fingerprint_for_row(row)
370 }
371
372 pub fn group_fingerprints(&self) -> Result<Vec<(Uuid, [u8; 32])>, KnowledgeError> {
374 self.groups
375 .iter()
376 .map(|row| Ok((row.group_uuid, group_fingerprint_for_row(row)?)))
377 .collect()
378 }
379
380 pub fn membership_fingerprint(
382 &self,
383 membership_event_uuid: Uuid,
384 ) -> Result<[u8; 32], KnowledgeError> {
385 let row = self
386 .membership_events
387 .iter()
388 .find(|row| row.membership_event_uuid == membership_event_uuid)
389 .ok_or(KnowledgeError::Dangling("membership_event_uuid"))?;
390 membership_fingerprint_for_row(row)
391 }
392
393 pub fn membership_fingerprints(&self) -> Result<Vec<(Uuid, [u8; 32])>, KnowledgeError> {
395 self.membership_events
396 .iter()
397 .map(|row| {
398 Ok((
399 row.membership_event_uuid,
400 membership_fingerprint_for_row(row)?,
401 ))
402 })
403 .collect()
404 }
405
406 pub fn selection_fingerprint(
408 &self,
409 selection_event_uuid: Uuid,
410 ) -> Result<[u8; 32], KnowledgeError> {
411 let row = self
412 .selection_events
413 .iter()
414 .find(|row| row.selection_event_uuid == selection_event_uuid)
415 .ok_or(KnowledgeError::Dangling("selection_event_uuid"))?;
416 selection_fingerprint_for_row(row)
417 }
418
419 pub fn selection_fingerprints(&self) -> Result<Vec<(Uuid, [u8; 32])>, KnowledgeError> {
421 self.selection_events
422 .iter()
423 .map(|row| {
424 Ok((
425 row.selection_event_uuid,
426 selection_fingerprint_for_row(row)?,
427 ))
428 })
429 .collect()
430 }
431
432 pub fn merge(&self, staged: &Self) -> Result<Self, KnowledgeError> {
434 self.merge_with_limit(staged, MAX_KNOWLEDGE_ROWS)
435 }
436
437 fn merge_with_limit(&self, staged: &Self, row_limit: usize) -> Result<Self, KnowledgeError> {
438 let mut groups = merge_rows(
439 &self.groups,
440 &staged.groups,
441 |row| row.group_uuid,
442 "group_uuid",
443 "hypothesis_groups",
444 row_limit,
445 )?;
446 let mut membership_events = merge_rows(
447 &self.membership_events,
448 &staged.membership_events,
449 |row| row.membership_event_uuid,
450 "membership_event_uuid",
451 "hypothesis_membership_events",
452 row_limit,
453 )?;
454 let mut selection_events = merge_rows(
455 &self.selection_events,
456 &staged.selection_events,
457 |row| row.selection_event_uuid,
458 "selection_event_uuid",
459 "hypothesis_selection_events",
460 row_limit,
461 )?;
462
463 validate_question_key_uniqueness(&groups)?;
464 groups.sort_by_key(|row| (row.recorded_at_micros, row.group_uuid));
465 membership_events.sort_by_key(|row| (row.recorded_at_micros, row.membership_event_uuid));
466 selection_events.sort_by_key(|row| (row.recorded_at_micros, row.selection_event_uuid));
467
468 validate_state(&groups, &membership_events, &selection_events)?;
472 Ok(Self {
473 groups,
474 membership_events,
475 selection_events,
476 })
477 }
478
479 pub fn group_batch(&self) -> Result<RecordBatch, KnowledgeError> {
481 let len = self.groups.len();
482 let mut ids = FixedSizeBinaryBuilder::with_capacity(len, 16);
483 let mut keys = StringBuilder::with_capacity(
484 len,
485 self.groups.iter().map(|r| r.question_key.len()).sum(),
486 );
487 let mut provenance = FixedSizeBinaryBuilder::with_capacity(len, 16);
488 let mut times = TimestampMicrosecondBuilder::with_capacity(len).with_timezone("UTC");
489 let mut versions = UInt32Builder::with_capacity(len);
490 for row in &self.groups {
491 append_uuid(&mut ids, row.group_uuid, "group_uuid")?;
492 keys.append_value(&row.question_key);
493 append_uuid(&mut provenance, row.provenance_uuid, "provenance_uuid")?;
494 times.append_value(row.recorded_at_micros);
495 versions.append_value(row.contract_version);
496 }
497 record_batch(
498 Arc::clone(&HYPOTHESIS_GROUP_SCHEMA),
499 vec![
500 Arc::new(ids.finish()),
501 Arc::new(keys.finish()),
502 Arc::new(provenance.finish()),
503 Arc::new(times.finish()),
504 Arc::new(versions.finish()),
505 ],
506 )
507 }
508
509 pub fn membership_batch(&self) -> Result<RecordBatch, KnowledgeError> {
511 let len = self.membership_events.len();
512 let mut ids = FixedSizeBinaryBuilder::with_capacity(len, 16);
513 let mut operations = FixedSizeBinaryBuilder::with_capacity(len, 16);
514 let mut groups = FixedSizeBinaryBuilder::with_capacity(len, 16);
515 let mut assertions = FixedSizeBinaryBuilder::with_capacity(len, 16);
516 let mut actions = StringBuilder::with_capacity(len, len * 7);
517 let mut reasoning = FixedSizeBinaryBuilder::with_capacity(len, 16);
518 let mut provenance = FixedSizeBinaryBuilder::with_capacity(len, 16);
519 let mut times = TimestampMicrosecondBuilder::with_capacity(len).with_timezone("UTC");
520 let mut versions = UInt32Builder::with_capacity(len);
521 for row in &self.membership_events {
522 append_uuid(&mut ids, row.membership_event_uuid, "membership_event_uuid")?;
523 append_uuid(&mut operations, row.operation_uuid, "operation_uuid")?;
524 append_uuid(&mut groups, row.group_uuid, "group_uuid")?;
525 append_uuid(&mut assertions, row.assertion_uuid, "assertion_uuid")?;
526 actions.append_value(row.action.as_str());
527 append_uuid(&mut reasoning, row.reasoning_uuid, "reasoning_uuid")?;
528 append_uuid(&mut provenance, row.provenance_uuid, "provenance_uuid")?;
529 times.append_value(row.recorded_at_micros);
530 versions.append_value(row.contract_version);
531 }
532 record_batch(
533 Arc::clone(&HYPOTHESIS_MEMBERSHIP_SCHEMA),
534 vec![
535 Arc::new(ids.finish()),
536 Arc::new(operations.finish()),
537 Arc::new(groups.finish()),
538 Arc::new(assertions.finish()),
539 Arc::new(actions.finish()),
540 Arc::new(reasoning.finish()),
541 Arc::new(provenance.finish()),
542 Arc::new(times.finish()),
543 Arc::new(versions.finish()),
544 ],
545 )
546 }
547
548 pub fn selection_batch(&self) -> Result<RecordBatch, KnowledgeError> {
550 let len = self.selection_events.len();
551 let mut ids = FixedSizeBinaryBuilder::with_capacity(len, 16);
552 let mut operations = FixedSizeBinaryBuilder::with_capacity(len, 16);
553 let mut groups = FixedSizeBinaryBuilder::with_capacity(len, 16);
554 let mut selected = FixedSizeBinaryBuilder::with_capacity(len, 16);
555 let mut reasoning = FixedSizeBinaryBuilder::with_capacity(len, 16);
556 let mut provenance = FixedSizeBinaryBuilder::with_capacity(len, 16);
557 let mut times = TimestampMicrosecondBuilder::with_capacity(len).with_timezone("UTC");
558 let mut versions = UInt32Builder::with_capacity(len);
559 for row in &self.selection_events {
560 append_uuid(&mut ids, row.selection_event_uuid, "selection_event_uuid")?;
561 append_uuid(&mut operations, row.operation_uuid, "operation_uuid")?;
562 append_uuid(&mut groups, row.group_uuid, "group_uuid")?;
563 if let Some(value) = row.selected_assertion_uuid {
564 append_uuid(&mut selected, value, "selected_assertion_uuid")?;
565 } else {
566 selected.append_null();
567 }
568 append_uuid(&mut reasoning, row.reasoning_uuid, "reasoning_uuid")?;
569 append_uuid(&mut provenance, row.provenance_uuid, "provenance_uuid")?;
570 times.append_value(row.recorded_at_micros);
571 versions.append_value(row.contract_version);
572 }
573 record_batch(
574 Arc::clone(&HYPOTHESIS_SELECTION_SCHEMA),
575 vec![
576 Arc::new(ids.finish()),
577 Arc::new(operations.finish()),
578 Arc::new(groups.finish()),
579 Arc::new(selected.finish()),
580 Arc::new(reasoning.finish()),
581 Arc::new(provenance.finish()),
582 Arc::new(times.finish()),
583 Arc::new(versions.finish()),
584 ],
585 )
586 }
587
588 pub fn from_batches(
590 group_batches: &[RecordBatch],
591 membership_batches: &[RecordBatch],
592 selection_batches: &[RecordBatch],
593 ) -> Result<Self, KnowledgeError> {
594 let mut groups = Vec::new();
595 for batch in group_batches {
596 require_schema(batch, &HYPOTHESIS_GROUP_SCHEMA, "hypothesis_group.schema")?;
597 let ids = fixed(batch, "group_uuid")?;
598 let keys = strings(batch, "question_key")?;
599 let provenance = fixed(batch, "provenance_uuid")?;
600 let times = timestamps(batch)?;
601 let versions = versions(batch)?;
602 for row in 0..batch.num_rows() {
603 groups.push(HypothesisGroup {
604 group_uuid: uuid_at(ids, row, "group_uuid")?,
605 question_key: keys.value(row).to_owned(),
606 provenance_uuid: uuid_at(provenance, row, "provenance_uuid")?,
607 recorded_at_micros: times.value(row),
608 contract_version: versions.value(row),
609 });
610 }
611 }
612 let mut membership = Vec::new();
613 for batch in membership_batches {
614 require_schema(
615 batch,
616 &HYPOTHESIS_MEMBERSHIP_SCHEMA,
617 "hypothesis_membership.schema",
618 )?;
619 let ids = fixed(batch, "membership_event_uuid")?;
620 let operations = fixed(batch, "operation_uuid")?;
621 let group_ids = fixed(batch, "group_uuid")?;
622 let assertions = fixed(batch, "assertion_uuid")?;
623 let actions = strings(batch, "action")?;
624 let reasoning = fixed(batch, "reasoning_uuid")?;
625 let provenance = fixed(batch, "provenance_uuid")?;
626 let times = timestamps(batch)?;
627 let versions = versions(batch)?;
628 for row in 0..batch.num_rows() {
629 membership.push(HypothesisMembershipEvent {
630 membership_event_uuid: uuid_at(ids, row, "membership_event_uuid")?,
631 operation_uuid: uuid_at(operations, row, "operation_uuid")?,
632 group_uuid: uuid_at(group_ids, row, "group_uuid")?,
633 assertion_uuid: uuid_at(assertions, row, "assertion_uuid")?,
634 action: HypothesisMembershipAction::parse(actions.value(row))?,
635 reasoning_uuid: uuid_at(reasoning, row, "reasoning_uuid")?,
636 provenance_uuid: uuid_at(provenance, row, "provenance_uuid")?,
637 recorded_at_micros: times.value(row),
638 contract_version: versions.value(row),
639 });
640 }
641 }
642 let mut selection = Vec::new();
643 for batch in selection_batches {
644 require_schema(
645 batch,
646 &HYPOTHESIS_SELECTION_SCHEMA,
647 "hypothesis_selection.schema",
648 )?;
649 let ids = fixed(batch, "selection_event_uuid")?;
650 let operations = fixed(batch, "operation_uuid")?;
651 let group_ids = fixed(batch, "group_uuid")?;
652 let selected = fixed(batch, "selected_assertion_uuid")?;
653 let reasoning = fixed(batch, "reasoning_uuid")?;
654 let provenance = fixed(batch, "provenance_uuid")?;
655 let times = timestamps(batch)?;
656 let versions = versions(batch)?;
657 for row in 0..batch.num_rows() {
658 selection.push(HypothesisSelectionEvent {
659 selection_event_uuid: uuid_at(ids, row, "selection_event_uuid")?,
660 operation_uuid: uuid_at(operations, row, "operation_uuid")?,
661 group_uuid: uuid_at(group_ids, row, "group_uuid")?,
662 selected_assertion_uuid: (!selected.is_null(row))
663 .then(|| uuid_at(selected, row, "selected_assertion_uuid"))
664 .transpose()?,
665 reasoning_uuid: uuid_at(reasoning, row, "reasoning_uuid")?,
666 provenance_uuid: uuid_at(provenance, row, "provenance_uuid")?,
667 recorded_at_micros: times.value(row),
668 contract_version: versions.value(row),
669 });
670 }
671 }
672 Self::new(groups, membership, selection)
673 }
674
675 #[must_use]
677 pub fn current_members(&self, group_uuid: Uuid) -> Vec<Uuid> {
678 let mut state = HashSet::new();
679 for event in self
680 .membership_events
681 .iter()
682 .filter(|row| row.group_uuid == group_uuid)
683 {
684 match event.action {
685 HypothesisMembershipAction::Added => {
686 state.insert(event.assertion_uuid);
687 }
688 HypothesisMembershipAction::Removed => {
689 state.remove(&event.assertion_uuid);
690 }
691 }
692 }
693 let mut members = state.into_iter().collect::<Vec<_>>();
694 members.sort_unstable();
695 members
696 }
697
698 #[must_use]
700 pub fn current_selection(&self, group_uuid: Uuid) -> Option<Uuid> {
701 self.selection_events
702 .iter()
703 .rfind(|row| row.group_uuid == group_uuid)
704 .and_then(|row| row.selected_assertion_uuid)
705 }
706}
707
708fn group_fingerprint_for_row(row: &HypothesisGroup) -> Result<[u8; 32], KnowledgeError> {
709 let mut writer = CanonicalWriter::new();
710 writer.raw(row.group_uuid.as_bytes())?;
711 writer.text(&row.question_key)?;
712 writer.raw(row.provenance_uuid.as_bytes())?;
713 writer.i64(row.recorded_at_micros)?;
714 writer.u32(row.contract_version)?;
715 fingerprint(
716 CanonicalDomain::HypothesisGroup,
717 CANONICAL_CONTRACT_VERSION,
718 &writer.finish(),
719 )
720 .map_err(Into::into)
721}
722
723fn membership_fingerprint_for_row(
724 row: &HypothesisMembershipEvent,
725) -> Result<[u8; 32], KnowledgeError> {
726 let mut writer = CanonicalWriter::new();
727 for value in [
728 row.membership_event_uuid,
729 row.operation_uuid,
730 row.group_uuid,
731 row.assertion_uuid,
732 ] {
733 writer.raw(value.as_bytes())?;
734 }
735 writer.text(row.action.as_str())?;
736 writer.raw(row.reasoning_uuid.as_bytes())?;
737 writer.raw(row.provenance_uuid.as_bytes())?;
738 writer.i64(row.recorded_at_micros)?;
739 writer.u32(row.contract_version)?;
740 fingerprint(
741 CanonicalDomain::HypothesisMembership,
742 CANONICAL_CONTRACT_VERSION,
743 &writer.finish(),
744 )
745 .map_err(Into::into)
746}
747
748fn selection_fingerprint_for_row(
749 row: &HypothesisSelectionEvent,
750) -> Result<[u8; 32], KnowledgeError> {
751 let mut writer = CanonicalWriter::new();
752 for value in [row.selection_event_uuid, row.operation_uuid, row.group_uuid] {
753 writer.raw(value.as_bytes())?;
754 }
755 match row.selected_assertion_uuid {
756 Some(value) => {
757 writer.u8(1)?;
758 writer.raw(value.as_bytes())?;
759 }
760 None => writer.u8(0)?,
761 }
762 writer.raw(row.reasoning_uuid.as_bytes())?;
763 writer.raw(row.provenance_uuid.as_bytes())?;
764 writer.i64(row.recorded_at_micros)?;
765 writer.u32(row.contract_version)?;
766 fingerprint(
767 CanonicalDomain::HypothesisSelection,
768 CANONICAL_CONTRACT_VERSION,
769 &writer.finish(),
770 )
771 .map_err(Into::into)
772}
773
774fn validate_question_key(value: &str) -> Result<(), KnowledgeError> {
775 if value.is_empty()
776 || value.len() > MAX_HYPOTHESIS_QUESTION_KEY_BYTES
777 || value.trim() != value
778 || !value.nfc().eq(value.chars())
779 {
780 return Err(invalid(
781 "hypothesis_group.question_key",
782 "must be non-empty bounded NFC without surrounding whitespace",
783 ));
784 }
785 Ok(())
786}
787
788fn validate_groups(groups: &[HypothesisGroup]) -> Result<(), KnowledgeError> {
789 let mut ids = HashSet::new();
790 let mut keys = HashSet::new();
791 for row in groups {
792 count_row_validation();
793 HypothesisGroup::new(
794 row.group_uuid,
795 row.question_key.clone(),
796 row.provenance_uuid,
797 row.recorded_at_micros,
798 )?;
799 if row.contract_version != HYPOTHESIS_GROUP_CONTRACT_VERSION {
800 return Err(invalid(
801 "hypothesis_group.contract_version",
802 "unsupported version",
803 ));
804 }
805 if !ids.insert(row.group_uuid) {
806 return Err(KnowledgeError::Duplicate("group_uuid"));
807 }
808 if !keys.insert(row.question_key.as_str()) {
809 return Err(KnowledgeError::Duplicate("question_key"));
810 }
811 }
812 Ok(())
813}
814
815fn validate_event_ids(
816 membership: &[HypothesisMembershipEvent],
817 selection: &[HypothesisSelectionEvent],
818) -> Result<(), KnowledgeError> {
819 let mut ids = HashSet::new();
820 for row in membership {
821 count_row_validation();
822 HypothesisMembershipEvent::new(
823 row.membership_event_uuid,
824 row.operation_uuid,
825 row.group_uuid,
826 row.assertion_uuid,
827 row.action,
828 row.reasoning_uuid,
829 row.provenance_uuid,
830 row.recorded_at_micros,
831 )?;
832 if row.contract_version != HYPOTHESIS_MEMBERSHIP_CONTRACT_VERSION {
833 return Err(invalid(
834 "hypothesis_membership.contract_version",
835 "unsupported version",
836 ));
837 }
838 if !ids.insert(row.membership_event_uuid) {
839 return Err(KnowledgeError::Duplicate("membership_event_uuid"));
840 }
841 }
842 ids.clear();
843 for row in selection {
844 count_row_validation();
845 HypothesisSelectionEvent::new(
846 row.selection_event_uuid,
847 row.operation_uuid,
848 row.group_uuid,
849 row.selected_assertion_uuid,
850 row.reasoning_uuid,
851 row.provenance_uuid,
852 row.recorded_at_micros,
853 )?;
854 if row.contract_version != HYPOTHESIS_SELECTION_CONTRACT_VERSION {
855 return Err(invalid(
856 "hypothesis_selection.contract_version",
857 "unsupported version",
858 ));
859 }
860 if !ids.insert(row.selection_event_uuid) {
861 return Err(KnowledgeError::Duplicate("selection_event_uuid"));
862 }
863 }
864 Ok(())
865}
866
867fn validate_question_key_uniqueness(groups: &[HypothesisGroup]) -> Result<(), KnowledgeError> {
868 let mut keys = HashSet::with_capacity(groups.len());
869 for row in groups {
870 if !keys.insert(row.question_key.as_str()) {
871 return Err(KnowledgeError::Duplicate("question_key"));
872 }
873 }
874 Ok(())
875}
876
877fn validate_row_count(
878 participant: &'static str,
879 observed: usize,
880 limit: usize,
881) -> Result<(), KnowledgeError> {
882 if observed > limit {
883 return Err(KnowledgeError::Limit {
884 participant,
885 observed,
886 limit,
887 });
888 }
889 Ok(())
890}
891
892#[inline]
893fn count_row_validation() {
894 #[cfg(test)]
895 ROW_VALIDATION_CALLS.with(|calls| {
896 if let Some(count) = calls.get() {
897 calls.set(Some(count + 1));
898 }
899 });
900}
901
902fn validate_state(
903 groups: &[HypothesisGroup],
904 membership: &[HypothesisMembershipEvent],
905 selection: &[HypothesisSelectionEvent],
906) -> Result<(), KnowledgeError> {
907 let group_times = groups
908 .iter()
909 .map(|row| (row.group_uuid, row.recorded_at_micros))
910 .collect::<HashMap<_, _>>();
911 let mut members = HashMap::<Uuid, HashSet<Uuid>>::new();
912 let mut selected = HashMap::<Uuid, Option<Uuid>>::new();
913 for (time, _, operation_uuid) in operation_keys(membership, selection) {
914 let selected_before = selected.clone();
915 let membership_at_time = membership
916 .iter()
917 .filter(|row| row.recorded_at_micros == time && row.operation_uuid == operation_uuid)
918 .collect::<Vec<_>>();
919 let selection_at_time = selection
920 .iter()
921 .filter(|row| row.recorded_at_micros == time && row.operation_uuid == operation_uuid)
922 .collect::<Vec<_>>();
923 for event in &membership_at_time {
924 let group_time = group_times
925 .get(&event.group_uuid)
926 .ok_or(KnowledgeError::Dangling("group_uuid"))?;
927 if event.recorded_at_micros < *group_time {
928 return Err(invalid(
929 "hypothesis_membership.recorded_at",
930 "cannot predate its hypothesis group",
931 ));
932 }
933 let state = members.entry(event.group_uuid).or_default();
934 match event.action {
935 HypothesisMembershipAction::Added if !state.insert(event.assertion_uuid) => {
936 return Err(invalid(
937 "hypothesis_membership.action",
938 "cannot add a current member",
939 ));
940 }
941 HypothesisMembershipAction::Removed if !state.remove(&event.assertion_uuid) => {
942 return Err(invalid(
943 "hypothesis_membership.action",
944 "cannot remove a non-member",
945 ));
946 }
947 _ => {}
948 }
949 }
950 for event in &selection_at_time {
951 let group_time = group_times
952 .get(&event.group_uuid)
953 .ok_or(KnowledgeError::Dangling("group_uuid"))?;
954 if event.recorded_at_micros < *group_time {
955 return Err(invalid(
956 "hypothesis_selection.recorded_at",
957 "cannot predate its hypothesis group",
958 ));
959 }
960 if let Some(assertion_uuid) = event.selected_assertion_uuid
961 && !members
962 .get(&event.group_uuid)
963 .is_some_and(|state| state.contains(&assertion_uuid))
964 {
965 return Err(invalid(
966 "hypothesis_selection.selected_assertion_uuid",
967 "selected assertion must be a current member",
968 ));
969 }
970 selected.insert(event.group_uuid, event.selected_assertion_uuid);
971 }
972 for removal in membership_at_time
973 .iter()
974 .filter(|row| row.action == HypothesisMembershipAction::Removed)
975 {
976 if selected_before
977 .get(&removal.group_uuid)
978 .copied()
979 .flatten()
980 .is_some_and(|id| id == removal.assertion_uuid)
981 {
982 let paired = selection_at_time.iter().any(|event| {
983 event.group_uuid == removal.group_uuid
984 && event.operation_uuid == removal.operation_uuid
985 && event.selected_assertion_uuid != Some(removal.assertion_uuid)
986 });
987 if !paired {
988 return Err(invalid(
989 "hypothesis_membership.action",
990 "selected-member removal requires a paired explicit selection event",
991 ));
992 }
993 }
994 }
995 }
996 Ok(())
997}
998
999fn operation_keys(
1000 membership: &[HypothesisMembershipEvent],
1001 selection: &[HypothesisSelectionEvent],
1002) -> Vec<(i64, Uuid, Uuid)> {
1003 let mut operations = HashMap::<(i64, Uuid), Uuid>::new();
1004 for (time, operation, event) in membership
1005 .iter()
1006 .map(|row| {
1007 (
1008 row.recorded_at_micros,
1009 row.operation_uuid,
1010 row.membership_event_uuid,
1011 )
1012 })
1013 .chain(selection.iter().map(|row| {
1014 (
1015 row.recorded_at_micros,
1016 row.operation_uuid,
1017 row.selection_event_uuid,
1018 )
1019 }))
1020 {
1021 operations
1022 .entry((time, operation))
1023 .and_modify(|first| *first = (*first).min(event))
1024 .or_insert(event);
1025 }
1026 let mut keys = operations
1027 .into_iter()
1028 .map(|((time, operation), first_event)| (time, first_event, operation))
1029 .collect::<Vec<_>>();
1030 keys.sort_unstable();
1031 keys
1032}
1033
1034fn merge_rows<T, F>(
1035 existing: &[T],
1036 staged: &[T],
1037 id: F,
1038 field: &'static str,
1039 participant: &'static str,
1040 row_limit: usize,
1041) -> Result<Vec<T>, KnowledgeError>
1042where
1043 T: Clone + Eq,
1044 F: Fn(&T) -> Uuid,
1045{
1046 validate_row_count(participant, existing.len(), row_limit)?;
1047 let mut rows = existing.to_vec();
1048 let mut by_id = rows
1049 .iter()
1050 .cloned()
1051 .map(|row| (id(&row), row))
1052 .collect::<HashMap<_, _>>();
1053 for row in staged {
1054 if let Some(current) = by_id.get(&id(row)) {
1055 if current != row {
1056 return Err(KnowledgeError::Conflict(field));
1057 }
1058 } else {
1059 validate_row_count(participant, rows.len().saturating_add(1), row_limit)?;
1060 rows.push(row.clone());
1061 by_id.insert(id(row), row.clone());
1062 }
1063 }
1064 Ok(rows)
1065}
1066
1067fn append_uuid(
1068 builder: &mut FixedSizeBinaryBuilder,
1069 value: Uuid,
1070 field: &'static str,
1071) -> Result<(), KnowledgeError> {
1072 builder
1073 .append_value(value.as_bytes())
1074 .map_err(|_| invalid(field, "invalid UUID width"))
1075}
1076
1077fn record_batch(
1078 schema: SchemaRef,
1079 columns: Vec<Arc<dyn arrow::array::Array>>,
1080) -> Result<RecordBatch, KnowledgeError> {
1081 RecordBatch::try_new(schema, columns)
1082 .map_err(|_| invalid("hypothesis", "Arrow batch construction failed"))
1083}
1084
1085fn require_schema(
1086 batch: &RecordBatch,
1087 schema: &SchemaRef,
1088 field: &'static str,
1089) -> Result<(), KnowledgeError> {
1090 if batch.schema().as_ref() != schema.as_ref() {
1091 return Err(invalid(field, "schema mismatch"));
1092 }
1093 Ok(())
1094}
1095
1096fn fixed<'a>(
1097 batch: &'a RecordBatch,
1098 name: &'static str,
1099) -> Result<&'a FixedSizeBinaryArray, KnowledgeError> {
1100 batch
1101 .column_by_name(name)
1102 .and_then(|value| value.as_any().downcast_ref())
1103 .ok_or_else(|| invalid(name, "column type mismatch"))
1104}
1105
1106fn strings<'a>(
1107 batch: &'a RecordBatch,
1108 name: &'static str,
1109) -> Result<&'a StringArray, KnowledgeError> {
1110 batch
1111 .column_by_name(name)
1112 .and_then(|value| value.as_any().downcast_ref())
1113 .ok_or_else(|| invalid(name, "column type mismatch"))
1114}
1115
1116fn timestamps(batch: &RecordBatch) -> Result<&TimestampMicrosecondArray, KnowledgeError> {
1117 batch
1118 .column_by_name("recorded_at")
1119 .and_then(|value| value.as_any().downcast_ref())
1120 .ok_or_else(|| invalid("recorded_at", "column type mismatch"))
1121}
1122
1123fn versions(batch: &RecordBatch) -> Result<&UInt32Array, KnowledgeError> {
1124 batch
1125 .column_by_name("contract_version")
1126 .and_then(|value| value.as_any().downcast_ref())
1127 .ok_or_else(|| invalid("contract_version", "column type mismatch"))
1128}
1129
1130fn uuid_at(
1131 values: &FixedSizeBinaryArray,
1132 row: usize,
1133 field: &'static str,
1134) -> Result<Uuid, KnowledgeError> {
1135 if values.is_null(row) {
1136 return Err(invalid(field, "unexpected null UUID"));
1137 }
1138 Uuid::from_slice(values.value(row)).map_err(|_| invalid(field, "invalid UUID bytes"))
1139}
1140
1141pub(crate) fn schema_registry_entries() -> [SchemaRegistryEntry; 3] {
1142 [
1143 entry(
1144 "hypothesis_groups",
1145 HYPOTHESIS_GROUP_CONTRACT_VERSION,
1146 Arc::clone(&HYPOTHESIS_GROUP_SCHEMA),
1147 *GROUP_SCHEMA_FINGERPRINT,
1148 &["recorded_at", "group_uuid"],
1149 (&["group_uuid"], "group_uuid"),
1150 &[("hypothesis_key_policy", HYPOTHESIS_KEY_POLICY_VERSION)],
1151 ),
1152 entry(
1153 "hypothesis_membership_events",
1154 HYPOTHESIS_MEMBERSHIP_CONTRACT_VERSION,
1155 Arc::clone(&HYPOTHESIS_MEMBERSHIP_SCHEMA),
1156 *MEMBERSHIP_SCHEMA_FINGERPRINT,
1157 &["recorded_at", "membership_event_uuid"],
1158 (&["membership_event_uuid"], "membership_event_uuid"),
1159 &[
1160 ("hypothesis_state_policy", HYPOTHESIS_STATE_POLICY_VERSION),
1161 ("membership_action", 1),
1162 ],
1163 ),
1164 entry(
1165 "hypothesis_selection_events",
1166 HYPOTHESIS_SELECTION_CONTRACT_VERSION,
1167 Arc::clone(&HYPOTHESIS_SELECTION_SCHEMA),
1168 *SELECTION_SCHEMA_FINGERPRINT,
1169 &["recorded_at", "selection_event_uuid"],
1170 (&["selection_event_uuid"], "selection_event_uuid"),
1171 &[("hypothesis_state_policy", HYPOTHESIS_STATE_POLICY_VERSION)],
1172 ),
1173 ]
1174}
1175
1176fn entry(
1177 family: &'static str,
1178 version: u32,
1179 schema: SchemaRef,
1180 schema_fingerprint: [u8; 32],
1181 sort_key: &'static [&'static str],
1182 diff_identity: (&'static [&'static str], &'static str),
1183 enums: &'static [(&'static str, u32)],
1184) -> SchemaRegistryEntry {
1185 SchemaRegistryEntry {
1186 capability_id: "epistemic",
1187 capability_version: EPISTEMIC_CAPABILITY_VERSION,
1188 record_family: family,
1189 record_version: version,
1190 schema,
1191 schema_fingerprint,
1192 enum_registry_versions: enums,
1193 sort_key,
1194 diff_identity_fields: diff_identity.0,
1195 diff_record_uuid_field: Some(diff_identity.1),
1196 fingerprint_domain: CanonicalDomain::Schema,
1197 owner: "graphforge-knowledge",
1198 implementation_issue: 779,
1199 max_rows: MAX_KNOWLEDGE_ROWS,
1200 }
1201}
1202
1203fn uuid_field(name: &str, nullable: bool) -> Field {
1204 Field::new(name, DataType::FixedSizeBinary(16), nullable)
1205}
1206
1207fn timestamp_field(name: &str) -> Field {
1208 Field::new(
1209 name,
1210 DataType::Timestamp(TimeUnit::Microsecond, Some("UTC".into())),
1211 false,
1212 )
1213}
1214
1215fn require_v7(value: Uuid, field: &'static str) -> Result<(), KnowledgeError> {
1216 if value.get_version() != Some(Version::SortRand) {
1217 return Err(invalid(field, "must be UUIDv7"));
1218 }
1219 require_uuid(value, field)
1220}
1221
1222fn require_uuid(value: Uuid, field: &'static str) -> Result<(), KnowledgeError> {
1223 if value.is_nil() {
1224 return Err(invalid(field, "must not be nil"));
1225 }
1226 Ok(())
1227}
1228
1229const fn invalid(field: &'static str, message: &'static str) -> KnowledgeError {
1230 KnowledgeError::Invalid { field, message }
1231}
1232
1233#[cfg(test)]
1234mod tests {
1235 use super::*;
1236 use std::time::{Duration, Instant};
1237
1238 fn invalid_code(error: &KnowledgeError) -> &'static str {
1239 match error {
1240 KnowledgeError::Invalid { .. } => "invalid",
1241 KnowledgeError::Duplicate(_) => "duplicate",
1242 KnowledgeError::Dangling(_) => "dangling",
1243 KnowledgeError::Conflict(_) => "conflict",
1244 _ => "other",
1245 }
1246 }
1247
1248 fn invalid_field(error: &KnowledgeError) -> Option<&'static str> {
1249 match error {
1250 KnowledgeError::Invalid { field, .. } => Some(field),
1251 _ => None,
1252 }
1253 }
1254
1255 fn uuid7(seed: u8) -> Uuid {
1256 let mut bytes = [seed; 16];
1257 bytes[6] = (bytes[6] & 0x0f) | 0x70;
1258 bytes[8] = (bytes[8] & 0x3f) | 0x80;
1259 Uuid::from_bytes(bytes)
1260 }
1261
1262 fn measurement_uuid(seed: u64) -> Uuid {
1263 let mut bytes = [0; 16];
1264 bytes[8..].copy_from_slice(&seed.to_be_bytes());
1265 bytes[6] = 0x70;
1266 bytes[8] = (bytes[8] & 0x3f) | 0x80;
1267 Uuid::from_bytes(bytes)
1268 }
1269
1270 fn measurement_group(seed: u64) -> HypothesisGroup {
1271 HypothesisGroup {
1272 group_uuid: measurement_uuid(seed + 1),
1273 question_key: format!("question.{seed:06}"),
1274 provenance_uuid: measurement_uuid(seed + 1_000_000),
1275 recorded_at_micros: i64::try_from(seed).unwrap(),
1276 contract_version: HYPOTHESIS_GROUP_CONTRACT_VERSION,
1277 }
1278 }
1279
1280 fn merge_with_full_revalidation(
1281 existing: &HypothesisLedger,
1282 staged: &HypothesisLedger,
1283 ) -> Result<HypothesisLedger, KnowledgeError> {
1284 HypothesisLedger::new(
1285 merge_rows(
1286 &existing.groups,
1287 &staged.groups,
1288 |row| row.group_uuid,
1289 "group_uuid",
1290 "hypothesis_groups",
1291 MAX_KNOWLEDGE_ROWS,
1292 )?,
1293 merge_rows(
1294 &existing.membership_events,
1295 &staged.membership_events,
1296 |row| row.membership_event_uuid,
1297 "membership_event_uuid",
1298 "hypothesis_membership_events",
1299 MAX_KNOWLEDGE_ROWS,
1300 )?,
1301 merge_rows(
1302 &existing.selection_events,
1303 &staged.selection_events,
1304 |row| row.selection_event_uuid,
1305 "selection_event_uuid",
1306 "hypothesis_selection_events",
1307 MAX_KNOWLEDGE_ROWS,
1308 )?,
1309 )
1310 }
1311
1312 fn measurement_input_digest(existing: &HypothesisLedger, staged: &HypothesisLedger) -> String {
1313 let mut writer = CanonicalWriter::new();
1314 for ledger in [existing, staged] {
1315 writer.u64(ledger.groups.len() as u64).unwrap();
1316 for row in &ledger.groups {
1317 writer.raw(row.group_uuid.as_bytes()).unwrap();
1318 writer.text(&row.question_key).unwrap();
1319 writer.raw(row.provenance_uuid.as_bytes()).unwrap();
1320 writer.i64(row.recorded_at_micros).unwrap();
1321 writer.u32(row.contract_version).unwrap();
1322 }
1323 }
1324 fingerprint(
1325 CanonicalDomain::HypothesisGroup,
1326 CANONICAL_CONTRACT_VERSION,
1327 &writer.finish(),
1328 )
1329 .unwrap()
1330 .iter()
1331 .map(|byte| format!("{byte:02x}"))
1332 .collect()
1333 }
1334
1335 fn median(mut values: Vec<Duration>) -> Duration {
1336 values.sort_unstable();
1337 values[values.len() / 2]
1338 }
1339
1340 #[test]
1341 #[ignore = "manual quiet-host before/after merge-cost measurement"]
1342 fn quiet_host_merge_cost_measurement() {
1343 for existing_count in [10_000_u64, 100_000] {
1344 let existing = HypothesisLedger::new(
1345 (0..existing_count).map(measurement_group).collect(),
1346 vec![],
1347 vec![],
1348 )
1349 .unwrap();
1350 let staged =
1351 HypothesisLedger::new(vec![measurement_group(existing_count)], vec![], vec![])
1352 .unwrap();
1353 let repetitions = 9;
1354 let mut before = Vec::with_capacity(repetitions);
1355 let mut after = Vec::with_capacity(repetitions);
1356 for _ in 0..repetitions {
1357 let start = Instant::now();
1358 std::hint::black_box(merge_with_full_revalidation(&existing, &staged).unwrap());
1359 before.push(start.elapsed());
1360
1361 let start = Instant::now();
1362 std::hint::black_box(existing.merge(&staged).unwrap());
1363 after.push(start.elapsed());
1364 }
1365 let before_median = median(before);
1366 let after_median = median(after);
1367 let divisor = existing_count as f64;
1368 println!(
1369 "existing_rows={existing_count} staged_rows=1 repetitions={repetitions} input_sha256={} before_ns={} before_ns_per_existing_row={:.3} after_ns={} after_ns_per_existing_row={:.3}",
1370 measurement_input_digest(&existing, &staged),
1371 before_median.as_nanos(),
1372 before_median.as_nanos() as f64 / divisor,
1373 after_median.as_nanos(),
1374 after_median.as_nanos() as f64 / divisor,
1375 );
1376 }
1377 }
1378
1379 fn group() -> HypothesisGroup {
1380 HypothesisGroup::new(uuid7(1), "cause.primary".into(), uuid7(2), 1).unwrap()
1381 }
1382
1383 fn member(
1384 id: u8,
1385 operation: u8,
1386 assertion: u8,
1387 action: HypothesisMembershipAction,
1388 time: i64,
1389 ) -> HypothesisMembershipEvent {
1390 HypothesisMembershipEvent::new(
1391 uuid7(id),
1392 uuid7(operation),
1393 uuid7(1),
1394 uuid7(assertion),
1395 action,
1396 uuid7(id.wrapping_add(40)),
1397 uuid7(id.wrapping_add(80)),
1398 time,
1399 )
1400 .unwrap()
1401 }
1402
1403 fn selection(
1404 id: u8,
1405 operation: u8,
1406 selected: Option<u8>,
1407 time: i64,
1408 ) -> HypothesisSelectionEvent {
1409 HypothesisSelectionEvent::new(
1410 uuid7(id),
1411 uuid7(operation),
1412 uuid7(1),
1413 selected.map(uuid7),
1414 uuid7(id.wrapping_add(40)),
1415 uuid7(id.wrapping_add(80)),
1416 time,
1417 )
1418 .unwrap()
1419 }
1420
1421 #[test]
1422 fn keys_are_exact_nfc_bounded_and_unique() {
1423 assert!(HypothesisGroup::new(uuid7(1), String::new(), uuid7(2), 1).is_err());
1424 assert!(HypothesisGroup::new(uuid7(1), " key".into(), uuid7(2), 1).is_err());
1425 assert!(HypothesisGroup::new(uuid7(1), "key ".into(), uuid7(2), 1).is_err());
1426 assert!(HypothesisGroup::new(uuid7(1), "e\u{301}".into(), uuid7(2), 1).is_err());
1427 assert!(
1428 HypothesisGroup::new(
1429 uuid7(1),
1430 "k".repeat(MAX_HYPOTHESIS_QUESTION_KEY_BYTES + 1),
1431 uuid7(2),
1432 1,
1433 )
1434 .is_err()
1435 );
1436 let upper = HypothesisGroup::new(uuid7(3), "Cause".into(), uuid7(4), 1).unwrap();
1437 let lower = HypothesisGroup::new(uuid7(5), "cause".into(), uuid7(6), 1).unwrap();
1438 assert!(HypothesisLedger::new(vec![upper, lower], vec![], vec![]).is_ok());
1439 let duplicate =
1440 HypothesisGroup::new(uuid7(7), "cause.primary".into(), uuid7(8), 1).unwrap();
1441 assert!(HypothesisLedger::new(vec![group(), duplicate], vec![], vec![]).is_err());
1442 }
1443
1444 #[test]
1445 fn add_select_change_clear_and_selected_removal_are_explicit() {
1446 let ledger = HypothesisLedger::new(
1447 vec![group()],
1448 vec![
1449 member(10, 20, 30, HypothesisMembershipAction::Added, 2),
1450 member(11, 21, 31, HypothesisMembershipAction::Added, 3),
1451 member(12, 22, 30, HypothesisMembershipAction::Removed, 5),
1452 ],
1453 vec![
1454 selection(13, 23, Some(30), 4),
1455 selection(14, 22, Some(31), 5),
1456 selection(15, 24, None, 6),
1457 ],
1458 )
1459 .unwrap();
1460 assert_eq!(ledger.current_members(uuid7(1)), vec![uuid7(31)]);
1461 assert_eq!(ledger.current_selection(uuid7(1)), None);
1462 assert_eq!(
1463 HypothesisLedger::from_batches(
1464 &[ledger.group_batch().unwrap()],
1465 &[ledger.membership_batch().unwrap()],
1466 &[ledger.selection_batch().unwrap()],
1467 )
1468 .unwrap(),
1469 ledger
1470 );
1471 assert_eq!(ledger.merge(&ledger).unwrap(), ledger);
1472
1473 assert!(
1474 HypothesisLedger::new(
1475 vec![group()],
1476 vec![
1477 member(10, 20, 30, HypothesisMembershipAction::Added, 2),
1478 member(12, 22, 30, HypothesisMembershipAction::Removed, 4),
1479 ],
1480 vec![selection(13, 23, Some(30), 3)],
1481 )
1482 .is_err()
1483 );
1484 }
1485
1486 #[test]
1487 fn canonical_fingerprints_are_exact_order_independent_and_round_trip_stable() {
1488 assert_eq!(
1489 CanonicalDomain::HypothesisGroup.as_str(),
1490 "graphforge/hypothesis-group"
1491 );
1492 assert_eq!(
1493 CanonicalDomain::HypothesisMembership.as_str(),
1494 "graphforge/hypothesis-membership"
1495 );
1496 assert_eq!(
1497 CanonicalDomain::HypothesisSelection.as_str(),
1498 "graphforge/hypothesis-selection"
1499 );
1500 let membership = member(10, 20, 30, HypothesisMembershipAction::Added, 2);
1501 let other = member(11, 21, 31, HypothesisMembershipAction::Added, 3);
1502 let selected = selection(12, 22, Some(30), 4);
1503 let cleared = selection(13, 23, None, 5);
1504 let ledger = HypothesisLedger::new(
1505 vec![group()],
1506 vec![membership.clone(), other.clone()],
1507 vec![selected.clone(), cleared.clone()],
1508 )
1509 .unwrap();
1510 let reordered = HypothesisLedger::new(
1511 vec![group()],
1512 vec![other, membership.clone()],
1513 vec![cleared.clone(), selected.clone()],
1514 )
1515 .unwrap();
1516 let decoded = HypothesisLedger::from_batches(
1517 &[ledger.group_batch().unwrap()],
1518 &[ledger.membership_batch().unwrap()],
1519 &[ledger.selection_batch().unwrap()],
1520 )
1521 .unwrap();
1522 let fingerprints = [
1523 ledger.group_fingerprint(uuid7(1)).unwrap(),
1524 ledger
1525 .membership_fingerprint(membership.membership_event_uuid)
1526 .unwrap(),
1527 ledger
1528 .selection_fingerprint(selected.selection_event_uuid)
1529 .unwrap(),
1530 ledger
1531 .selection_fingerprint(cleared.selection_event_uuid)
1532 .unwrap(),
1533 ];
1534 let hex = |value: [u8; 32]| {
1535 value
1536 .iter()
1537 .map(|byte| format!("{byte:02x}"))
1538 .collect::<String>()
1539 };
1540 assert_eq!(
1541 hex(fingerprints[0]),
1542 "336fe80f4e94c5758b5e77de06c9c5b6ebf51ff20355c5d6950ab3e648991265"
1543 );
1544 assert_eq!(
1545 hex(fingerprints[1]),
1546 "fc4fe4882c1469a3f4606c5352028a63d43d369301e26ac0216373889945e403"
1547 );
1548 assert_eq!(
1549 hex(fingerprints[2]),
1550 "ba0ec00b86950fd8b60afaa40327f240377f8fd1695a54dad1d51cbf45665615"
1551 );
1552 assert_eq!(
1553 hex(fingerprints[3]),
1554 "fc220def4f2a61fb134f5a414a257d50cb29a1aea364464831f89b7747e44f9b"
1555 );
1556 assert_eq!(
1557 reordered.group_fingerprint(uuid7(1)).unwrap(),
1558 fingerprints[0]
1559 );
1560 assert_eq!(
1561 reordered
1562 .membership_fingerprint(membership.membership_event_uuid)
1563 .unwrap(),
1564 fingerprints[1]
1565 );
1566 assert_eq!(
1567 reordered
1568 .selection_fingerprint(selected.selection_event_uuid)
1569 .unwrap(),
1570 fingerprints[2]
1571 );
1572 assert_eq!(
1573 reordered
1574 .selection_fingerprint(cleared.selection_event_uuid)
1575 .unwrap(),
1576 fingerprints[3]
1577 );
1578 assert_eq!(
1579 decoded.group_fingerprint(uuid7(1)).unwrap(),
1580 fingerprints[0]
1581 );
1582 assert_eq!(
1583 decoded
1584 .membership_fingerprint(membership.membership_event_uuid)
1585 .unwrap(),
1586 fingerprints[1]
1587 );
1588 assert_eq!(
1589 decoded
1590 .selection_fingerprint(selected.selection_event_uuid)
1591 .unwrap(),
1592 fingerprints[2]
1593 );
1594 assert_eq!(
1595 decoded
1596 .selection_fingerprint(cleared.selection_event_uuid)
1597 .unwrap(),
1598 fingerprints[3]
1599 );
1600 }
1601
1602 #[test]
1603 fn selecting_nonmember_and_invalid_membership_transitions_fail() {
1604 assert!(
1605 HypothesisLedger::new(vec![group()], vec![], vec![selection(10, 20, Some(30), 2)],)
1606 .is_err()
1607 );
1608 assert!(
1609 HypothesisLedger::new(
1610 vec![group()],
1611 vec![member(10, 20, 30, HypothesisMembershipAction::Removed, 2)],
1612 vec![],
1613 )
1614 .is_err()
1615 );
1616 assert!(
1617 HypothesisLedger::new(
1618 vec![group()],
1619 vec![
1620 member(10, 20, 30, HypothesisMembershipAction::Added, 2),
1621 member(11, 21, 30, HypothesisMembershipAction::Added, 3),
1622 ],
1623 vec![],
1624 )
1625 .is_err()
1626 );
1627 }
1628
1629 #[test]
1630 fn wave12_contract_versions_and_duplicate_identities_fail_closed() {
1631 let mut bad_group_version = group();
1632 bad_group_version.contract_version = 2;
1633 assert_eq!(
1634 invalid_code(
1635 &HypothesisLedger::new(vec![bad_group_version], vec![], vec![]).unwrap_err()
1636 ),
1637 "invalid"
1638 );
1639 assert_eq!(
1640 invalid_code(
1641 &HypothesisLedger::new(vec![group(), group()], vec![], vec![]).unwrap_err()
1642 ),
1643 "duplicate"
1644 );
1645
1646 let membership = member(10, 20, 30, HypothesisMembershipAction::Added, 2);
1647 let mut bad_membership_version = membership.clone();
1648 bad_membership_version.contract_version = 2;
1649 assert_eq!(
1650 invalid_code(
1651 &HypothesisLedger::new(vec![group()], vec![bad_membership_version], vec![])
1652 .unwrap_err()
1653 ),
1654 "invalid"
1655 );
1656 assert_eq!(
1657 invalid_code(
1658 &HypothesisLedger::new(vec![group()], vec![membership.clone(), membership], vec![])
1659 .unwrap_err()
1660 ),
1661 "duplicate"
1662 );
1663
1664 let selected = selection(11, 21, None, 3);
1665 let mut bad_selection_version = selected.clone();
1666 bad_selection_version.contract_version = 2;
1667 assert_eq!(
1668 invalid_code(
1669 &HypothesisLedger::new(vec![group()], vec![], vec![bad_selection_version])
1670 .unwrap_err()
1671 ),
1672 "invalid"
1673 );
1674 assert_eq!(
1675 invalid_code(
1676 &HypothesisLedger::new(vec![group()], vec![], vec![selected.clone(), selected])
1677 .unwrap_err()
1678 ),
1679 "duplicate"
1680 );
1681 }
1682
1683 #[test]
1684 fn ledger_rejects_invalid_uuid_fields_in_all_record_families() {
1685 let invalid_uuid = Uuid::from_u128(1);
1686 let mut bad_group_uuid = group();
1687 bad_group_uuid.group_uuid = invalid_uuid;
1688 let mut bad_group_provenance = group();
1689 bad_group_provenance.provenance_uuid = Uuid::nil();
1690 assert_eq!(
1691 invalid_field(
1692 &HypothesisLedger::new(vec![bad_group_uuid], vec![], vec![]).unwrap_err()
1693 ),
1694 Some("group_uuid")
1695 );
1696 assert_eq!(
1697 invalid_field(
1698 &HypothesisLedger::new(vec![bad_group_provenance], vec![], vec![]).unwrap_err()
1699 ),
1700 Some("provenance_uuid")
1701 );
1702
1703 let membership_cases = [
1704 ("membership_event_uuid", {
1705 let mut row = member(10, 20, 30, HypothesisMembershipAction::Added, 2);
1706 row.membership_event_uuid = invalid_uuid;
1707 row
1708 }),
1709 ("operation_uuid", {
1710 let mut row = member(10, 20, 30, HypothesisMembershipAction::Added, 2);
1711 row.operation_uuid = invalid_uuid;
1712 row
1713 }),
1714 ("group_uuid", {
1715 let mut row = member(10, 20, 30, HypothesisMembershipAction::Added, 2);
1716 row.group_uuid = invalid_uuid;
1717 row
1718 }),
1719 ("assertion_uuid", {
1720 let mut row = member(10, 20, 30, HypothesisMembershipAction::Added, 2);
1721 row.assertion_uuid = invalid_uuid;
1722 row
1723 }),
1724 ("reasoning_uuid", {
1725 let mut row = member(10, 20, 30, HypothesisMembershipAction::Added, 2);
1726 row.reasoning_uuid = invalid_uuid;
1727 row
1728 }),
1729 ("provenance_uuid", {
1730 let mut row = member(10, 20, 30, HypothesisMembershipAction::Added, 2);
1731 row.provenance_uuid = Uuid::nil();
1732 row
1733 }),
1734 ];
1735 for (expected_field, row) in membership_cases {
1736 assert_eq!(
1737 invalid_field(
1738 &HypothesisLedger::new(vec![group()], vec![row], vec![]).unwrap_err()
1739 ),
1740 Some(expected_field)
1741 );
1742 }
1743
1744 let selection_cases = [
1745 ("selection_event_uuid", {
1746 let mut row = selection(10, 20, None, 2);
1747 row.selection_event_uuid = invalid_uuid;
1748 row
1749 }),
1750 ("operation_uuid", {
1751 let mut row = selection(10, 20, None, 2);
1752 row.operation_uuid = invalid_uuid;
1753 row
1754 }),
1755 ("group_uuid", {
1756 let mut row = selection(10, 20, None, 2);
1757 row.group_uuid = invalid_uuid;
1758 row
1759 }),
1760 ("selected_assertion_uuid", {
1761 let mut row = selection(10, 20, None, 2);
1762 row.selected_assertion_uuid = Some(invalid_uuid);
1763 row
1764 }),
1765 ("reasoning_uuid", {
1766 let mut row = selection(10, 20, None, 2);
1767 row.reasoning_uuid = invalid_uuid;
1768 row
1769 }),
1770 ("provenance_uuid", {
1771 let mut row = selection(10, 20, None, 2);
1772 row.provenance_uuid = Uuid::nil();
1773 row
1774 }),
1775 ];
1776 for (expected_field, row) in selection_cases {
1777 assert_eq!(
1778 invalid_field(
1779 &HypothesisLedger::new(vec![group()], vec![], vec![row]).unwrap_err()
1780 ),
1781 Some(expected_field)
1782 );
1783 }
1784 }
1785
1786 #[test]
1787 fn wave12_dangling_and_temporally_invalid_events_are_rejected() {
1788 let dangling_member = HypothesisMembershipEvent::new(
1789 uuid7(10),
1790 uuid7(20),
1791 uuid7(99),
1792 uuid7(30),
1793 HypothesisMembershipAction::Added,
1794 uuid7(40),
1795 uuid7(50),
1796 2,
1797 )
1798 .unwrap();
1799 assert_eq!(
1800 invalid_code(
1801 &HypothesisLedger::new(vec![group()], vec![dangling_member], vec![]).unwrap_err()
1802 ),
1803 "dangling"
1804 );
1805 let early_member = member(11, 21, 31, HypothesisMembershipAction::Added, 0);
1806 assert_eq!(
1807 invalid_code(
1808 &HypothesisLedger::new(vec![group()], vec![early_member], vec![]).unwrap_err()
1809 ),
1810 "invalid"
1811 );
1812
1813 let dangling_selection = HypothesisSelectionEvent::new(
1814 uuid7(12),
1815 uuid7(22),
1816 uuid7(99),
1817 None,
1818 uuid7(42),
1819 uuid7(52),
1820 2,
1821 )
1822 .unwrap();
1823 assert_eq!(
1824 invalid_code(
1825 &HypothesisLedger::new(vec![group()], vec![], vec![dangling_selection])
1826 .unwrap_err()
1827 ),
1828 "dangling"
1829 );
1830 let early_selection = selection(13, 23, None, 0);
1831 assert_eq!(
1832 invalid_code(
1833 &HypothesisLedger::new(vec![group()], vec![], vec![early_selection]).unwrap_err()
1834 ),
1835 "invalid"
1836 );
1837 }
1838
1839 #[test]
1840 fn wave12_merge_conflicts_and_arrow_decode_errors_are_structured() {
1841 let base = HypothesisLedger::new(vec![group()], vec![], vec![]).unwrap();
1842 let conflicting_group =
1843 HypothesisGroup::new(uuid7(1), "different.question".into(), uuid7(2), 1).unwrap();
1844 let staged = HypothesisLedger::new(vec![conflicting_group], vec![], vec![]).unwrap();
1845 assert_eq!(invalid_code(&base.merge(&staged).unwrap_err()), "conflict");
1846
1847 let wrong_schema = RecordBatch::new_empty(Arc::new(Schema::empty()));
1848 assert_eq!(
1849 invalid_code(&HypothesisLedger::from_batches(&[wrong_schema], &[], &[]).unwrap_err()),
1850 "invalid"
1851 );
1852
1853 let ledger = HypothesisLedger::new(
1854 vec![group()],
1855 vec![member(10, 20, 30, HypothesisMembershipAction::Added, 2)],
1856 vec![],
1857 )
1858 .unwrap();
1859 let batch = ledger.membership_batch().unwrap();
1860 let mut columns = batch.columns().to_vec();
1861 columns[4] = Arc::new(StringArray::from(vec!["unknown"]));
1862 let unknown_action = RecordBatch::try_new(batch.schema(), columns).unwrap();
1863 assert_eq!(
1864 invalid_code(
1865 &HypothesisLedger::from_batches(
1866 &[ledger.group_batch().unwrap()],
1867 &[unknown_action],
1868 &[]
1869 )
1870 .unwrap_err()
1871 ),
1872 "invalid"
1873 );
1874 }
1875
1876 #[test]
1877 fn merge_skips_row_validation_and_replays_coalesced_operation_history() {
1878 let added = member(10, 20, 30, HypothesisMembershipAction::Added, 2);
1879 let base = HypothesisLedger::new(vec![group()], vec![added.clone()], vec![]).unwrap();
1880 let staged = HypothesisLedger::new(
1881 vec![group()],
1882 vec![added],
1883 vec![selection(9, 20, Some(30), 2)],
1884 )
1885 .unwrap();
1886
1887 ROW_VALIDATION_CALLS.with(|calls| calls.set(Some(0)));
1888 let merged = base.merge(&staged).unwrap();
1889 ROW_VALIDATION_CALLS.with(|calls| {
1890 assert_eq!(calls.get(), Some(0));
1891 calls.set(None);
1892 });
1893
1894 assert_eq!(merged.groups(), base.groups());
1895 assert_eq!(merged.membership_events(), base.membership_events());
1896 assert_eq!(merged.current_members(uuid7(1)), vec![uuid7(30)]);
1897 assert_eq!(merged.current_selection(uuid7(1)), Some(uuid7(30)));
1898 assert_eq!(merged.selection_events().len(), 1);
1899 }
1900
1901 #[test]
1902 fn same_time_distinct_operations_retain_their_event_order() {
1903 let added = member(40, 22, 30, HypothesisMembershipAction::Added, 2);
1904 let selected = selection(10, 21, Some(30), 2);
1905 assert!(HypothesisLedger::new(vec![group()], vec![added], vec![selected]).is_err());
1906
1907 let ordered_operations = HypothesisLedger::new(
1908 vec![group()],
1909 vec![
1910 member(40, 22, 30, HypothesisMembershipAction::Added, 2),
1911 member(70, 22, 31, HypothesisMembershipAction::Added, 2),
1912 member(50, 23, 30, HypothesisMembershipAction::Removed, 2),
1913 ],
1914 vec![],
1915 )
1916 .unwrap();
1917 assert_eq!(
1918 ordered_operations.current_members(uuid7(1)),
1919 vec![uuid7(31)]
1920 );
1921 }
1922
1923 #[test]
1924 fn merge_sorts_all_appended_families_before_state_replay() {
1925 let original_group =
1926 HypothesisGroup::new(uuid7(1), "cause.primary".into(), uuid7(2), 50).unwrap();
1927 let existing_member = member(20, 30, 40, HypothesisMembershipAction::Added, 70);
1928 let existing_selection = selection(31, 41, None, 90);
1929 let base = HypothesisLedger::new(
1930 vec![original_group.clone()],
1931 vec![existing_member.clone()],
1932 vec![existing_selection.clone()],
1933 )
1934 .unwrap();
1935 let staged = HypothesisLedger::new(
1936 vec![
1937 original_group,
1938 HypothesisGroup::new(uuid7(3), "cause.other".into(), uuid7(4), 30).unwrap(),
1939 ],
1940 vec![
1941 existing_member,
1942 member(21, 31, 41, HypothesisMembershipAction::Added, 60),
1943 ],
1944 vec![existing_selection, selection(32, 42, None, 80)],
1945 )
1946 .unwrap();
1947
1948 let merged = base.merge(&staged).unwrap();
1949 assert_eq!(
1950 merged
1951 .groups()
1952 .iter()
1953 .map(|row| row.recorded_at_micros)
1954 .collect::<Vec<_>>(),
1955 vec![30, 50]
1956 );
1957 assert_eq!(
1958 merged
1959 .membership_events()
1960 .iter()
1961 .map(|row| row.recorded_at_micros)
1962 .collect::<Vec<_>>(),
1963 vec![60, 70]
1964 );
1965 assert_eq!(
1966 merged
1967 .selection_events()
1968 .iter()
1969 .map(|row| row.recorded_at_micros)
1970 .collect::<Vec<_>>(),
1971 vec![80, 90]
1972 );
1973 }
1974
1975 #[test]
1976 fn merge_replays_membership_transitions_and_selected_removal_pairing() {
1977 let added = member(10, 20, 30, HypothesisMembershipAction::Added, 2);
1978 let selected = selection(11, 21, Some(30), 3);
1979 let base =
1980 HypothesisLedger::new(vec![group()], vec![added.clone()], vec![selected.clone()])
1981 .unwrap();
1982 let removed = member(12, 22, 30, HypothesisMembershipAction::Removed, 4);
1983 let cleared = selection(13, 22, None, 4);
1984 let staged =
1985 HypothesisLedger::new(vec![group()], vec![added, removed], vec![selected, cleared])
1986 .unwrap();
1987
1988 let merged = base.merge(&staged).unwrap();
1989 assert!(merged.current_members(uuid7(1)).is_empty());
1990 assert_eq!(merged.current_selection(uuid7(1)), None);
1991 assert_eq!(merged.membership_events().len(), 2);
1992 assert_eq!(merged.selection_events().len(), 2);
1993 }
1994
1995 #[test]
1996 fn merge_preserves_conflicts_and_canonical_order_for_each_family() {
1997 let group_base = HypothesisLedger::new(vec![group()], vec![], vec![]).unwrap();
1998 let mut changed_group = group();
1999 changed_group.question_key = "cause.changed".into();
2000 let group_staged = HypothesisLedger::new(vec![changed_group], vec![], vec![]).unwrap();
2001 assert!(matches!(
2002 group_base.merge(&group_staged),
2003 Err(KnowledgeError::Conflict("group_uuid"))
2004 ));
2005
2006 let member_base = HypothesisLedger::new(
2007 vec![group()],
2008 vec![member(10, 20, 30, HypothesisMembershipAction::Added, 2)],
2009 vec![],
2010 )
2011 .unwrap();
2012 let member_staged = HypothesisLedger::new(
2013 vec![group()],
2014 vec![member(10, 21, 30, HypothesisMembershipAction::Added, 2)],
2015 vec![],
2016 )
2017 .unwrap();
2018 assert!(matches!(
2019 member_base.merge(&member_staged),
2020 Err(KnowledgeError::Conflict("membership_event_uuid"))
2021 ));
2022
2023 let selection_base =
2024 HypothesisLedger::new(vec![group()], vec![], vec![selection(10, 20, None, 2)]).unwrap();
2025 let selection_staged =
2026 HypothesisLedger::new(vec![group()], vec![], vec![selection(10, 21, None, 2)]).unwrap();
2027 assert!(matches!(
2028 selection_base.merge(&selection_staged),
2029 Err(KnowledgeError::Conflict("selection_event_uuid"))
2030 ));
2031
2032 let groups = HypothesisLedger::new(
2033 vec![
2034 HypothesisGroup::new(uuid7(3), "cause.other".into(), uuid7(4), 30).unwrap(),
2035 group(),
2036 ],
2037 vec![],
2038 vec![],
2039 )
2040 .unwrap();
2041 let staged = HypothesisLedger::new(
2042 vec![
2043 HypothesisGroup::new(uuid7(5), "cause.third".into(), uuid7(6), 20).unwrap(),
2044 HypothesisGroup::new(uuid7(7), "cause.fourth".into(), uuid7(8), 10).unwrap(),
2045 ],
2046 vec![],
2047 vec![],
2048 )
2049 .unwrap();
2050 let merged = groups.merge(&staged).unwrap();
2051 assert_eq!(
2052 merged
2053 .groups()
2054 .iter()
2055 .map(|row| row.recorded_at_micros)
2056 .collect::<Vec<_>>(),
2057 vec![1, 10, 20, 30]
2058 );
2059 }
2060
2061 #[test]
2062 fn merge_enforces_cross_ledger_question_key_uniqueness_and_family_caps() {
2063 let base = HypothesisLedger::new(vec![group()], vec![], vec![]).unwrap();
2064 let duplicate_key =
2065 HypothesisGroup::new(uuid7(3), "cause.primary".into(), uuid7(4), 1).unwrap();
2066 let staged = HypothesisLedger::new(vec![duplicate_key], vec![], vec![]).unwrap();
2067 assert_eq!(invalid_code(&base.merge(&staged).unwrap_err()), "duplicate");
2068
2069 assert!(matches!(
2070 base.merge_with_limit(&base, 0),
2071 Err(KnowledgeError::Limit {
2072 participant: "hypothesis_groups",
2073 observed: 1,
2074 limit: 0,
2075 })
2076 ));
2077 assert_eq!(base.merge_with_limit(&base, 1).unwrap(), base);
2078
2079 let membership = vec![
2080 member(10, 20, 30, HypothesisMembershipAction::Added, 2),
2081 member(11, 21, 31, HypothesisMembershipAction::Added, 3),
2082 ];
2083 let member_ledger = HypothesisLedger::new(vec![group()], membership, vec![]).unwrap();
2084 assert!(matches!(
2085 member_ledger.merge_with_limit(&member_ledger, 1),
2086 Err(KnowledgeError::Limit {
2087 participant: "hypothesis_membership_events",
2088 observed: 2,
2089 limit: 1,
2090 })
2091 ));
2092
2093 let selection_ledger = HypothesisLedger::new(
2094 vec![group()],
2095 vec![],
2096 vec![selection(10, 20, None, 2), selection(11, 21, None, 3)],
2097 )
2098 .unwrap();
2099 assert!(matches!(
2100 selection_ledger.merge_with_limit(&selection_ledger, 1),
2101 Err(KnowledgeError::Limit {
2102 participant: "hypothesis_selection_events",
2103 observed: 2,
2104 limit: 1,
2105 })
2106 ));
2107
2108 let added_member = HypothesisLedger::new(
2109 vec![group()],
2110 vec![member(20, 30, 40, HypothesisMembershipAction::Added, 2)],
2111 vec![],
2112 )
2113 .unwrap();
2114 let another_member = HypothesisLedger::new(
2115 vec![group()],
2116 vec![member(21, 31, 41, HypothesisMembershipAction::Added, 3)],
2117 vec![],
2118 )
2119 .unwrap();
2120 assert!(matches!(
2121 added_member.merge_with_limit(&another_member, 1),
2122 Err(KnowledgeError::Limit {
2123 participant: "hypothesis_membership_events",
2124 observed: 2,
2125 limit: 1,
2126 })
2127 ));
2128
2129 let first_selection =
2130 HypothesisLedger::new(vec![group()], vec![], vec![selection(20, 30, None, 2)]).unwrap();
2131 let second_selection =
2132 HypothesisLedger::new(vec![group()], vec![], vec![selection(21, 31, None, 3)]).unwrap();
2133 assert!(matches!(
2134 first_selection.merge_with_limit(&second_selection, 1),
2135 Err(KnowledgeError::Limit {
2136 participant: "hypothesis_selection_events",
2137 observed: 2,
2138 limit: 1,
2139 })
2140 ));
2141
2142 let second_group = HypothesisLedger::new(
2143 vec![HypothesisGroup::new(uuid7(3), "cause.other".into(), uuid7(4), 2).unwrap()],
2144 vec![],
2145 vec![],
2146 )
2147 .unwrap();
2148 assert!(matches!(
2149 base.merge_with_limit(&second_group, 1),
2150 Err(KnowledgeError::Limit {
2151 participant: "hypothesis_groups",
2152 observed: 2,
2153 limit: 1,
2154 })
2155 ));
2156 }
2157
2158 #[test]
2159 fn small_constructor_cap_checks_each_record_family() {
2160 assert!(matches!(
2161 HypothesisLedger::new_with_limit(vec![group()], vec![], vec![], 0),
2162 Err(KnowledgeError::Limit {
2163 participant: "hypothesis_groups",
2164 observed: 1,
2165 limit: 0,
2166 })
2167 ));
2168 assert!(matches!(
2169 HypothesisLedger::new_with_limit(
2170 vec![group()],
2171 vec![
2172 member(10, 20, 30, HypothesisMembershipAction::Added, 2),
2173 member(11, 21, 31, HypothesisMembershipAction::Added, 3),
2174 ],
2175 vec![],
2176 1,
2177 ),
2178 Err(KnowledgeError::Limit {
2179 participant: "hypothesis_membership_events",
2180 observed: 2,
2181 limit: 1,
2182 })
2183 ));
2184 assert!(matches!(
2185 HypothesisLedger::new_with_limit(
2186 vec![group()],
2187 vec![],
2188 vec![selection(10, 20, None, 2), selection(11, 21, None, 3)],
2189 1,
2190 ),
2191 Err(KnowledgeError::Limit {
2192 participant: "hypothesis_selection_events",
2193 observed: 2,
2194 limit: 1,
2195 })
2196 ));
2197 }
2198
2199 #[test]
2200 fn selected_removal_requires_a_paired_selection_for_the_same_group() {
2201 let primary = group();
2202 let secondary =
2203 HypothesisGroup::new(uuid7(3), "cause.secondary".into(), uuid7(4), 1).unwrap();
2204 let added = member(10, 20, 30, HypothesisMembershipAction::Added, 2);
2205 let selected = selection(11, 21, Some(30), 3);
2206 let removed = member(12, 22, 30, HypothesisMembershipAction::Removed, 4);
2207 let other_group_clear = HypothesisSelectionEvent::new(
2208 uuid7(13),
2209 uuid7(22),
2210 secondary.group_uuid,
2211 None,
2212 uuid7(53),
2213 uuid7(93),
2214 4,
2215 )
2216 .unwrap();
2217
2218 let base = HypothesisLedger::new(
2219 vec![primary.clone(), secondary.clone()],
2220 vec![added.clone()],
2221 vec![selected],
2222 )
2223 .unwrap();
2224 let staged = HypothesisLedger::new(
2225 vec![primary, secondary],
2226 vec![added, removed],
2227 vec![other_group_clear],
2228 )
2229 .unwrap();
2230
2231 assert!(matches!(
2232 base.merge(&staged),
2233 Err(KnowledgeError::Invalid {
2234 field: "hypothesis_membership.action",
2235 ..
2236 })
2237 ));
2238 }
2239
2240 #[test]
2241 fn same_time_operations_follow_first_event_uuid_before_operation_uuid() {
2242 let base_add = member(10, 90, 30, HypothesisMembershipAction::Added, 2);
2243 let staged_history = vec![
2244 member(5, 70, 30, HypothesisMembershipAction::Added, 1),
2245 member(50, 20, 30, HypothesisMembershipAction::Removed, 2),
2246 ];
2247 let base = HypothesisLedger::new(vec![group()], vec![base_add], vec![]).unwrap();
2248 let staged = HypothesisLedger::new(vec![group()], staged_history, vec![]).unwrap();
2249
2250 assert!(matches!(
2251 base.merge(&staged),
2252 Err(KnowledgeError::Invalid {
2253 field: "hypothesis_membership.action",
2254 ..
2255 })
2256 ));
2257 }
2258
2259 #[test]
2260 fn merge_orders_equal_time_groups_by_uuid() {
2261 let existing =
2262 HypothesisGroup::new(uuid7(20), "cause.existing".into(), uuid7(21), 10).unwrap();
2263 let staged_group =
2264 HypothesisGroup::new(uuid7(10), "cause.staged".into(), uuid7(11), 10).unwrap();
2265 let base = HypothesisLedger::new(vec![existing], vec![], vec![]).unwrap();
2266 let staged = HypothesisLedger::new(vec![staged_group], vec![], vec![]).unwrap();
2267
2268 let merged = base.merge(&staged).unwrap();
2269
2270 assert_eq!(
2271 merged
2272 .groups()
2273 .iter()
2274 .map(|row| row.group_uuid)
2275 .collect::<Vec<_>>(),
2276 vec![uuid7(10), uuid7(20)]
2277 );
2278 }
2279
2280 #[test]
2281 fn merge_orders_equal_time_membership_events_by_uuid() {
2282 let existing = member(19, 90, 30, HypothesisMembershipAction::Added, 2);
2283 let staged_event = member(13, 90, 31, HypothesisMembershipAction::Added, 2);
2284 let base = HypothesisLedger::new(vec![group()], vec![existing], vec![]).unwrap();
2285 let staged = HypothesisLedger::new(vec![group()], vec![staged_event], vec![]).unwrap();
2286
2287 let merged = base.merge(&staged).unwrap();
2288
2289 assert_eq!(
2290 merged
2291 .membership_events()
2292 .iter()
2293 .map(|row| row.membership_event_uuid)
2294 .collect::<Vec<_>>(),
2295 vec![uuid7(13), uuid7(19)]
2296 );
2297 }
2298
2299 #[test]
2300 fn merge_orders_equal_time_selection_events_by_uuid() {
2301 let existing = selection(19, 90, None, 2);
2302 let staged_event = selection(13, 90, None, 2);
2303 let base = HypothesisLedger::new(vec![group()], vec![], vec![existing]).unwrap();
2304 let staged = HypothesisLedger::new(vec![group()], vec![], vec![staged_event]).unwrap();
2305
2306 let merged = base.merge(&staged).unwrap();
2307
2308 assert_eq!(
2309 merged
2310 .selection_events()
2311 .iter()
2312 .map(|row| row.selection_event_uuid)
2313 .collect::<Vec<_>>(),
2314 vec![uuid7(13), uuid7(19)]
2315 );
2316 }
2317}