Skip to main content

graphforge_knowledge/
belief_projection.rs

1//! Append-only epistemic attachments connecting resolved interpretation to completed knowledge runs.
2
3use std::collections::{HashMap, HashSet};
4use std::sync::{Arc, LazyLock};
5#[cfg(test)]
6use std::{cell::Cell, thread_local};
7
8use arrow::array::{
9    Array, BinaryArray, BinaryBuilder, FixedSizeBinaryArray, FixedSizeBinaryBuilder, ListArray,
10    ListBuilder, TimestampMicrosecondArray, TimestampMicrosecondBuilder, UInt32Array,
11    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, MAX_CANONICAL_BINARY_BYTES,
17    fingerprint,
18};
19use uuid::{Uuid, Version};
20
21use crate::{
22    EPISTEMIC_CAPABILITY_VERSION, KnowledgeError, MAX_KNOWLEDGE_ROWS, SchemaRegistryEntry,
23};
24
25/// Frozen `belief_projection_attachment/1` record contract.
26pub const BELIEF_PROJECTION_ATTACHMENT_CONTRACT_VERSION: u32 = 1;
27
28/// Authoritative `algorithm_interpretation_attachments` schema.
29pub static ALGORITHM_INTERPRETATION_ATTACHMENT_SCHEMA: LazyLock<SchemaRef> = LazyLock::new(|| {
30    let uuid_list = DataType::List(Arc::new(Field::new(
31        "item",
32        DataType::FixedSizeBinary(16),
33        false,
34    )));
35    Arc::new(Schema::new(vec![
36        uuid_field("attachment_uuid", false),
37        uuid_field("run_uuid", false),
38        uuid_field("source_generation_uuid", false),
39        timestamp_field("transaction_cutoff", false),
40        timestamp_field("valid_time", true),
41        Field::new("policy_version", DataType::UInt32, false),
42        Field::new("policy_bytes", DataType::Binary, false),
43        fingerprint_field("policy_fingerprint", false),
44        fingerprint_field("snapshot_fingerprint", false),
45        fingerprint_field("valid_time_fingerprint", true),
46        fingerprint_field("graph_content_fingerprint", false),
47        fingerprint_field("descriptor_fingerprint", false),
48        Field::new("source_record_uuids", uuid_list, false),
49        uuid_field("provenance_uuid", false),
50        timestamp_field("recorded_at", false),
51        Field::new("contract_version", DataType::UInt32, false),
52    ]))
53});
54
55static SCHEMA_FINGERPRINT: LazyLock<[u8; 32]> = LazyLock::new(|| {
56    fingerprint(
57        CanonicalDomain::Schema,
58        CANONICAL_CONTRACT_VERSION,
59        b"belief_projection_attachment/1|attachment_uuid:fixed[16]:required|run_uuid:fixed[16]:required|source_generation_uuid:fixed[16]:required|transaction_cutoff:timestamp_us_utc:required|valid_time:timestamp_us_utc:nullable|policy_version:u32:required|policy_bytes:binary:required|policy_fingerprint:fixed[32]:required|snapshot_fingerprint:fixed[32]:required|valid_time_fingerprint:fixed[32]:nullable|graph_content_fingerprint:fixed[32]:required|descriptor_fingerprint:fixed[32]:required|source_record_uuids:list<fixed[16]>:required|provenance_uuid:fixed[16]:required|recorded_at:timestamp_us_utc:required|contract_version:u32:required",
60    )
61    .expect("registered belief-projection attachment schema is bounded")
62});
63
64/// One immutable interpretation attachment for a completed algorithm run.
65#[derive(Clone, Debug, Eq, PartialEq)]
66pub struct BeliefProjectionAttachment {
67    /// Stable attachment identity and idempotency key.
68    pub attachment_uuid: Uuid,
69    /// Completed knowledge run identity.
70    pub run_uuid: Uuid,
71    /// Source project generation pinned during resolution.
72    pub source_generation_uuid: Uuid,
73    /// Mandatory transaction-time cutoff.
74    pub transaction_cutoff_micros: i64,
75    /// Optional valid time applied after transaction reconstruction.
76    pub valid_time_micros: Option<i64>,
77    /// Explicit resolved-belief policy version.
78    pub policy_version: u32,
79    /// Canonical language-neutral policy bytes.
80    pub policy_bytes: Vec<u8>,
81    /// Canonical fingerprint of `policy_bytes`.
82    pub policy_fingerprint: [u8; 32],
83    /// Fingerprint of the transaction-time epistemic snapshot.
84    pub snapshot_fingerprint: [u8; 32],
85    /// Fingerprint of the optional valid-time interpretation.
86    pub valid_time_fingerprint: Option<[u8; 32]>,
87    /// Canonical logical graph-content fingerprint.
88    pub graph_content_fingerprint: [u8; 32],
89    /// Neutral algorithm invocation-descriptor fingerprint.
90    pub descriptor_fingerprint: [u8; 32],
91    /// Sorted and deduplicated decision-relevant epistemic records.
92    pub source_record_uuids: Vec<Uuid>,
93    /// Producing provenance event.
94    pub provenance_uuid: Uuid,
95    /// Mandatory attachment transaction time.
96    pub recorded_at_micros: i64,
97    /// Frozen record contract.
98    pub contract_version: u32,
99}
100
101impl BeliefProjectionAttachment {
102    /// Validate and construct an attachment, canonicalizing source UUIDs.
103    #[allow(clippy::too_many_arguments)]
104    pub fn new(
105        attachment_uuid: Uuid,
106        run_uuid: Uuid,
107        source_generation_uuid: Uuid,
108        transaction_cutoff_micros: i64,
109        valid_time_micros: Option<i64>,
110        policy_version: u32,
111        policy_bytes: Vec<u8>,
112        snapshot_fingerprint: [u8; 32],
113        valid_time_fingerprint: Option<[u8; 32]>,
114        graph_content_fingerprint: [u8; 32],
115        descriptor_fingerprint: [u8; 32],
116        mut source_record_uuids: Vec<Uuid>,
117        provenance_uuid: Uuid,
118        recorded_at_micros: i64,
119    ) -> Result<Self, KnowledgeError> {
120        source_record_uuids.sort_unstable();
121        source_record_uuids.dedup();
122        let policy_fingerprint = fingerprint(
123            CanonicalDomain::BeliefProjectionPolicy,
124            CANONICAL_CONTRACT_VERSION,
125            &policy_bytes,
126        )?;
127        let row = Self {
128            attachment_uuid,
129            run_uuid,
130            source_generation_uuid,
131            transaction_cutoff_micros,
132            valid_time_micros,
133            policy_version,
134            policy_bytes,
135            policy_fingerprint,
136            snapshot_fingerprint,
137            valid_time_fingerprint,
138            graph_content_fingerprint,
139            descriptor_fingerprint,
140            source_record_uuids,
141            provenance_uuid,
142            recorded_at_micros,
143            contract_version: BELIEF_PROJECTION_ATTACHMENT_CONTRACT_VERSION,
144        };
145        validate(&row)?;
146        Ok(row)
147    }
148}
149
150/// Validated append-only interpretation-attachment participant.
151///
152/// The validated collection is private so callers cannot invalidate it after
153/// construction. Read through [`Self::attachments`] or consume with
154/// [`Self::into_attachments`].
155/// This is a source compatibility change for callers that directly used the
156/// former public `attachments` field; construct ledgers with [`Self::new`].
157///
158/// ```compile_fail
159/// use graphforge_knowledge::BeliefProjectionAttachmentLedger;
160/// let mut ledger = BeliefProjectionAttachmentLedger::default();
161/// ledger.attachments.clear();
162/// ```
163///
164/// ```compile_fail
165/// use graphforge_knowledge::BeliefProjectionAttachmentLedger;
166/// let ledger = BeliefProjectionAttachmentLedger::default();
167/// ledger.attachments()[0].policy_version = 0;
168/// ```
169#[derive(Clone, Debug, Default, Eq, PartialEq)]
170pub struct BeliefProjectionAttachmentLedger {
171    /// Attachments ordered by `(recorded_at, attachment_uuid)`.
172    attachments: Vec<BeliefProjectionAttachment>,
173}
174
175impl BeliefProjectionAttachmentLedger {
176    /// Borrow the validated attachments in deterministic order.
177    #[must_use]
178    pub fn attachments(&self) -> &[BeliefProjectionAttachment] {
179        &self.attachments
180    }
181
182    /// Consume the ledger and return its validated attachments.
183    #[must_use]
184    pub fn into_attachments(self) -> Vec<BeliefProjectionAttachment> {
185        self.attachments
186    }
187
188    /// Validate, sort, and construct a complete participant.
189    pub fn new(attachments: Vec<BeliefProjectionAttachment>) -> Result<Self, KnowledgeError> {
190        Self::new_with_limit(attachments, MAX_KNOWLEDGE_ROWS)
191    }
192
193    fn new_with_limit(
194        mut attachments: Vec<BeliefProjectionAttachment>,
195        row_limit: usize,
196    ) -> Result<Self, KnowledgeError> {
197        validate_row_count(attachments.len(), row_limit)?;
198        let mut ids = HashSet::with_capacity(attachments.len());
199        for row in &attachments {
200            validate(row)?;
201            if !ids.insert(row.attachment_uuid) {
202                return Err(KnowledgeError::Duplicate("attachment_uuid"));
203            }
204        }
205        attachments.sort_by_key(|row| (row.recorded_at_micros, row.attachment_uuid));
206        Ok(Self { attachments })
207    }
208
209    /// Merge with exact replay and transaction-conflict semantics.
210    pub fn merge(&self, staged: &Self) -> Result<Self, KnowledgeError> {
211        self.merge_with_limit(staged, MAX_KNOWLEDGE_ROWS)
212    }
213
214    fn merge_with_limit(&self, staged: &Self, row_limit: usize) -> Result<Self, KnowledgeError> {
215        // Both operands crossed `new()` or `from_batches()`, and callers cannot
216        // mutate the private row vector. Revalidate only at those boundaries.
217        let mut rows = Vec::with_capacity(self.attachments.len() + staged.attachments.len());
218        rows.extend(self.attachments.iter().cloned());
219        let mut by_id = HashMap::with_capacity(self.attachments.len() + staged.attachments.len());
220        by_id.extend(
221            self.attachments
222                .iter()
223                .map(|row| (row.attachment_uuid, row)),
224        );
225        for row in &staged.attachments {
226            if let Some(existing) = by_id.get(&row.attachment_uuid) {
227                if *existing != row {
228                    return Err(KnowledgeError::TransactionConflict("attachment_uuid"));
229                }
230            } else {
231                rows.push(row.clone());
232                by_id.insert(row.attachment_uuid, row);
233            }
234        }
235        // Preserve the previous error precedence: scan every staged identity
236        // for conflicts before reporting a combined row-limit violation.
237        validate_row_count(rows.len(), row_limit)?;
238        rows.sort_by_key(|row| (row.recorded_at_micros, row.attachment_uuid));
239        Ok(Self { attachments: rows })
240    }
241
242    /// Canonical fingerprint over one exact immutable attachment.
243    pub fn attachment_fingerprint(&self, id: Uuid) -> Result<[u8; 32], KnowledgeError> {
244        let row = self
245            .attachments
246            .iter()
247            .find(|row| row.attachment_uuid == id)
248            .ok_or(KnowledgeError::Dangling("attachment_uuid"))?;
249        let mut writer = CanonicalWriter::new();
250        writer.raw(row.attachment_uuid.as_bytes())?;
251        writer.raw(row.run_uuid.as_bytes())?;
252        writer.raw(row.source_generation_uuid.as_bytes())?;
253        writer.i64(row.transaction_cutoff_micros)?;
254        optional_i64(&mut writer, row.valid_time_micros)?;
255        writer.u32(row.policy_version)?;
256        writer.binary(&row.policy_bytes)?;
257        writer.raw(&row.policy_fingerprint)?;
258        writer.raw(&row.snapshot_fingerprint)?;
259        optional_fingerprint(&mut writer, row.valid_time_fingerprint)?;
260        writer.raw(&row.graph_content_fingerprint)?;
261        writer.raw(&row.descriptor_fingerprint)?;
262        writer.u64(u64::try_from(row.source_record_uuids.len()).unwrap_or(u64::MAX))?;
263        for source in &row.source_record_uuids {
264            writer.raw(source.as_bytes())?;
265        }
266        writer.raw(row.provenance_uuid.as_bytes())?;
267        writer.i64(row.recorded_at_micros)?;
268        writer.u32(row.contract_version)?;
269        fingerprint(
270            CanonicalDomain::BeliefProjectionAttachment,
271            CANONICAL_CONTRACT_VERSION,
272            &writer.finish(),
273        )
274        .map_err(Into::into)
275    }
276
277    /// Build the authoritative Arrow batch.
278    pub fn batch(&self) -> Result<RecordBatch, KnowledgeError> {
279        let len = self.attachments.len();
280        let mut attachment = FixedSizeBinaryBuilder::with_capacity(len, 16);
281        let mut run = FixedSizeBinaryBuilder::with_capacity(len, 16);
282        let mut generation = FixedSizeBinaryBuilder::with_capacity(len, 16);
283        let mut cutoff = TimestampMicrosecondBuilder::new().with_timezone("UTC");
284        let mut valid_time = TimestampMicrosecondBuilder::new().with_timezone("UTC");
285        let mut policy_version = UInt32Builder::new();
286        let mut policy_bytes = BinaryBuilder::new();
287        let mut policy_fp = FixedSizeBinaryBuilder::with_capacity(len, 32);
288        let mut snapshot_fp = FixedSizeBinaryBuilder::with_capacity(len, 32);
289        let mut valid_fp = FixedSizeBinaryBuilder::with_capacity(len, 32);
290        let mut graph_fp = FixedSizeBinaryBuilder::with_capacity(len, 32);
291        let mut descriptor_fp = FixedSizeBinaryBuilder::with_capacity(len, 32);
292        let mut sources = ListBuilder::new(FixedSizeBinaryBuilder::new(16)).with_field(Arc::new(
293            Field::new("item", DataType::FixedSizeBinary(16), false),
294        ));
295        let mut provenance = FixedSizeBinaryBuilder::with_capacity(len, 16);
296        let mut recorded = TimestampMicrosecondBuilder::new().with_timezone("UTC");
297        let mut version = UInt32Builder::new();
298        for row in &self.attachments {
299            append(&mut attachment, row.attachment_uuid.as_bytes())?;
300            append(&mut run, row.run_uuid.as_bytes())?;
301            append(&mut generation, row.source_generation_uuid.as_bytes())?;
302            cutoff.append_value(row.transaction_cutoff_micros);
303            valid_time.append_option(row.valid_time_micros);
304            policy_version.append_value(row.policy_version);
305            policy_bytes.append_value(&row.policy_bytes);
306            append(&mut policy_fp, &row.policy_fingerprint)?;
307            append(&mut snapshot_fp, &row.snapshot_fingerprint)?;
308            append_optional(&mut valid_fp, row.valid_time_fingerprint.as_ref())?;
309            append(&mut graph_fp, &row.graph_content_fingerprint)?;
310            append(&mut descriptor_fp, &row.descriptor_fingerprint)?;
311            for source in &row.source_record_uuids {
312                append(sources.values(), source.as_bytes())?;
313            }
314            sources.append(true);
315            append(&mut provenance, row.provenance_uuid.as_bytes())?;
316            recorded.append_value(row.recorded_at_micros);
317            version.append_value(row.contract_version);
318        }
319        Ok(RecordBatch::try_new(
320            Arc::clone(&ALGORITHM_INTERPRETATION_ATTACHMENT_SCHEMA),
321            vec![
322                Arc::new(attachment.finish()),
323                Arc::new(run.finish()),
324                Arc::new(generation.finish()),
325                Arc::new(cutoff.finish()),
326                Arc::new(valid_time.finish()),
327                Arc::new(policy_version.finish()),
328                Arc::new(policy_bytes.finish()),
329                Arc::new(policy_fp.finish()),
330                Arc::new(snapshot_fp.finish()),
331                Arc::new(valid_fp.finish()),
332                Arc::new(graph_fp.finish()),
333                Arc::new(descriptor_fp.finish()),
334                Arc::new(sources.finish()),
335                Arc::new(provenance.finish()),
336                Arc::new(recorded.finish()),
337                Arc::new(version.finish()),
338            ],
339        )?)
340    }
341
342    /// Decode exact Arrow batches and re-run every invariant.
343    pub fn from_batches(batches: &[RecordBatch]) -> Result<Self, KnowledgeError> {
344        let mut rows = Vec::new();
345        for batch in batches {
346            if batch.schema().as_ref() != ALGORITHM_INTERPRETATION_ATTACHMENT_SCHEMA.as_ref() {
347                return Err(invalid(
348                    "belief_projection_attachment.schema",
349                    "schema mismatch",
350                ));
351            }
352            let attachment = fixed(batch, "attachment_uuid")?;
353            let run = fixed(batch, "run_uuid")?;
354            let generation = fixed(batch, "source_generation_uuid")?;
355            let cutoff = timestamp(batch, "transaction_cutoff")?;
356            let valid_time = timestamp(batch, "valid_time")?;
357            let policy_version = uint32(batch, "policy_version")?;
358            let policy_bytes = binary(batch, "policy_bytes")?;
359            let policy_fp = fixed(batch, "policy_fingerprint")?;
360            let snapshot_fp = fixed(batch, "snapshot_fingerprint")?;
361            let valid_fp = fixed(batch, "valid_time_fingerprint")?;
362            let graph_fp = fixed(batch, "graph_content_fingerprint")?;
363            let descriptor_fp = fixed(batch, "descriptor_fingerprint")?;
364            let sources = list(batch, "source_record_uuids")?;
365            let provenance = fixed(batch, "provenance_uuid")?;
366            let recorded = timestamp(batch, "recorded_at")?;
367            let versions = uint32(batch, "contract_version")?;
368            for row in 0..batch.num_rows() {
369                rows.push(BeliefProjectionAttachment {
370                    attachment_uuid: uuid_at(attachment, row, "attachment_uuid")?,
371                    run_uuid: uuid_at(run, row, "run_uuid")?,
372                    source_generation_uuid: uuid_at(generation, row, "source_generation_uuid")?,
373                    transaction_cutoff_micros: cutoff.value(row),
374                    valid_time_micros: optional_timestamp(valid_time, row),
375                    policy_version: policy_version.value(row),
376                    policy_bytes: policy_bytes.value(row).to_vec(),
377                    policy_fingerprint: bytes32(policy_fp, row, "policy_fingerprint")?,
378                    snapshot_fingerprint: bytes32(snapshot_fp, row, "snapshot_fingerprint")?,
379                    valid_time_fingerprint: optional_bytes32(
380                        valid_fp,
381                        row,
382                        "valid_time_fingerprint",
383                    )?,
384                    graph_content_fingerprint: bytes32(graph_fp, row, "graph_content_fingerprint")?,
385                    descriptor_fingerprint: bytes32(descriptor_fp, row, "descriptor_fingerprint")?,
386                    source_record_uuids: uuid_list_at(sources, row)?,
387                    provenance_uuid: uuid_at(provenance, row, "provenance_uuid")?,
388                    recorded_at_micros: recorded.value(row),
389                    contract_version: versions.value(row),
390                });
391            }
392        }
393        Self::new(rows)
394    }
395}
396
397pub(crate) fn schema_registry_entry() -> SchemaRegistryEntry {
398    SchemaRegistryEntry {
399        capability_id: "epistemic",
400        capability_version: EPISTEMIC_CAPABILITY_VERSION,
401        record_family: "algorithm_interpretation_attachments",
402        record_version: BELIEF_PROJECTION_ATTACHMENT_CONTRACT_VERSION,
403        schema: Arc::clone(&ALGORITHM_INTERPRETATION_ATTACHMENT_SCHEMA),
404        schema_fingerprint: *SCHEMA_FINGERPRINT,
405        enum_registry_versions: &[],
406        sort_key: &["recorded_at", "attachment_uuid"],
407        diff_identity_fields: &["attachment_uuid"],
408        diff_record_uuid_field: Some("attachment_uuid"),
409        fingerprint_domain: CanonicalDomain::BeliefProjectionAttachment,
410        owner: "graphforge-knowledge",
411        implementation_issue: 2004,
412        max_rows: MAX_KNOWLEDGE_ROWS,
413    }
414}
415
416fn validate(row: &BeliefProjectionAttachment) -> Result<(), KnowledgeError> {
417    #[cfg(test)]
418    VALIDATION_CALLS.with(|calls| calls.set(calls.get() + 1));
419
420    if row.contract_version != BELIEF_PROJECTION_ATTACHMENT_CONTRACT_VERSION {
421        return Err(invalid(
422            "belief_projection_attachment.contract_version",
423            "unsupported version",
424        ));
425    }
426    require_v7(row.attachment_uuid, "attachment_uuid")?;
427    require_v7(row.run_uuid, "run_uuid")?;
428    require_uuid(row.source_generation_uuid, "source_generation_uuid")?;
429    require_uuid(row.provenance_uuid, "provenance_uuid")?;
430    if row.policy_version == 0 {
431        return Err(invalid(
432            "belief_projection_attachment.policy_version",
433            "must be positive",
434        ));
435    }
436    validate_policy_len(row.policy_bytes.len())?;
437    let expected = fingerprint(
438        CanonicalDomain::BeliefProjectionPolicy,
439        CANONICAL_CONTRACT_VERSION,
440        &row.policy_bytes,
441    )?;
442    if row.policy_fingerprint != expected {
443        return Err(invalid(
444            "belief_projection_attachment.policy_fingerprint",
445            "does not match policy bytes",
446        ));
447    }
448    if row
449        .source_record_uuids
450        .windows(2)
451        .any(|pair| pair[0] >= pair[1])
452    {
453        return Err(invalid(
454            "belief_projection_attachment.source_record_uuids",
455            "must be sorted and deduplicated",
456        ));
457    }
458    for source in &row.source_record_uuids {
459        require_uuid(*source, "source_record_uuid")?;
460    }
461    Ok(())
462}
463
464#[cfg(test)]
465thread_local! {
466    static VALIDATION_CALLS: Cell<usize> = const { Cell::new(0) };
467}
468
469#[cfg(test)]
470fn reset_validation_calls() {
471    VALIDATION_CALLS.with(|calls| calls.set(0));
472}
473
474#[cfg(test)]
475fn validation_call_count() -> usize {
476    VALIDATION_CALLS.with(Cell::get)
477}
478
479fn validate_row_count(count: usize, limit: usize) -> Result<(), KnowledgeError> {
480    if count > limit {
481        Err(KnowledgeError::Limit {
482            participant: "algorithm_interpretation_attachments",
483            observed: count,
484            limit,
485        })
486    } else {
487        Ok(())
488    }
489}
490
491fn validate_policy_len(length: usize) -> Result<(), KnowledgeError> {
492    if u64::try_from(length).unwrap_or(u64::MAX) > MAX_CANONICAL_BINARY_BYTES {
493        Err(KnowledgeError::Limit {
494            participant: "policy_bytes",
495            observed: length,
496            limit: usize::try_from(MAX_CANONICAL_BINARY_BYTES).unwrap_or(usize::MAX),
497        })
498    } else {
499        Ok(())
500    }
501}
502
503fn optional_i64(writer: &mut CanonicalWriter, value: Option<i64>) -> Result<(), KnowledgeError> {
504    match value {
505        Some(value) => {
506            writer.u8(1)?;
507            writer.i64(value)?;
508        }
509        None => writer.u8(0)?,
510    }
511    Ok(())
512}
513fn optional_fingerprint(
514    writer: &mut CanonicalWriter,
515    value: Option<[u8; 32]>,
516) -> Result<(), KnowledgeError> {
517    match value {
518        Some(value) => {
519            writer.u8(1)?;
520            writer.raw(&value)?;
521        }
522        None => writer.u8(0)?,
523    }
524    Ok(())
525}
526fn append(builder: &mut FixedSizeBinaryBuilder, value: &[u8]) -> Result<(), KnowledgeError> {
527    builder
528        .append_value(value)
529        .map_err(|_| invalid("belief_projection_attachment", "invalid fixed-width value"))
530}
531fn append_optional(
532    builder: &mut FixedSizeBinaryBuilder,
533    value: Option<&[u8; 32]>,
534) -> Result<(), KnowledgeError> {
535    if let Some(value) = value {
536        append(builder, value)
537    } else {
538        builder.append_null();
539        Ok(())
540    }
541}
542fn uuid_field(name: &str, nullable: bool) -> Field {
543    Field::new(name, DataType::FixedSizeBinary(16), nullable)
544}
545fn fingerprint_field(name: &str, nullable: bool) -> Field {
546    Field::new(name, DataType::FixedSizeBinary(32), nullable)
547}
548fn timestamp_field(name: &str, nullable: bool) -> Field {
549    Field::new(
550        name,
551        DataType::Timestamp(TimeUnit::Microsecond, Some("UTC".into())),
552        nullable,
553    )
554}
555fn fixed<'a>(
556    batch: &'a RecordBatch,
557    name: &'static str,
558) -> Result<&'a FixedSizeBinaryArray, KnowledgeError> {
559    batch
560        .column_by_name(name)
561        .and_then(|v| v.as_any().downcast_ref())
562        .ok_or_else(|| invalid(name, "column type mismatch"))
563}
564fn timestamp<'a>(
565    batch: &'a RecordBatch,
566    name: &'static str,
567) -> Result<&'a TimestampMicrosecondArray, KnowledgeError> {
568    batch
569        .column_by_name(name)
570        .and_then(|v| v.as_any().downcast_ref())
571        .ok_or_else(|| invalid(name, "column type mismatch"))
572}
573fn uint32<'a>(
574    batch: &'a RecordBatch,
575    name: &'static str,
576) -> Result<&'a UInt32Array, KnowledgeError> {
577    batch
578        .column_by_name(name)
579        .and_then(|v| v.as_any().downcast_ref())
580        .ok_or_else(|| invalid(name, "column type mismatch"))
581}
582fn binary<'a>(
583    batch: &'a RecordBatch,
584    name: &'static str,
585) -> Result<&'a BinaryArray, KnowledgeError> {
586    batch
587        .column_by_name(name)
588        .and_then(|v| v.as_any().downcast_ref())
589        .ok_or_else(|| invalid(name, "column type mismatch"))
590}
591fn list<'a>(batch: &'a RecordBatch, name: &'static str) -> Result<&'a ListArray, KnowledgeError> {
592    batch
593        .column_by_name(name)
594        .and_then(|v| v.as_any().downcast_ref())
595        .ok_or_else(|| invalid(name, "column type mismatch"))
596}
597fn uuid_at(
598    values: &FixedSizeBinaryArray,
599    row: usize,
600    field: &'static str,
601) -> Result<Uuid, KnowledgeError> {
602    Uuid::from_slice(values.value(row)).map_err(|_| invalid(field, "invalid UUID bytes"))
603}
604fn bytes32(
605    values: &FixedSizeBinaryArray,
606    row: usize,
607    field: &'static str,
608) -> Result<[u8; 32], KnowledgeError> {
609    values
610        .value(row)
611        .try_into()
612        .map_err(|_| invalid(field, "invalid fingerprint width"))
613}
614fn optional_bytes32(
615    values: &FixedSizeBinaryArray,
616    row: usize,
617    field: &'static str,
618) -> Result<Option<[u8; 32]>, KnowledgeError> {
619    (!values.is_null(row))
620        .then(|| bytes32(values, row, field))
621        .transpose()
622}
623fn optional_timestamp(values: &TimestampMicrosecondArray, row: usize) -> Option<i64> {
624    (!values.is_null(row)).then(|| values.value(row))
625}
626fn uuid_list_at(values: &ListArray, row: usize) -> Result<Vec<Uuid>, KnowledgeError> {
627    let value = values.value(row);
628    let fixed = value
629        .as_any()
630        .downcast_ref::<FixedSizeBinaryArray>()
631        .ok_or_else(|| invalid("source_record_uuids", "item type mismatch"))?;
632    (0..fixed.len())
633        .map(|index| uuid_at(fixed, index, "source_record_uuid"))
634        .collect()
635}
636fn require_v7(value: Uuid, field: &'static str) -> Result<(), KnowledgeError> {
637    if value.get_version() != Some(Version::SortRand) {
638        return Err(invalid(field, "must be UUIDv7"));
639    }
640    require_uuid(value, field)
641}
642fn require_uuid(value: Uuid, field: &'static str) -> Result<(), KnowledgeError> {
643    if value.is_nil() {
644        Err(invalid(field, "must not be nil"))
645    } else {
646        Ok(())
647    }
648}
649const fn invalid(field: &'static str, message: &'static str) -> KnowledgeError {
650    KnowledgeError::Invalid { field, message }
651}
652
653#[cfg(test)]
654mod tests {
655    use super::*;
656    use std::time::{Duration, Instant};
657    fn uuid7(seed: u8) -> Uuid {
658        let mut bytes = [seed; 16];
659        bytes[6] = (bytes[6] & 0x0f) | 0x70;
660        bytes[8] = (bytes[8] & 0x3f) | 0x80;
661        Uuid::from_bytes(bytes)
662    }
663    fn attachment(id: u8, sources: Vec<Uuid>) -> BeliefProjectionAttachment {
664        BeliefProjectionAttachment::new(
665            uuid7(id),
666            uuid7(id + 1),
667            uuid7(id + 2),
668            10,
669            Some(11),
670            1,
671            b"policy-v1".to_vec(),
672            [3; 32],
673            Some([4; 32]),
674            [5; 32],
675            [6; 32],
676            sources,
677            uuid7(id + 3),
678            20,
679        )
680        .unwrap()
681    }
682
683    fn attachment_at(id: u8, recorded_at_micros: i64) -> BeliefProjectionAttachment {
684        let mut row = attachment(id, vec![]);
685        row.recorded_at_micros = recorded_at_micros;
686        row
687    }
688
689    fn non_v7_uuid(seed: u8) -> Uuid {
690        let mut bytes = [seed; 16];
691        bytes[6] = (bytes[6] & 0x0f) | 0x40;
692        bytes[8] = (bytes[8] & 0x3f) | 0x80;
693        Uuid::from_bytes(bytes)
694    }
695
696    fn benchmark_uuid(seed: u64) -> Uuid {
697        let mut bytes = [0; 16];
698        bytes[8..].copy_from_slice(&seed.to_be_bytes());
699        bytes[6] = 0x70;
700        bytes[8] = (bytes[8] & 0x3f) | 0x80;
701        Uuid::from_bytes(bytes)
702    }
703
704    fn benchmark_attachment(seed: u64) -> BeliefProjectionAttachment {
705        let policy_bytes = b"benchmark-policy".to_vec();
706        BeliefProjectionAttachment {
707            attachment_uuid: benchmark_uuid(seed + 1),
708            run_uuid: benchmark_uuid(seed + 2),
709            source_generation_uuid: benchmark_uuid(seed + 3),
710            transaction_cutoff_micros: 10,
711            valid_time_micros: Some(11),
712            policy_version: 1,
713            policy_fingerprint: fingerprint(
714                CanonicalDomain::BeliefProjectionPolicy,
715                CANONICAL_CONTRACT_VERSION,
716                &policy_bytes,
717            )
718            .unwrap(),
719            policy_bytes,
720            snapshot_fingerprint: [3; 32],
721            valid_time_fingerprint: Some([4; 32]),
722            graph_content_fingerprint: [5; 32],
723            descriptor_fingerprint: [6; 32],
724            source_record_uuids: vec![benchmark_uuid(seed + 4)],
725            provenance_uuid: benchmark_uuid(seed + 5),
726            recorded_at_micros: i64::try_from(seed).unwrap(),
727            contract_version: BELIEF_PROJECTION_ATTACHMENT_CONTRACT_VERSION,
728        }
729    }
730
731    fn merge_with_full_revalidation(
732        existing: &BeliefProjectionAttachmentLedger,
733        staged: &BeliefProjectionAttachmentLedger,
734    ) -> Result<BeliefProjectionAttachmentLedger, KnowledgeError> {
735        let mut rows = existing.attachments.clone();
736        let mut by_id = rows
737            .iter()
738            .cloned()
739            .map(|row| (row.attachment_uuid, row))
740            .collect::<HashMap<_, _>>();
741        for row in &staged.attachments {
742            if let Some(previous) = by_id.get(&row.attachment_uuid) {
743                if previous != row {
744                    return Err(KnowledgeError::TransactionConflict("attachment_uuid"));
745                }
746            } else {
747                rows.push(row.clone());
748                by_id.insert(row.attachment_uuid, row.clone());
749            }
750        }
751        BeliefProjectionAttachmentLedger::new(rows)
752    }
753
754    fn median(mut values: Vec<Duration>) -> Duration {
755        values.sort_unstable();
756        values[values.len() / 2]
757    }
758
759    fn input_digest(
760        existing: &BeliefProjectionAttachmentLedger,
761        staged: &BeliefProjectionAttachmentLedger,
762    ) -> String {
763        let mut writer = CanonicalWriter::new();
764        for ledger in [existing, staged] {
765            writer.u64(ledger.attachments.len() as u64).unwrap();
766            for row in &ledger.attachments {
767                writer.raw(row.attachment_uuid.as_bytes()).unwrap();
768                writer.raw(row.run_uuid.as_bytes()).unwrap();
769                writer.raw(row.source_generation_uuid.as_bytes()).unwrap();
770                writer.i64(row.transaction_cutoff_micros).unwrap();
771                writer
772                    .u8(u8::from(row.valid_time_micros.is_some()))
773                    .unwrap();
774                if let Some(value) = row.valid_time_micros {
775                    writer.i64(value).unwrap();
776                }
777                writer.u32(row.policy_version).unwrap();
778                writer.binary(&row.policy_bytes).unwrap();
779                writer.raw(&row.policy_fingerprint).unwrap();
780                writer.raw(&row.snapshot_fingerprint).unwrap();
781                writer
782                    .u8(u8::from(row.valid_time_fingerprint.is_some()))
783                    .unwrap();
784                if let Some(value) = row.valid_time_fingerprint {
785                    writer.raw(&value).unwrap();
786                }
787                writer.raw(&row.graph_content_fingerprint).unwrap();
788                writer.raw(&row.descriptor_fingerprint).unwrap();
789                writer.u64(row.source_record_uuids.len() as u64).unwrap();
790                for source in &row.source_record_uuids {
791                    writer.raw(source.as_bytes()).unwrap();
792                }
793                writer.raw(row.provenance_uuid.as_bytes()).unwrap();
794                writer.i64(row.recorded_at_micros).unwrap();
795                writer.u32(row.contract_version).unwrap();
796            }
797        }
798        fingerprint(
799            CanonicalDomain::BeliefProjectionAttachment,
800            CANONICAL_CONTRACT_VERSION,
801            &writer.finish(),
802        )
803        .unwrap()
804        .iter()
805        .map(|byte| format!("{byte:02x}"))
806        .collect()
807    }
808
809    #[test]
810    fn sources_are_canonical_and_arrow_round_trip_is_exact() {
811        let row = attachment(1, vec![uuid7(9), uuid7(8), uuid7(9)]);
812        assert_eq!(row.source_record_uuids, vec![uuid7(8), uuid7(9)]);
813        let ledger = BeliefProjectionAttachmentLedger::new(vec![row]).unwrap();
814        let decoded =
815            BeliefProjectionAttachmentLedger::from_batches(&[ledger.batch().unwrap()]).unwrap();
816        assert_eq!(decoded, ledger);
817        assert_eq!(
818            decoded.attachment_fingerprint(uuid7(1)).unwrap(),
819            ledger.attachment_fingerprint(uuid7(1)).unwrap()
820        );
821    }
822
823    #[test]
824    fn replay_is_idempotent_and_conflict_has_transaction_code() {
825        let row = attachment(10, vec![]);
826        let ledger = BeliefProjectionAttachmentLedger::new(vec![row.clone()]).unwrap();
827        assert_eq!(
828            ledger
829                .merge(&BeliefProjectionAttachmentLedger::new(vec![row.clone()]).unwrap())
830                .unwrap(),
831            ledger
832        );
833        let mut different = row;
834        different.graph_content_fingerprint = [99; 32];
835        let error = ledger
836            .merge(&BeliefProjectionAttachmentLedger::new(vec![different]).unwrap())
837            .unwrap_err();
838        assert_eq!(error.code(), "GF_TRANSACTION_CONFLICT");
839    }
840
841    #[test]
842    fn registry_is_epistemic_owned_and_frozen() {
843        let entry = schema_registry_entry();
844        assert_eq!(entry.capability_id, "epistemic");
845        assert_eq!(entry.record_family, "algorithm_interpretation_attachments");
846        assert_eq!(entry.sort_key, &["recorded_at", "attachment_uuid"]);
847        assert_eq!(entry.implementation_issue, 2004);
848    }
849
850    #[test]
851    fn defensive_attachment_validation_and_optional_fingerprints_are_exact() {
852        let row = attachment(30, vec![uuid7(40)]);
853        assert!(matches!(
854            BeliefProjectionAttachmentLedger::new(vec![row.clone(), row.clone()]),
855            Err(KnowledgeError::Duplicate("attachment_uuid"))
856        ));
857
858        let mut invalid = row.clone();
859        invalid.contract_version += 1;
860        assert!(
861            validate(&invalid)
862                .unwrap_err()
863                .to_string()
864                .contains("unsupported version")
865        );
866        invalid = row.clone();
867        invalid.policy_version = 0;
868        assert!(
869            validate(&invalid)
870                .unwrap_err()
871                .to_string()
872                .contains("must be positive")
873        );
874        invalid = row.clone();
875        invalid.policy_fingerprint = [0; 32];
876        assert!(
877            validate(&invalid)
878                .unwrap_err()
879                .to_string()
880                .contains("does not match policy bytes")
881        );
882        invalid = row.clone();
883        invalid.source_record_uuids = vec![uuid7(42), uuid7(41)];
884        assert!(
885            validate(&invalid)
886                .unwrap_err()
887                .to_string()
888                .contains("sorted and deduplicated")
889        );
890        invalid = row.clone();
891        invalid.source_record_uuids = vec![uuid7(41), uuid7(41)];
892        assert!(
893            validate(&invalid)
894                .unwrap_err()
895                .to_string()
896                .contains("sorted and deduplicated")
897        );
898        invalid = row;
899        invalid.source_record_uuids = vec![Uuid::nil()];
900        assert!(
901            validate(&invalid)
902                .unwrap_err()
903                .to_string()
904                .contains("must not be nil")
905        );
906
907        let without_optional = BeliefProjectionAttachment::new(
908            uuid7(50),
909            uuid7(51),
910            uuid7(52),
911            10,
912            None,
913            1,
914            b"policy-v1".to_vec(),
915            [3; 32],
916            None,
917            [5; 32],
918            [6; 32],
919            vec![],
920            uuid7(53),
921            20,
922        )
923        .unwrap();
924        BeliefProjectionAttachmentLedger::new(vec![without_optional])
925            .unwrap()
926            .attachment_fingerprint(uuid7(50))
927            .unwrap();
928
929        let wrong = RecordBatch::new_empty(Arc::new(Schema::empty()));
930        assert!(
931            BeliefProjectionAttachmentLedger::from_batches(&[wrong])
932                .unwrap_err()
933                .to_string()
934                .contains("schema mismatch")
935        );
936    }
937
938    #[test]
939    fn attachment_and_run_uuids_must_be_v7() {
940        let row = attachment(31, vec![]);
941        let mut invalid = row.clone();
942        invalid.attachment_uuid = non_v7_uuid(1);
943        assert!(
944            validate(&invalid)
945                .unwrap_err()
946                .to_string()
947                .contains("must be UUIDv7")
948        );
949        invalid = row;
950        invalid.run_uuid = non_v7_uuid(2);
951        assert!(
952            validate(&invalid)
953                .unwrap_err()
954                .to_string()
955                .contains("must be UUIDv7")
956        );
957    }
958
959    #[test]
960    fn attachment_contract_version_must_be_supported() {
961        let mut invalid = attachment(40, vec![]);
962        invalid.contract_version = BELIEF_PROJECTION_ATTACHMENT_CONTRACT_VERSION + 1;
963        assert!(
964            validate(&invalid)
965                .unwrap_err()
966                .to_string()
967                .contains("unsupported version")
968        );
969    }
970
971    #[test]
972    fn duplicate_attachment_uuid_is_rejected() {
973        let first = attachment(41, vec![]);
974        let mut duplicate = first.clone();
975        duplicate.recorded_at_micros += 1;
976        assert!(matches!(
977            BeliefProjectionAttachmentLedger::new(vec![first, duplicate]),
978            Err(KnowledgeError::Duplicate("attachment_uuid"))
979        ));
980    }
981
982    #[test]
983    fn source_generation_and_provenance_uuids_must_be_non_nil() {
984        let row = attachment(32, vec![]);
985        let mut invalid = row.clone();
986        invalid.source_generation_uuid = Uuid::nil();
987        assert!(
988            validate(&invalid)
989                .unwrap_err()
990                .to_string()
991                .contains("must not be nil")
992        );
993        invalid = row;
994        invalid.provenance_uuid = Uuid::nil();
995        assert!(
996            validate(&invalid)
997                .unwrap_err()
998                .to_string()
999                .contains("must not be nil")
1000        );
1001    }
1002
1003    #[test]
1004    fn policy_version_must_be_positive() {
1005        let mut invalid = attachment(33, vec![]);
1006        invalid.policy_version = 0;
1007        assert!(
1008            validate(&invalid)
1009                .unwrap_err()
1010                .to_string()
1011                .contains("must be positive")
1012        );
1013    }
1014
1015    #[test]
1016    fn policy_byte_limit_rejects_the_first_excess_byte() {
1017        let maximum = usize::try_from(MAX_CANONICAL_BINARY_BYTES).unwrap();
1018        assert!(validate_policy_len(maximum).is_ok());
1019        assert!(matches!(
1020            validate_policy_len(maximum + 1),
1021            Err(KnowledgeError::Limit {
1022                participant: "policy_bytes",
1023                observed,
1024                limit,
1025            }) if observed == maximum + 1 && limit == maximum
1026        ));
1027        let mut invalid = attachment(39, vec![]);
1028        invalid.policy_bytes = vec![0; maximum + 1];
1029        assert!(
1030            validate(&invalid)
1031                .unwrap_err()
1032                .to_string()
1033                .contains("policy_bytes")
1034        );
1035    }
1036
1037    #[test]
1038    fn policy_fingerprint_must_match_policy_bytes() {
1039        let mut invalid = attachment(34, vec![]);
1040        invalid.policy_fingerprint = [0; 32];
1041        assert!(
1042            validate(&invalid)
1043                .unwrap_err()
1044                .to_string()
1045                .contains("does not match policy bytes")
1046        );
1047    }
1048
1049    #[test]
1050    fn source_record_uuids_must_be_sorted_unique_and_non_nil() {
1051        let row = attachment(35, vec![uuid7(40), uuid7(41)]);
1052        let mut invalid = row.clone();
1053        invalid.source_record_uuids = vec![uuid7(41), uuid7(40)];
1054        assert!(
1055            validate(&invalid)
1056                .unwrap_err()
1057                .to_string()
1058                .contains("sorted and deduplicated")
1059        );
1060        invalid = row.clone();
1061        invalid.source_record_uuids = vec![uuid7(40), uuid7(40)];
1062        assert!(
1063            validate(&invalid)
1064                .unwrap_err()
1065                .to_string()
1066                .contains("sorted and deduplicated")
1067        );
1068        invalid = row;
1069        invalid.source_record_uuids = vec![Uuid::nil()];
1070        assert!(
1071            validate(&invalid)
1072                .unwrap_err()
1073                .to_string()
1074                .contains("must not be nil")
1075        );
1076    }
1077
1078    #[test]
1079    fn row_limit_constructor_and_merge_reject_first_excess() {
1080        let rows = vec![attachment(36, vec![]), attachment(37, vec![])];
1081        assert!(BeliefProjectionAttachmentLedger::new_with_limit(rows.clone(), 2).is_ok());
1082        assert!(matches!(
1083            BeliefProjectionAttachmentLedger::new_with_limit(rows.clone(), 1),
1084            Err(KnowledgeError::Limit {
1085                observed: 2,
1086                limit: 1,
1087                ..
1088            })
1089        ));
1090        let existing = BeliefProjectionAttachmentLedger::new_with_limit(rows, 2).unwrap();
1091        let staged = BeliefProjectionAttachmentLedger::new(vec![attachment(38, vec![])]).unwrap();
1092        assert!(matches!(
1093            existing.merge_with_limit(&staged, 2),
1094            Err(KnowledgeError::Limit {
1095                observed: 3,
1096                limit: 2,
1097                ..
1098            })
1099        ));
1100        let replay =
1101            BeliefProjectionAttachmentLedger::new(vec![existing.attachments()[0].clone()]).unwrap();
1102        assert_eq!(existing.merge_with_limit(&replay, 2).unwrap(), existing);
1103
1104        let novel = attachment_at(39, 10);
1105        let mut conflicting = attachment_at(36, 30);
1106        conflicting.graph_content_fingerprint = [99; 32];
1107        let staged = BeliefProjectionAttachmentLedger::new(vec![novel, conflicting]).unwrap();
1108        assert!(matches!(
1109            existing.merge_with_limit(&staged, 2),
1110            Err(KnowledgeError::TransactionConflict("attachment_uuid"))
1111        ));
1112    }
1113
1114    #[test]
1115    fn incremental_merge_checks_staged_rows_once_and_never_existing_rows() {
1116        let first = attachment(60, vec![]);
1117        let replay = first.clone();
1118        let later = attachment_at(70, 30);
1119        let earlier = attachment_at(80, 15);
1120        reset_validation_calls();
1121        let existing = BeliefProjectionAttachmentLedger::new(vec![first]).unwrap();
1122        assert_eq!(validation_call_count(), 1);
1123        reset_validation_calls();
1124        let staged = BeliefProjectionAttachmentLedger::new(vec![later, replay, earlier]).unwrap();
1125        assert_eq!(validation_call_count(), 3);
1126        reset_validation_calls();
1127        let merged = existing.merge(&staged).unwrap();
1128        assert_eq!(validation_call_count(), 0);
1129        assert_eq!(
1130            merged
1131                .attachments()
1132                .iter()
1133                .map(|r| r.attachment_uuid)
1134                .collect::<Vec<_>>(),
1135            vec![uuid7(80), uuid7(60), uuid7(70)]
1136        );
1137        assert_eq!(merged.clone().into_attachments().len(), 3);
1138    }
1139
1140    #[test]
1141    fn constructor_validates_each_new_row_once() {
1142        let rows = vec![attachment(100, vec![]), attachment(101, vec![])];
1143        reset_validation_calls();
1144        let ledger = BeliefProjectionAttachmentLedger::new(rows).unwrap();
1145        assert_eq!(ledger.attachments().len(), 2);
1146        assert_eq!(validation_call_count(), 2);
1147    }
1148
1149    #[test]
1150    fn constructor_sorts_rows_by_recorded_time_then_uuid() {
1151        let late = attachment_at(110, 30);
1152        let same_time_lower_uuid = attachment_at(100, 30);
1153        let same_time_higher_uuid = attachment_at(120, 30);
1154        let ledger = BeliefProjectionAttachmentLedger::new(vec![
1155            same_time_higher_uuid,
1156            late,
1157            same_time_lower_uuid,
1158        ])
1159        .unwrap();
1160        assert_eq!(
1161            ledger
1162                .attachments()
1163                .iter()
1164                .map(|row| row.attachment_uuid)
1165                .collect::<Vec<_>>(),
1166            vec![uuid7(100), uuid7(110), uuid7(120)]
1167        );
1168    }
1169
1170    #[test]
1171    #[ignore = "manual quiet-host before/after merge-cost measurement"]
1172    fn quiet_host_merge_cost_measurement() {
1173        for existing_count in [1_000_u64, 10_000] {
1174            let existing = BeliefProjectionAttachmentLedger::new(
1175                (0..existing_count).map(benchmark_attachment).collect(),
1176            )
1177            .unwrap();
1178            let mut staged_row = benchmark_attachment(existing_count + 10);
1179            staged_row.recorded_at_micros = i64::try_from(existing_count / 2).unwrap();
1180            let staged = BeliefProjectionAttachmentLedger::new(vec![staged_row]).unwrap();
1181            let baseline = merge_with_full_revalidation(&existing, &staged).unwrap();
1182            let incremental = existing.merge(&staged).unwrap();
1183            assert_eq!(baseline, incremental);
1184            let repetitions = 9;
1185            let mut before = Vec::with_capacity(repetitions);
1186            let mut after = Vec::with_capacity(repetitions);
1187            for _ in 0..repetitions {
1188                let start = Instant::now();
1189                std::hint::black_box(merge_with_full_revalidation(&existing, &staged).unwrap());
1190                before.push(start.elapsed());
1191                let start = Instant::now();
1192                std::hint::black_box(existing.merge(&staged).unwrap());
1193                after.push(start.elapsed());
1194            }
1195            let before_median = median(before);
1196            let after_median = median(after);
1197            let divisor = existing_count as f64;
1198            println!(
1199                "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}",
1200                input_digest(&existing, &staged),
1201                before_median.as_nanos(),
1202                before_median.as_nanos() as f64 / divisor,
1203                after_median.as_nanos(),
1204                after_median.as_nanos() as f64 / divisor,
1205            );
1206        }
1207    }
1208}