Skip to main content

graphforge_knowledge/
hypothesis.rs

1//! Append-only epistemic hypothesis groups, membership, and explicit selection.
2
3use 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
25/// Frozen hypothesis-group record contract.
26pub const HYPOTHESIS_GROUP_CONTRACT_VERSION: u32 = 1;
27/// Frozen membership-event record contract.
28pub const HYPOTHESIS_MEMBERSHIP_CONTRACT_VERSION: u32 = 1;
29/// Frozen selection-event record contract.
30pub const HYPOTHESIS_SELECTION_CONTRACT_VERSION: u32 = 1;
31/// Canonical group-key policy.
32pub const HYPOTHESIS_KEY_POLICY_VERSION: u32 = 1;
33/// Membership/selection state policy.
34pub const HYPOTHESIS_STATE_POLICY_VERSION: u32 = 1;
35/// Maximum canonical question-key bytes.
36pub const MAX_HYPOTHESIS_QUESTION_KEY_BYTES: usize = 1_024;
37
38/// Authoritative group schema.
39pub 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
49/// Authoritative membership-event schema.
50pub 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
64/// Authoritative selection-event schema.
65pub 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/// Closed membership action.
111#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
112pub enum HypothesisMembershipAction {
113    /// Make an assertion a current group member.
114    Added,
115    /// Remove an assertion from current membership.
116    Removed,
117}
118
119impl HypothesisMembershipAction {
120    /// Stable persisted spelling.
121    #[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/// One immutable hypothesis group.
142#[derive(Clone, Debug, Eq, PartialEq)]
143pub struct HypothesisGroup {
144    /// Caller-supplied UUIDv7 group identity.
145    pub group_uuid: Uuid,
146    /// Canonical question key.
147    pub question_key: String,
148    /// Existing producing provenance event.
149    pub provenance_uuid: Uuid,
150    /// Mandatory transaction time.
151    pub recorded_at_micros: i64,
152    /// Frozen record contract.
153    pub contract_version: u32,
154}
155
156impl HypothesisGroup {
157    /// Validate and construct one canonical group.
158    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/// One immutable membership transition.
178#[derive(Clone, Debug, Eq, PartialEq)]
179pub struct HypothesisMembershipEvent {
180    /// Caller-supplied UUIDv7 event identity.
181    pub membership_event_uuid: Uuid,
182    /// Publication operation identity.
183    pub operation_uuid: Uuid,
184    /// Existing hypothesis group.
185    pub group_uuid: Uuid,
186    /// Existing immutable assertion.
187    pub assertion_uuid: Uuid,
188    /// Explicit add/remove action.
189    pub action: HypothesisMembershipAction,
190    /// Existing decision rationale.
191    pub reasoning_uuid: Uuid,
192    /// Existing producing provenance event.
193    pub provenance_uuid: Uuid,
194    /// Mandatory transaction time.
195    pub recorded_at_micros: i64,
196    /// Frozen record contract.
197    pub contract_version: u32,
198}
199
200impl HypothesisMembershipEvent {
201    /// Validate and construct one membership event.
202    #[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/// One immutable explicit selection or clear event.
238#[derive(Clone, Debug, Eq, PartialEq)]
239pub struct HypothesisSelectionEvent {
240    /// Caller-supplied UUIDv7 event identity.
241    pub selection_event_uuid: Uuid,
242    /// Publication operation identity.
243    pub operation_uuid: Uuid,
244    /// Existing hypothesis group.
245    pub group_uuid: Uuid,
246    /// Explicit selected member, or `None` to clear selection.
247    pub selected_assertion_uuid: Option<Uuid>,
248    /// Existing decision rationale.
249    pub reasoning_uuid: Uuid,
250    /// Existing producing provenance event.
251    pub provenance_uuid: Uuid,
252    /// Mandatory transaction time.
253    pub recorded_at_micros: i64,
254    /// Frozen record contract.
255    pub contract_version: u32,
256}
257
258impl HypothesisSelectionEvent {
259    /// Validate and construct one explicit selection event.
260    #[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/// Validated complete hypothesis state participants.
296#[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    /// Validate, order, and construct all three record families together.
305    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    /// Return validated groups in canonical order.
346    pub fn groups(&self) -> &[HypothesisGroup] {
347        &self.groups
348    }
349
350    #[must_use]
351    /// Return validated membership history in canonical order.
352    pub fn membership_events(&self) -> &[HypothesisMembershipEvent] {
353        &self.membership_events
354    }
355
356    #[must_use]
357    /// Return validated selection history in canonical order.
358    pub fn selection_events(&self) -> &[HypothesisSelectionEvent] {
359        &self.selection_events
360    }
361
362    /// Canonical fingerprint over one exact immutable hypothesis group.
363    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    /// Compute canonical group fingerprints in one pass without UUID searches.
373    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    /// Canonical fingerprint over one exact membership event.
381    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    /// Compute canonical membership fingerprints in one pass without UUID searches.
394    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    /// Canonical fingerprint over one exact selection event.
407    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    /// Compute canonical selection fingerprints in one pass without UUID searches.
420    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    /// Merge append-only participants with exact replay semantics.
433    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        // The two immutable operands were validated at construction. Merging
469        // can only add rows or replay exact identities, but it can change the
470        // combined operation history, so replay all state transitions.
471        validate_state(&groups, &membership_events, &selection_events)?;
472        Ok(Self {
473            groups,
474            membership_events,
475            selection_events,
476        })
477    }
478
479    /// Build the authoritative group batch.
480    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    /// Build the authoritative membership-history batch.
510    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    /// Build the authoritative selection-history batch.
549    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    /// Decode and validate the three authoritative record families.
589    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    /// Current membership for one group, ordered by assertion UUID.
676    #[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    /// Current explicit selection for one group.
699    #[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}