Skip to main content

zeppelin_embed/graph/
build.rs

1//! Single-threaded flat Vamana graph construction.
2
3use std::path::{Path, PathBuf};
4
5use xxhash_rust::xxh3::xxh3_64;
6
7use crate::graph::GraphParams;
8use crate::graph::block::{
9    GraphNodeBlockBuild, GraphNodeBlockInput, GraphNodeError, GraphNodeLayout,
10    NODE_BLOCK_TRAILER_LEN, encode_node_blocks,
11};
12use crate::lifecycle::QueryCancellation;
13use crate::lifecycle::durability::{DurabilityPolicy, SyncRequirement};
14use crate::lifecycle::stats::{AccountedCounter, Accounting, AllocationComponent};
15use crate::lifecycle::{QueryControl, SnapshotLease, Store, StoreError};
16use crate::quant::{Bit4Factors, QuantError, est_dot_bit4, prepare_bit4_query};
17use crate::scan::ScanError;
18use crate::segment::layout::RegionKind;
19use crate::segment::reader::SegmentReader;
20use crate::segment::writer::{
21    SegmentBuild, SegmentCopiedRegion, SegmentDocumentVersions, SegmentFactors,
22    write_segment_with_encoded_graph_and_copied_regions,
23};
24use crate::segment::{SegmentError, SegmentId, SegmentMeta};
25use crate::vfs::Vfs;
26
27const ENTRY_POINT_COUNT: usize = 4;
28const MEDOID_SAMPLE_ROWS: usize = 1_000;
29const CHECKPOINT_MAGIC: [u8; 8] = *b"ZEVAMCP1";
30const CHECKPOINT_VERSION: u16 = 1;
31const CHECKPOINT_HEADER_BYTES: usize = 80;
32const CHECKPOINT_CHECKSUM_BYTES: usize = 8;
33const CANCELLATION_CHECK_ROWS: usize = 64;
34
35/// How graph promotion accounts for one source directory region.
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub(crate) enum GraphRewriteRegion {
38    /// The graph writer reconstructs this region from validated typed input.
39    Reencoded,
40    /// The graph writer replaces this derived region with a newly built one.
41    Replaced,
42    /// The graph writer copies the exact source version and payload bytes.
43    CopyForward,
44    /// Promotion must remain deferred because this region is not carried.
45    Unsupported,
46}
47
48/// Defines the single guard/rewrite contract for every source region kind.
49pub(crate) const fn graph_rewrite_source_region(kind: u16) -> GraphRewriteRegion {
50    match RegionKind::from_id(kind) {
51        Some(
52            RegionKind::Columns
53            | RegionKind::Alive
54            | RegionKind::VectorCodes
55            | RegionKind::VectorFactors
56            | RegionKind::VectorRescore
57            | RegionKind::DocumentVersions,
58        ) => GraphRewriteRegion::Reencoded,
59        Some(RegionKind::GraphNodeBlocks | RegionKind::ChecksumTable) => {
60            GraphRewriteRegion::Replaced
61        }
62        Some(RegionKind::Postings | RegionKind::StoredMetadata | RegionKind::StoredText) | None => {
63            GraphRewriteRegion::CopyForward
64        }
65        Some(
66            RegionKind::GraphColocatedCodes
67            | RegionKind::SignPlane
68            | RegionKind::PdxClusteredBlocks
69            | RegionKind::VectorSpaceN,
70        ) => GraphRewriteRegion::Unsupported,
71    }
72}
73
74/// Number of alpha-pruning passes applied by one measurement/build request.
75#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
76pub enum GraphBuildPasses {
77    /// The shipped alpha=1.0 build: faster at matched recall across 10 processes.
78    /// Provenance: `docs/19-m5-defaults.md`.
79    #[default]
80    One,
81    /// Explicit research arm; M5 found its hop reduction did not pay for itself.
82    Two,
83}
84
85/// Completed owned graph bytes ready for the existing node-block writer.
86pub struct GraphBuildArtifact {
87    encoded_region: Vec<u8>,
88    entry_points: Vec<u32>,
89    work_rows_completed: u64,
90    _memory: Option<AccountedCounter>,
91}
92
93impl GraphBuildArtifact {
94    /// Returns the complete fixed-stride graph region for deterministic evidence.
95    #[must_use]
96    pub fn encoded_region(&self) -> &[u8] {
97        &self.encoded_region
98    }
99
100    /// Returns the medoid followed by up to three deterministic refined seeds.
101    #[must_use]
102    pub fn entry_points(&self) -> &[u32] {
103        &self.entry_points
104    }
105
106    /// Returns rows advanced by this checkpointed invocation, excluding prior work.
107    #[must_use]
108    pub const fn work_rows_completed(&self) -> u64 {
109        self.work_rows_completed
110    }
111
112    /// Atomically publishes the sealed input plus this graph through M2's writer.
113    pub fn write_segment_with_graph(
114        &self,
115        vfs: &dyn Vfs,
116        directory: &Path,
117        input: &SegmentReader,
118        output_id: SegmentId,
119        policy: DurabilityPolicy,
120    ) -> Result<SegmentMeta, GraphBuildError> {
121        write_segment_with_encoded_graph(
122            &self.encoded_region,
123            vfs,
124            directory,
125            input,
126            output_id,
127            policy,
128        )
129    }
130}
131
132pub(crate) fn write_segment_with_encoded_graph(
133    encoded_region: &[u8],
134    vfs: &dyn Vfs,
135    directory: &Path,
136    input: &SegmentReader,
137    output_id: SegmentId,
138    policy: DurabilityPolicy,
139) -> Result<SegmentMeta, GraphBuildError> {
140    let columns = input.columns()?;
141    let alive = input.alive()?;
142    let factors = input.bit4_factors()?;
143    let mut doc_ids = Vec::with_capacity(input.meta().row_count as usize);
144    let mut revisions = Vec::with_capacity(input.meta().row_count as usize);
145    let mut has_documents = None;
146    for row in 0..input.meta().row_count as usize {
147        let document = input.document_version(row)?;
148        let present = document.is_some();
149        if has_documents.is_some_and(|expected| expected != present) {
150            return Err(GraphBuildError::Segment(SegmentError::Geometry(
151                "document-version region is present for only part of the segment".to_owned(),
152            )));
153        }
154        has_documents = Some(present);
155        if let Some(document) = document {
156            doc_ids.push(document.doc_id());
157            revisions.push(document.revision());
158        }
159    }
160    let build = SegmentBuild {
161        id: output_id,
162        scheme: input.meta().scheme,
163        dims: input.meta().dims,
164        codes: input.bit4_codes()?,
165        factors: SegmentFactors::Bit4(factors),
166        rescore: input.rescore_f32()?,
167        columns: &columns,
168        alive: &alive,
169    };
170    let documents = (has_documents == Some(true)).then_some(SegmentDocumentVersions {
171        doc_ids: &doc_ids,
172        revisions: &revisions,
173    });
174    let mut copied_regions = Vec::new();
175    for entry in input.directory() {
176        match graph_rewrite_source_region(entry.kind) {
177            GraphRewriteRegion::CopyForward => {
178                copied_regions.push(SegmentCopiedRegion {
179                    kind: entry.kind,
180                    version: entry.version,
181                    bytes: input.region_by_id(entry.kind)?,
182                });
183            }
184            GraphRewriteRegion::Unsupported => {
185                return Err(GraphBuildError::Segment(SegmentError::Geometry(format!(
186                    "graph rewrite cannot carry source region kind {}",
187                    entry.kind
188                ))));
189            }
190            GraphRewriteRegion::Reencoded | GraphRewriteRegion::Replaced => {}
191        }
192    }
193    write_segment_with_encoded_graph_and_copied_regions(
194        vfs,
195        directory,
196        build,
197        encoded_region,
198        documents,
199        &copied_regions,
200        policy,
201    )
202    .map_err(GraphBuildError::Segment)
203}
204
205/// Typed failure from construction before any graph artifact is published.
206#[derive(Debug)]
207pub enum GraphBuildError {
208    /// The pinned epoch has no measured graph-construction profile.
209    Profile(crate::graph::search::GraphProfileError),
210    /// Reading, replacing, or removing the resumable checkpoint failed.
211    CheckpointIo {
212        /// Checkpoint or checkpoint-temporary path.
213        path: PathBuf,
214        /// Underlying filesystem failure.
215        source: std::io::Error,
216    },
217    /// Checkpoint bytes failed their closed contract and were removed for a fresh retry.
218    CheckpointCorrupt(String),
219    /// A caller work limit stopped after atomically checkpointing completed batches.
220    BudgetExhausted {
221        /// Rows advanced by this invocation and persisted in the checkpoint.
222        rows_completed: u64,
223    },
224    /// An angular segment carried a finite f32 row outside the unit-norm band.
225    UnitNorm {
226        /// Dense segment-local row that violated the epoch declaration.
227        row_id: u32,
228        /// Squared L2 norm computed from the persisted f32 row.
229        squared_norm: f64,
230        /// Absolute permitted deviation from a squared norm of one.
231        tolerance: f64,
232    },
233    /// Caller cancellation stopped a batch without publishing partial results.
234    Cancelled {
235        /// Permanently false: graph builds never return partial artifacts.
236        partial: bool,
237    },
238    /// A deadline stopped a batch without publishing partial results.
239    Timeout {
240        /// Permanently false: graph builds never return partial artifacts.
241        partial: bool,
242    },
243    /// Store close stopped a batch before its snapshot could be released.
244    ReadCancelled {
245        /// Permanently false: graph builds never return partial artifacts.
246        partial: bool,
247    },
248    /// The sealed segment did not expose valid Bit4 or f32 regions.
249    Segment(SegmentError),
250    /// A Bit4 query or score failed validation.
251    Quant(QuantError),
252    /// The frozen node-block writer rejected constructed values.
253    NodeBlock(GraphNodeError),
254    /// Checked row, dimension, or allocation geometry was invalid.
255    Geometry(String),
256    /// A graph node id was checked before address arithmetic and was out of range.
257    NodeIdOutOfRange {
258        /// Rejected dense row id.
259        node_id: u32,
260        /// Valid dense row count.
261        node_count: u32,
262    },
263    /// Store budget or accounting rejected the graph construction arena.
264    Store(StoreError),
265}
266
267impl std::fmt::Display for GraphBuildError {
268    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
269        match self {
270            Self::Profile(error) => write!(formatter, "graph build {error}"),
271            Self::CheckpointIo { path, source } => {
272                write!(
273                    formatter,
274                    "graph build checkpoint I/O {}: {source}",
275                    path.display()
276                )
277            }
278            Self::CheckpointCorrupt(detail) => {
279                write!(formatter, "graph build checkpoint is corrupt: {detail}")
280            }
281            Self::BudgetExhausted { rows_completed } => write!(
282                formatter,
283                "graph build work budget exhausted after {rows_completed} checkpointed rows"
284            ),
285            Self::UnitNorm {
286                row_id,
287                squared_norm,
288                tolerance,
289            } => write!(
290                formatter,
291                "angular graph row {row_id} has squared norm {squared_norm}; expected 1 within {tolerance}"
292            ),
293            Self::Cancelled { partial } => {
294                write!(formatter, "graph build was cancelled (partial={partial})")
295            }
296            Self::Timeout { partial } => {
297                write!(
298                    formatter,
299                    "graph build deadline expired (partial={partial})"
300                )
301            }
302            Self::ReadCancelled { partial } => write!(
303                formatter,
304                "store close cancelled graph build (partial={partial})"
305            ),
306            Self::Segment(error) => write!(formatter, "graph build segment input: {error}"),
307            Self::Quant(error) => write!(formatter, "graph build Bit4 score: {error}"),
308            Self::NodeBlock(error) => write!(formatter, "graph build node blocks: {error}"),
309            Self::Geometry(detail) => write!(formatter, "graph build geometry: {detail}"),
310            Self::NodeIdOutOfRange {
311                node_id,
312                node_count,
313            } => write!(
314                formatter,
315                "graph build node id {node_id} is outside dense row count {node_count}"
316            ),
317            Self::Store(error) => write!(formatter, "graph build memory accounting: {error}"),
318        }
319    }
320}
321
322impl std::error::Error for GraphBuildError {
323    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
324        match self {
325            Self::Profile(error) => Some(error),
326            Self::CheckpointIo { source, .. } => Some(source),
327            Self::Segment(error) => Some(error),
328            Self::Quant(error) => Some(error),
329            Self::NodeBlock(error) => Some(error),
330            Self::Store(error) => Some(error),
331            Self::CheckpointCorrupt(_)
332            | Self::BudgetExhausted { .. }
333            | Self::UnitNorm { .. }
334            | Self::Cancelled { .. }
335            | Self::Timeout { .. }
336            | Self::ReadCancelled { .. }
337            | Self::Geometry(_)
338            | Self::NodeIdOutOfRange { .. } => None,
339        }
340    }
341}
342
343struct GraphBuildSession<'a> {
344    vfs: &'a dyn Vfs,
345    policy: DurabilityPolicy,
346    vectors: SegmentVectors<'a>,
347    params: GraphParams,
348    seed: u64,
349    passes: GraphBuildPasses,
350    checkpoint_path: PathBuf,
351    state: BuildState,
352}
353
354impl<'a> GraphBuildSession<'a> {
355    #[allow(clippy::too_many_arguments)]
356    fn new(
357        vfs: &'a dyn Vfs,
358        reader: &'a SegmentReader,
359        params: GraphParams,
360        seed: u64,
361        passes: GraphBuildPasses,
362        checkpoint_path: &Path,
363        policy: DurabilityPolicy,
364        accounting: Option<&std::sync::Arc<Accounting>>,
365    ) -> Result<Self, GraphBuildError> {
366        let vectors = SegmentVectors::new(reader)?;
367        let state = match vfs.read(checkpoint_path) {
368            Ok(bytes) => {
369                match decode_checkpoint(&bytes, &vectors, params, seed, passes, accounting) {
370                    Ok(state) => state,
371                    Err(error @ GraphBuildError::CheckpointCorrupt(_)) => {
372                        remove_checkpoint(vfs, checkpoint_path)?;
373                        return Err(error);
374                    }
375                    Err(other) => return Err(other),
376                }
377            }
378            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
379                BuildState::new(&vectors, params, seed, accounting)?
380            }
381            Err(source) => {
382                return Err(GraphBuildError::CheckpointIo {
383                    path: checkpoint_path.to_path_buf(),
384                    source,
385                });
386            }
387        };
388        Ok(Self {
389            vfs,
390            policy,
391            vectors,
392            params,
393            seed,
394            passes,
395            checkpoint_path: checkpoint_path.to_path_buf(),
396            state,
397        })
398    }
399
400    fn advance_batch(
401        &mut self,
402        cancellation: &QueryCancellation<'_>,
403    ) -> Result<bool, GraphBuildError> {
404        check_cancellation(cancellation)?;
405        let complete = self.state.advance_batch(
406            &self.vectors,
407            self.params,
408            self.seed,
409            self.passes,
410            Some(cancellation),
411        )?;
412        check_cancellation(cancellation)?;
413        {
414            let checkpoint_bytes = checkpoint_encoded_bytes(&self.state)?;
415            let base_charge = match self.state.memory.as_mut() {
416                Some(charge) => {
417                    let base = charge.bytes();
418                    let peak = usize::try_from(base)
419                        .ok()
420                        .and_then(|base| base.checked_add(checkpoint_bytes))
421                        .ok_or_else(|| {
422                            GraphBuildError::Geometry("checkpoint accounting overflow".to_owned())
423                        })?;
424                    charge.set(peak)?;
425                    Some(base)
426                }
427                None => None,
428            };
429            let written = write_checkpoint(
430                self.vfs,
431                self.policy,
432                &self.checkpoint_path,
433                &self.vectors,
434                self.params,
435                self.seed,
436                self.passes,
437                &self.state,
438            );
439            if let (Some(charge), Some(base)) = (self.state.memory.as_mut(), base_charge) {
440                let base = usize::try_from(base).map_err(|_| {
441                    GraphBuildError::Geometry("base accounting exceeds usize".to_owned())
442                })?;
443                charge.set(base)?;
444            }
445            written?;
446        }
447        Ok(complete)
448    }
449
450    fn completed_work_rows(&self) -> Result<u64, GraphBuildError> {
451        let rows = u64::try_from(self.state.order.len())
452            .map_err(|_| GraphBuildError::Geometry("graph row count exceeds u64".to_owned()))?;
453        let next = u64::try_from(self.state.next_index)
454            .map_err(|_| GraphBuildError::Geometry("graph progress exceeds u64".to_owned()))?;
455        match self.state.phase {
456            BuildPhase::Build => Ok(next),
457            BuildPhase::Refine => rows
458                .checked_add(next)
459                .ok_or_else(|| GraphBuildError::Geometry("graph progress overflow".to_owned())),
460            BuildPhase::Complete => match self.passes {
461                GraphBuildPasses::One => Ok(rows),
462                GraphBuildPasses::Two => rows
463                    .checked_mul(2)
464                    .ok_or_else(|| GraphBuildError::Geometry("graph progress overflow".to_owned())),
465            },
466        }
467    }
468
469    fn artifact(self) -> Result<GraphBuildArtifact, GraphBuildError> {
470        if !self.state.is_complete(self.passes) {
471            return Err(GraphBuildError::Geometry(
472                "graph artifact requested before every pass completed".to_owned(),
473            ));
474        }
475        let BuildState {
476            entries,
477            adjacency,
478            memory,
479            ..
480        } = self.state;
481        let artifact = encode_artifact(&self.vectors, self.params, &entries, &adjacency, memory)?;
482        remove_checkpoint(self.vfs, &self.checkpoint_path)?;
483        Ok(artifact)
484    }
485}
486
487impl From<SegmentError> for GraphBuildError {
488    fn from(error: SegmentError) -> Self {
489        Self::Segment(error)
490    }
491}
492
493impl From<QuantError> for GraphBuildError {
494    fn from(error: QuantError) -> Self {
495        Self::Quant(error)
496    }
497}
498
499impl From<GraphNodeError> for GraphBuildError {
500    fn from(error: GraphNodeError) -> Self {
501        Self::NodeBlock(error)
502    }
503}
504
505impl From<StoreError> for GraphBuildError {
506    fn from(error: StoreError) -> Self {
507        Self::Store(error)
508    }
509}
510
511#[cfg(test)]
512fn build_graph(
513    reader: &SegmentReader,
514    params: GraphParams,
515    seed: u64,
516    passes: GraphBuildPasses,
517) -> Result<GraphBuildArtifact, GraphBuildError> {
518    let vectors = SegmentVectors::new(reader)?;
519    let mut state = BuildState::new(&vectors, params, seed, None)?;
520    while !state.advance_batch(&vectors, params, seed, passes, None)? {}
521    let BuildState {
522        entries,
523        adjacency,
524        memory,
525        ..
526    } = state;
527    encode_artifact(&vectors, params, &entries, &adjacency, memory)
528}
529
530/// Validated controls for an interruptible graph build.
531#[derive(Clone, Copy, Debug)]
532pub struct CheckpointedGraphBuild<'a> {
533    params: GraphParams,
534    seed: u64,
535    passes: GraphBuildPasses,
536    checkpoint_path: &'a Path,
537    control: &'a QueryControl,
538    max_work_rows: Option<u64>,
539}
540
541impl<'a> CheckpointedGraphBuild<'a> {
542    /// Packages the validated graph parameters with checkpoint and cancellation controls.
543    #[must_use]
544    pub fn new(
545        params: GraphParams,
546        seed: u64,
547        passes: GraphBuildPasses,
548        checkpoint_path: &'a Path,
549        control: &'a QueryControl,
550    ) -> Self {
551        Self {
552            params,
553            seed,
554            passes,
555            checkpoint_path,
556            control,
557            max_work_rows: None,
558        }
559    }
560
561    /// Stops after checkpointing at least this many rows in the current invocation.
562    ///
563    /// Callers that need a strict row limit align it to `checkpoint_batch_rows`.
564    #[must_use]
565    pub const fn with_max_work_rows(mut self, max_work_rows: u64) -> Self {
566        self.max_work_rows = Some(max_work_rows);
567        self
568    }
569}
570
571/// Builds or resumes from a checkpoint while reusing Task 09-D cancellation.
572///
573/// The checkpoint is replaced atomically after every completed batch. No graph
574/// artifact is returned on cancellation, timeout, or store-close cancellation.
575/// A corrupt checkpoint is reported once and removed; the next invocation starts
576/// a fresh build.
577pub fn build_graph_checkpointed(
578    store: &Store,
579    reader: &SegmentReader,
580    request: CheckpointedGraphBuild<'_>,
581    lease: &SnapshotLease,
582) -> Result<GraphBuildArtifact, GraphBuildError> {
583    let cancellation = QueryCancellation::new(request.control, lease);
584    check_cancellation(&cancellation)?;
585    if lease.graph_profile() == Ok(crate::graph::search::EpochGraphProfile::AngularClass) {
586        validate_angular_rows(reader)?;
587    }
588    let mut session = GraphBuildSession::new(
589        store.vfs.as_ref(),
590        reader,
591        request.params,
592        request.seed,
593        request.passes,
594        request.checkpoint_path,
595        store.durability_policy,
596        Some(&store.accounting),
597    )?;
598    let started_rows = session.completed_work_rows()?;
599    loop {
600        let complete = session.advance_batch(&cancellation)?;
601        let completed_rows = session
602            .completed_work_rows()?
603            .checked_sub(started_rows)
604            .ok_or_else(|| GraphBuildError::Geometry("graph work progress regressed".to_owned()))?;
605        if complete {
606            let mut artifact = session.artifact()?;
607            artifact.work_rows_completed = completed_rows;
608            return Ok(artifact);
609        }
610        if request
611            .max_work_rows
612            .is_some_and(|maximum| completed_rows >= maximum)
613        {
614            return Err(GraphBuildError::BudgetExhausted {
615                rows_completed: completed_rows,
616            });
617        }
618    }
619}
620
621fn validate_angular_rows(reader: &SegmentReader) -> Result<(), GraphBuildError> {
622    let dimensions = reader.meta().dims as usize;
623    for (row, vector) in reader.rescore_f32()?.chunks_exact(dimensions).enumerate() {
624        if let Some(squared_norm) = crate::graph::search::non_unit_squared_norm(vector) {
625            return Err(GraphBuildError::UnitNorm {
626                row_id: u32::try_from(row).map_err(|_| {
627                    GraphBuildError::Geometry("angular row id exceeds u32".to_owned())
628                })?,
629                squared_norm,
630                tolerance: crate::graph::search::UNIT_NORM_SQUARED_TOLERANCE,
631            });
632        }
633    }
634    Ok(())
635}
636
637struct SegmentVectors<'a> {
638    segment_id: [u8; 16],
639    node_count: u32,
640    dimensions: usize,
641    code_stride: usize,
642    codes: &'a [u8],
643    factors: &'a [Bit4Factors],
644    rescore: &'a [f32],
645}
646
647impl<'a> SegmentVectors<'a> {
648    fn new(reader: &'a SegmentReader) -> Result<Self, GraphBuildError> {
649        if reader.meta().scheme != 4 {
650            return Err(GraphBuildError::Geometry(format!(
651                "flat Vamana requires Bit4 scheme 4, got {}",
652                reader.meta().scheme
653            )));
654        }
655        let dimensions = reader.meta().dims as usize;
656        let node_count = reader.meta().row_count;
657        if node_count == 0 {
658            return Err(GraphBuildError::Geometry(
659                "flat Vamana requires at least one row".to_owned(),
660            ));
661        }
662        Ok(Self {
663            segment_id: *reader.meta().id.as_bytes(),
664            node_count,
665            dimensions,
666            code_stride: dimensions.div_ceil(2),
667            codes: reader.bit4_codes()?,
668            factors: reader.bit4_factors()?,
669            rescore: reader.rescore_f32()?,
670        })
671    }
672
673    fn validate_node(&self, node_id: u32) -> Result<usize, GraphBuildError> {
674        if node_id >= self.node_count {
675            return Err(GraphBuildError::NodeIdOutOfRange {
676                node_id,
677                node_count: self.node_count,
678            });
679        }
680        usize::try_from(node_id)
681            .map_err(|_| GraphBuildError::Geometry("node id exceeds usize".to_owned()))
682    }
683
684    fn f32_row(&self, node_id: u32) -> Result<&'a [f32], GraphBuildError> {
685        let node = self.validate_node(node_id)?;
686        let start = node
687            .checked_mul(self.dimensions)
688            .ok_or_else(|| GraphBuildError::Geometry("f32 row offset overflow".to_owned()))?;
689        let end = start
690            .checked_add(self.dimensions)
691            .ok_or_else(|| GraphBuildError::Geometry("f32 row end overflow".to_owned()))?;
692        self.rescore.get(start..end).ok_or_else(|| {
693            GraphBuildError::Geometry(format!("f32 row {node_id} is outside validated rescore"))
694        })
695    }
696
697    fn code_row(&self, node_id: u32) -> Result<&'a [u8], GraphBuildError> {
698        let node = self.validate_node(node_id)?;
699        let start = node
700            .checked_mul(self.code_stride)
701            .ok_or_else(|| GraphBuildError::Geometry("Bit4 row offset overflow".to_owned()))?;
702        let end = start
703            .checked_add(self.code_stride)
704            .ok_or_else(|| GraphBuildError::Geometry("Bit4 row end overflow".to_owned()))?;
705        self.codes.get(start..end).ok_or_else(|| {
706            GraphBuildError::Geometry(format!("Bit4 row {node_id} is outside validated codes"))
707        })
708    }
709
710    fn factor(&self, node_id: u32) -> Result<Bit4Factors, GraphBuildError> {
711        let node = self.validate_node(node_id)?;
712        self.factors.get(node).copied().ok_or_else(|| {
713            GraphBuildError::Geometry(format!("Bit4 factor {node_id} is unavailable"))
714        })
715    }
716
717    fn exact_distance(&self, left: u32, right: u32) -> Result<f64, GraphBuildError> {
718        let left = self.f32_row(left)?;
719        let right = self.f32_row(right)?;
720        Ok(left
721            .iter()
722            .zip(right)
723            .map(|(left, right)| {
724                let difference = f64::from(*left) - f64::from(*right);
725                difference * difference
726            })
727            .sum())
728    }
729}
730
731struct Adjacency {
732    slots: Vec<u32>,
733    degrees: Vec<u8>,
734    node_count: u32,
735    r_max: u8,
736}
737
738impl Adjacency {
739    fn new(node_count: u32, r_max: u8) -> Result<Self, GraphBuildError> {
740        let slot_count = (node_count as usize)
741            .checked_mul(usize::from(r_max))
742            .ok_or_else(|| GraphBuildError::Geometry("adjacency size overflow".to_owned()))?;
743        Ok(Self {
744            slots: vec![u32::MAX; slot_count],
745            degrees: vec![0; node_count as usize],
746            node_count,
747            r_max,
748        })
749    }
750
751    fn row_range(&self, node_id: u32) -> Result<std::ops::Range<usize>, GraphBuildError> {
752        if node_id >= self.node_count {
753            return Err(GraphBuildError::NodeIdOutOfRange {
754                node_id,
755                node_count: self.node_count,
756            });
757        }
758        let start = (node_id as usize)
759            .checked_mul(usize::from(self.r_max))
760            .ok_or_else(|| GraphBuildError::Geometry("adjacency offset overflow".to_owned()))?;
761        let end = start
762            .checked_add(usize::from(self.r_max))
763            .ok_or_else(|| GraphBuildError::Geometry("adjacency end overflow".to_owned()))?;
764        Ok(start..end)
765    }
766
767    fn neighbors(&self, node_id: u32) -> Result<&[u32], GraphBuildError> {
768        let node = usize::try_from(node_id)
769            .map_err(|_| GraphBuildError::Geometry("node id exceeds usize".to_owned()))?;
770        let degree = usize::from(*self.degrees.get(node).ok_or(
771            GraphBuildError::NodeIdOutOfRange {
772                node_id,
773                node_count: self.node_count,
774            },
775        )?);
776        let range = self.row_range(node_id)?;
777        let start = range.start;
778        let active_end = start
779            .checked_add(degree)
780            .ok_or_else(|| GraphBuildError::Geometry("degree end overflow".to_owned()))?;
781        self.slots.get(start..active_end).ok_or_else(|| {
782            GraphBuildError::Geometry(format!("adjacency row {node_id} is unavailable"))
783        })
784    }
785
786    fn set_neighbors(&mut self, node_id: u32, neighbors: &[u32]) -> Result<(), GraphBuildError> {
787        if neighbors.len() > usize::from(self.r_max) {
788            return Err(GraphBuildError::Geometry(format!(
789                "node {node_id} degree {} exceeds hard cap {}",
790                neighbors.len(),
791                self.r_max
792            )));
793        }
794        for neighbor in neighbors {
795            if *neighbor >= self.node_count {
796                return Err(GraphBuildError::NodeIdOutOfRange {
797                    node_id: *neighbor,
798                    node_count: self.node_count,
799                });
800            }
801        }
802        let range = self.row_range(node_id)?;
803        let row = self.slots.get_mut(range).ok_or_else(|| {
804            GraphBuildError::Geometry(format!("adjacency row {node_id} is unavailable"))
805        })?;
806        row.fill(u32::MAX);
807        let destination = row.get_mut(..neighbors.len()).ok_or_else(|| {
808            GraphBuildError::Geometry(format!("adjacency row {node_id} active slice failed"))
809        })?;
810        destination.copy_from_slice(neighbors);
811        let node = usize::try_from(node_id)
812            .map_err(|_| GraphBuildError::Geometry("node id exceeds usize".to_owned()))?;
813        let degree = self
814            .degrees
815            .get_mut(node)
816            .ok_or(GraphBuildError::NodeIdOutOfRange {
817                node_id,
818                node_count: self.node_count,
819            })?;
820        *degree = u8::try_from(neighbors.len())
821            .map_err(|_| GraphBuildError::Geometry("degree exceeds u8".to_owned()))?;
822        Ok(())
823    }
824}
825
826#[derive(Clone, Copy, Debug, Eq, PartialEq)]
827enum BuildPhase {
828    Build,
829    Refine,
830    Complete,
831}
832
833fn build_arena_bytes(node_count: u32, params: GraphParams) -> Result<usize, GraphBuildError> {
834    let rows = usize::try_from(node_count)
835        .map_err(|_| GraphBuildError::Geometry("node count exceeds usize".to_owned()))?;
836    let slots = rows
837        .checked_mul(usize::from(params.r_max()))
838        .and_then(|count| count.checked_mul(std::mem::size_of::<u32>()))
839        .ok_or_else(|| GraphBuildError::Geometry("accounted adjacency overflow".to_owned()))?;
840    let row_state = rows
841        .checked_mul(
842            std::mem::size_of::<u32>()
843                .checked_mul(2)
844                .and_then(|bytes| bytes.checked_add(2))
845                .ok_or_else(|| GraphBuildError::Geometry("row state width overflow".to_owned()))?,
846        )
847        .ok_or_else(|| GraphBuildError::Geometry("accounted row state overflow".to_owned()))?;
848    let construction = usize::from(params.l_build());
849    let scratch_per_candidate = std::mem::size_of::<ScoredNode>()
850        .checked_mul(4)
851        .and_then(|bytes| bytes.checked_add(std::mem::size_of::<u32>() * 6))
852        .ok_or_else(|| GraphBuildError::Geometry("scratch width overflow".to_owned()))?;
853    let candidate_scratch = construction
854        .checked_add(usize::from(params.r_max()))
855        .and_then(|candidates| candidates.checked_mul(scratch_per_candidate))
856        .ok_or_else(|| GraphBuildError::Geometry("scratch arena overflow".to_owned()))?;
857    let medoid_scratch = MEDOID_SAMPLE_ROWS
858        .checked_mul(std::mem::size_of::<ScoredNode>())
859        .ok_or_else(|| GraphBuildError::Geometry("medoid scratch overflow".to_owned()))?;
860    let scratch = candidate_scratch.max(medoid_scratch);
861    slots
862        .checked_add(row_state)
863        .and_then(|bytes| bytes.checked_add(ENTRY_POINT_COUNT * std::mem::size_of::<u32>()))
864        .and_then(|bytes| bytes.checked_add(scratch))
865        .ok_or_else(|| GraphBuildError::Geometry("graph build arena overflow".to_owned()))
866}
867
868struct BuildState {
869    phase: BuildPhase,
870    next_index: usize,
871    order: Vec<u32>,
872    entries: Vec<u32>,
873    adjacency: Adjacency,
874    inserted: Vec<u8>,
875    visited: Vec<u32>,
876    visit_epoch: u32,
877    memory: Option<AccountedCounter>,
878}
879
880impl BuildState {
881    fn new(
882        vectors: &SegmentVectors<'_>,
883        params: GraphParams,
884        seed: u64,
885        accounting: Option<&std::sync::Arc<Accounting>>,
886    ) -> Result<Self, GraphBuildError> {
887        let mut memory = match accounting {
888            Some(accounting) => Some(AccountedCounter::new(
889                accounting,
890                AllocationComponent::Temporary,
891            )?),
892            None => None,
893        };
894        if let Some(charge) = memory.as_mut() {
895            charge.set(build_arena_bytes(vectors.node_count, params)?)?;
896        }
897        let mut random = SplitMix64::new(seed);
898        let mut order = (0..vectors.node_count).collect::<Vec<_>>();
899        shuffle(&mut order, &mut random)?;
900        let entries = refined_entry_points(vectors, &order)?;
901        move_entries_to_front(&mut order, &entries);
902        Ok(Self {
903            phase: BuildPhase::Build,
904            next_index: 0,
905            order,
906            entries,
907            adjacency: Adjacency::new(vectors.node_count, params.r_max())?,
908            inserted: vec![0; vectors.node_count as usize],
909            visited: vec![0; vectors.node_count as usize],
910            visit_epoch: 0,
911            memory,
912        })
913    }
914
915    fn is_complete(&self, _passes: GraphBuildPasses) -> bool {
916        self.phase == BuildPhase::Complete
917    }
918
919    fn advance_batch(
920        &mut self,
921        vectors: &SegmentVectors<'_>,
922        params: GraphParams,
923        seed: u64,
924        passes: GraphBuildPasses,
925        cancellation: Option<&QueryCancellation<'_>>,
926    ) -> Result<bool, GraphBuildError> {
927        if self.phase == BuildPhase::Complete {
928            return Ok(true);
929        }
930        let batch_rows = usize::try_from(params.checkpoint_batch_rows()).map_err(|_| {
931            GraphBuildError::Geometry("checkpoint batch rows exceed usize".to_owned())
932        })?;
933        let end = self
934            .next_index
935            .checked_add(batch_rows)
936            .map(|end| end.min(self.order.len()))
937            .ok_or_else(|| GraphBuildError::Geometry("batch end overflow".to_owned()))?;
938        let alpha = match self.phase {
939            BuildPhase::Build => params.alpha_build(),
940            BuildPhase::Refine => params.alpha_refine(),
941            BuildPhase::Complete => return Ok(true),
942        };
943        let pass_seed = match self.phase {
944            BuildPhase::Build => seed,
945            BuildPhase::Refine => seed ^ 0xa1fa_1200_0000_0002,
946            BuildPhase::Complete => seed,
947        };
948        for position in self.next_index..end {
949            if position.is_multiple_of(CANCELLATION_CHECK_ROWS)
950                && let Some(cancellation) = cancellation
951            {
952                check_cancellation(cancellation)?;
953            }
954            let order_position = match self.phase {
955                BuildPhase::Build => position,
956                BuildPhase::Refine => {
957                    self.order.len().checked_sub(position + 1).ok_or_else(|| {
958                        GraphBuildError::Geometry("refinement order underflow".to_owned())
959                    })?
960                }
961                BuildPhase::Complete => position,
962            };
963            let node_id = *self.order.get(order_position).ok_or_else(|| {
964                GraphBuildError::Geometry(format!(
965                    "build order position {order_position} is unavailable"
966                ))
967            })?;
968            process_node(
969                vectors,
970                params,
971                pass_seed,
972                alpha,
973                node_id,
974                &self.entries,
975                &mut self.adjacency,
976                &mut self.inserted,
977                &mut self.visited,
978                &mut self.visit_epoch,
979            )?;
980        }
981        self.next_index = end;
982        if self.next_index == self.order.len() {
983            match (self.phase, passes) {
984                (BuildPhase::Build, GraphBuildPasses::Two) => {
985                    self.phase = BuildPhase::Refine;
986                    self.next_index = 0;
987                    self.inserted.fill(1);
988                }
989                (BuildPhase::Build, GraphBuildPasses::One) | (BuildPhase::Refine, _) => {
990                    self.phase = BuildPhase::Complete;
991                }
992                (BuildPhase::Complete, _) => {}
993            }
994        }
995        Ok(self.phase == BuildPhase::Complete)
996    }
997}
998
999fn check_cancellation(cancellation: &QueryCancellation<'_>) -> Result<(), GraphBuildError> {
1000    match cancellation.check_graph() {
1001        Ok(()) => Ok(()),
1002        Err(ScanError::Cancelled { .. }) => Err(GraphBuildError::Cancelled { partial: false }),
1003        Err(ScanError::Timeout { .. }) => Err(GraphBuildError::Timeout { partial: false }),
1004        Err(ScanError::ReadCancelled { .. }) => {
1005            Err(GraphBuildError::ReadCancelled { partial: false })
1006        }
1007        Err(error) => Err(GraphBuildError::Geometry(format!(
1008            "query cancellation seam returned non-cancellation error: {error}"
1009        ))),
1010    }
1011}
1012
1013fn checkpoint_temp_path(path: &Path) -> PathBuf {
1014    let mut temporary = path.as_os_str().to_os_string();
1015    temporary.push(".tmp");
1016    PathBuf::from(temporary)
1017}
1018
1019#[allow(clippy::too_many_arguments)]
1020fn write_checkpoint(
1021    vfs: &dyn Vfs,
1022    policy: DurabilityPolicy,
1023    path: &Path,
1024    vectors: &SegmentVectors<'_>,
1025    params: GraphParams,
1026    seed: u64,
1027    passes: GraphBuildPasses,
1028    state: &BuildState,
1029) -> Result<(), GraphBuildError> {
1030    let directory = path.parent().ok_or_else(|| GraphBuildError::CheckpointIo {
1031        path: path.to_path_buf(),
1032        source: std::io::Error::new(
1033            std::io::ErrorKind::InvalidInput,
1034            "checkpoint path has no parent directory",
1035        ),
1036    })?;
1037    let slot_bytes = state
1038        .adjacency
1039        .slots
1040        .len()
1041        .checked_mul(std::mem::size_of::<u32>())
1042        .ok_or_else(|| GraphBuildError::Geometry("checkpoint slot bytes overflow".to_owned()))?;
1043    let payload_bytes = state
1044        .adjacency
1045        .degrees
1046        .len()
1047        .checked_add(slot_bytes)
1048        .ok_or_else(|| GraphBuildError::Geometry("checkpoint payload overflow".to_owned()))?;
1049    let total_bytes = CHECKPOINT_HEADER_BYTES
1050        .checked_add(payload_bytes)
1051        .and_then(|bytes| bytes.checked_add(CHECKPOINT_CHECKSUM_BYTES))
1052        .ok_or_else(|| GraphBuildError::Geometry("checkpoint length overflow".to_owned()))?;
1053    let mut bytes = Vec::with_capacity(total_bytes);
1054    bytes.extend_from_slice(&CHECKPOINT_MAGIC);
1055    bytes.extend_from_slice(&CHECKPOINT_VERSION.to_le_bytes());
1056    bytes.push(phase_code(state.phase));
1057    bytes.push(pass_code(passes));
1058    bytes.push(params.r_target());
1059    bytes.push(params.r_max());
1060    bytes.extend_from_slice(&0_u16.to_le_bytes());
1061    bytes.extend_from_slice(&params.l_build().to_le_bytes());
1062    bytes.extend_from_slice(&0_u16.to_le_bytes());
1063    bytes.extend_from_slice(&params.checkpoint_batch_rows().to_le_bytes());
1064    bytes.extend_from_slice(&vectors.node_count.to_le_bytes());
1065    let dimensions = u32::try_from(vectors.dimensions)
1066        .map_err(|_| GraphBuildError::Geometry("checkpoint dimensions exceed u32".to_owned()))?;
1067    bytes.extend_from_slice(&dimensions.to_le_bytes());
1068    let next_index = u32::try_from(state.next_index)
1069        .map_err(|_| GraphBuildError::Geometry("checkpoint position exceeds u32".to_owned()))?;
1070    bytes.extend_from_slice(&next_index.to_le_bytes());
1071    bytes.extend_from_slice(&0_u32.to_le_bytes());
1072    bytes.extend_from_slice(&seed.to_le_bytes());
1073    bytes.extend_from_slice(&params.alpha_build().to_bits().to_le_bytes());
1074    bytes.extend_from_slice(&params.alpha_refine().to_bits().to_le_bytes());
1075    bytes.extend_from_slice(&vectors.segment_id);
1076    bytes.extend_from_slice(
1077        &u64::try_from(payload_bytes)
1078            .map_err(|_| GraphBuildError::Geometry("checkpoint payload exceeds u64".to_owned()))?
1079            .to_le_bytes(),
1080    );
1081    if bytes.len() != CHECKPOINT_HEADER_BYTES {
1082        return Err(GraphBuildError::Geometry(format!(
1083            "checkpoint header encoded {} bytes, expected {CHECKPOINT_HEADER_BYTES}",
1084            bytes.len()
1085        )));
1086    }
1087    bytes.extend_from_slice(&state.adjacency.degrees);
1088    for neighbor in &state.adjacency.slots {
1089        bytes.extend_from_slice(&neighbor.to_le_bytes());
1090    }
1091    bytes.extend_from_slice(&xxh3_64(&bytes).to_le_bytes());
1092    let temporary = checkpoint_temp_path(path);
1093    if let Err(source) = vfs.write(&temporary, &bytes) {
1094        let _ = vfs.delete(&temporary);
1095        return Err(GraphBuildError::CheckpointIo {
1096            path: temporary,
1097            source,
1098        });
1099    }
1100    if let SyncRequirement::Sync(kind) = policy.data_file_sync()
1101        && let Err(source) = vfs.sync(&temporary, kind)
1102    {
1103        let _ = vfs.delete(&temporary);
1104        return Err(GraphBuildError::CheckpointIo {
1105            path: temporary,
1106            source,
1107        });
1108    }
1109    if let Err(source) = vfs.rename(&temporary, path) {
1110        let _ = vfs.delete(&temporary);
1111        return Err(GraphBuildError::CheckpointIo {
1112            path: path.to_path_buf(),
1113            source,
1114        });
1115    }
1116    match policy.directory_sync() {
1117        SyncRequirement::Skip => Ok(()),
1118        SyncRequirement::Sync(kind) => {
1119            vfs.sync(directory, kind)
1120                .map_err(|source| GraphBuildError::CheckpointIo {
1121                    path: directory.to_path_buf(),
1122                    source,
1123                })
1124        }
1125    }
1126}
1127
1128fn checkpoint_encoded_bytes(state: &BuildState) -> Result<usize, GraphBuildError> {
1129    let slot_bytes = state
1130        .adjacency
1131        .slots
1132        .len()
1133        .checked_mul(std::mem::size_of::<u32>())
1134        .ok_or_else(|| GraphBuildError::Geometry("checkpoint slot bytes overflow".to_owned()))?;
1135    CHECKPOINT_HEADER_BYTES
1136        .checked_add(state.adjacency.degrees.len())
1137        .and_then(|bytes| bytes.checked_add(slot_bytes))
1138        .and_then(|bytes| bytes.checked_add(CHECKPOINT_CHECKSUM_BYTES))
1139        .ok_or_else(|| GraphBuildError::Geometry("checkpoint encoded bytes overflow".to_owned()))
1140}
1141
1142fn remove_checkpoint(vfs: &dyn Vfs, path: &Path) -> Result<(), GraphBuildError> {
1143    let temporary = checkpoint_temp_path(path);
1144    match vfs.delete(&temporary) {
1145        Ok(()) => {}
1146        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
1147        Err(source) => {
1148            return Err(GraphBuildError::CheckpointIo {
1149                path: temporary,
1150                source,
1151            });
1152        }
1153    }
1154    match vfs.delete(path) {
1155        Ok(()) => Ok(()),
1156        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
1157        Err(source) => Err(GraphBuildError::CheckpointIo {
1158            path: path.to_path_buf(),
1159            source,
1160        }),
1161    }
1162}
1163
1164fn decode_checkpoint(
1165    bytes: &[u8],
1166    vectors: &SegmentVectors<'_>,
1167    params: GraphParams,
1168    seed: u64,
1169    passes: GraphBuildPasses,
1170    accounting: Option<&std::sync::Arc<Accounting>>,
1171) -> Result<BuildState, GraphBuildError> {
1172    validate_graph_build_checkpoint(bytes)?;
1173    let minimum = CHECKPOINT_HEADER_BYTES
1174        .checked_add(CHECKPOINT_CHECKSUM_BYTES)
1175        .ok_or_else(|| GraphBuildError::CheckpointCorrupt("minimum length overflow".to_owned()))?;
1176    if bytes.len() < minimum {
1177        return Err(GraphBuildError::CheckpointCorrupt(format!(
1178            "truncated: need at least {minimum} bytes, got {}",
1179            bytes.len()
1180        )));
1181    }
1182    if bytes.get(..8) != Some(CHECKPOINT_MAGIC.as_slice()) {
1183        return Err(GraphBuildError::CheckpointCorrupt(
1184            "magic does not match ZEVAMCP1".to_owned(),
1185        ));
1186    }
1187    let checksum_start = bytes
1188        .len()
1189        .checked_sub(CHECKPOINT_CHECKSUM_BYTES)
1190        .ok_or_else(|| GraphBuildError::CheckpointCorrupt("checksum underflow".to_owned()))?;
1191    let stored_checksum = read_checkpoint_u64(bytes, checksum_start, "checksum")?;
1192    let checksummed = bytes.get(..checksum_start).ok_or_else(|| {
1193        GraphBuildError::CheckpointCorrupt("checksummed prefix is unavailable".to_owned())
1194    })?;
1195    let actual_checksum = xxh3_64(checksummed);
1196    if stored_checksum != actual_checksum {
1197        return Err(GraphBuildError::CheckpointCorrupt(format!(
1198            "xxh3-64 expected {stored_checksum:#018x}, computed {actual_checksum:#018x}"
1199        )));
1200    }
1201    let version = read_checkpoint_u16(bytes, 8, "version")?;
1202    if version != CHECKPOINT_VERSION {
1203        return Err(GraphBuildError::CheckpointCorrupt(format!(
1204            "version {version}, expected {CHECKPOINT_VERSION}"
1205        )));
1206    }
1207    let phase = decode_phase(read_checkpoint_u8(bytes, 10, "phase")?)?;
1208    let stored_passes = decode_passes(read_checkpoint_u8(bytes, 11, "passes")?)?;
1209    if stored_passes != passes {
1210        return Err(GraphBuildError::CheckpointCorrupt(
1211            "pass count does not match requested build".to_owned(),
1212        ));
1213    }
1214    let stored_r_target = read_checkpoint_u8(bytes, 12, "target degree")?;
1215    let stored_r_max = read_checkpoint_u8(bytes, 13, "maximum degree")?;
1216    let reserved_14 = read_checkpoint_u16(bytes, 14, "reserved degree bytes")?;
1217    let stored_l_build = read_checkpoint_u16(bytes, 16, "construction width")?;
1218    let reserved_18 = read_checkpoint_u16(bytes, 18, "reserved width bytes")?;
1219    let stored_batch = read_checkpoint_u32(bytes, 20, "checkpoint batch")?;
1220    let node_count = read_checkpoint_u32(bytes, 24, "node count")?;
1221    let dimensions = read_checkpoint_u32(bytes, 28, "dimensions")?;
1222    let next_index = read_checkpoint_u32(bytes, 32, "next index")?;
1223    let reserved_36 = read_checkpoint_u32(bytes, 36, "reserved index bytes")?;
1224    let stored_seed = read_checkpoint_u64(bytes, 40, "seed")?;
1225    let stored_alpha_build = f32::from_bits(read_checkpoint_u32(bytes, 48, "build alpha")?);
1226    let stored_alpha_refine = f32::from_bits(read_checkpoint_u32(bytes, 52, "refine alpha")?);
1227    let segment_id = bytes
1228        .get(56..72)
1229        .ok_or_else(|| GraphBuildError::CheckpointCorrupt("segment id is truncated".to_owned()))?;
1230    let payload_bytes = read_checkpoint_u64(bytes, 72, "payload length")?;
1231    if reserved_14 != 0 || reserved_18 != 0 || reserved_36 != 0 {
1232        return Err(GraphBuildError::CheckpointCorrupt(
1233            "reserved header bytes are non-zero".to_owned(),
1234        ));
1235    }
1236    if stored_r_target != params.r_target()
1237        || stored_r_max != params.r_max()
1238        || stored_l_build != params.l_build()
1239        || stored_batch != params.checkpoint_batch_rows()
1240        || stored_alpha_build.to_bits() != params.alpha_build().to_bits()
1241        || stored_alpha_refine.to_bits() != params.alpha_refine().to_bits()
1242        || node_count != vectors.node_count
1243        || dimensions != vectors.dimensions as u32
1244        || stored_seed != seed
1245        || segment_id != vectors.segment_id
1246    {
1247        return Err(GraphBuildError::CheckpointCorrupt(
1248            "checkpoint identity or graph parameters do not match input".to_owned(),
1249        ));
1250    }
1251    if passes == GraphBuildPasses::One && phase == BuildPhase::Refine {
1252        return Err(GraphBuildError::CheckpointCorrupt(
1253            "one-pass build cannot resume refinement".to_owned(),
1254        ));
1255    }
1256    let actual_payload = checksum_start
1257        .checked_sub(CHECKPOINT_HEADER_BYTES)
1258        .ok_or_else(|| GraphBuildError::CheckpointCorrupt("payload underflow".to_owned()))?;
1259    if usize::try_from(payload_bytes).ok() != Some(actual_payload) {
1260        return Err(GraphBuildError::CheckpointCorrupt(format!(
1261            "declared payload {payload_bytes} bytes, actual {actual_payload}"
1262        )));
1263    }
1264    let degree_bytes = node_count as usize;
1265    let degrees_end = CHECKPOINT_HEADER_BYTES
1266        .checked_add(degree_bytes)
1267        .ok_or_else(|| GraphBuildError::CheckpointCorrupt("degree end overflow".to_owned()))?;
1268    let degrees = bytes
1269        .get(CHECKPOINT_HEADER_BYTES..degrees_end)
1270        .ok_or_else(|| GraphBuildError::CheckpointCorrupt("degree bytes are truncated".to_owned()))?
1271        .to_vec();
1272    let expected_slots = (node_count as usize)
1273        .checked_mul(usize::from(params.r_max()))
1274        .ok_or_else(|| GraphBuildError::CheckpointCorrupt("slot count overflow".to_owned()))?;
1275    let slot_payload = bytes.get(degrees_end..checksum_start).ok_or_else(|| {
1276        GraphBuildError::CheckpointCorrupt("neighbor payload is unavailable".to_owned())
1277    })?;
1278    let expected_slot_bytes = expected_slots
1279        .checked_mul(std::mem::size_of::<u32>())
1280        .ok_or_else(|| GraphBuildError::CheckpointCorrupt("slot bytes overflow".to_owned()))?;
1281    if slot_payload.len() != expected_slot_bytes {
1282        return Err(GraphBuildError::CheckpointCorrupt(format!(
1283            "neighbor payload {} bytes, expected {expected_slot_bytes}",
1284            slot_payload.len()
1285        )));
1286    }
1287    let slots = slot_payload
1288        .chunks_exact(4)
1289        .map(|chunk| {
1290            let raw: [u8; 4] = chunk.try_into().map_err(|_| {
1291                GraphBuildError::CheckpointCorrupt("neighbor width is not four bytes".to_owned())
1292            })?;
1293            Ok(u32::from_le_bytes(raw))
1294        })
1295        .collect::<Result<Vec<_>, GraphBuildError>>()?;
1296    let adjacency = Adjacency {
1297        slots,
1298        degrees,
1299        node_count,
1300        r_max: params.r_max(),
1301    };
1302    validate_checkpoint_adjacency(&adjacency)?;
1303    let mut state = BuildState::new(vectors, params, seed, accounting)?;
1304    state.phase = phase;
1305    state.next_index = usize::try_from(next_index)
1306        .map_err(|_| GraphBuildError::CheckpointCorrupt("next index exceeds usize".to_owned()))?;
1307    if state.next_index > state.order.len() {
1308        return Err(GraphBuildError::CheckpointCorrupt(format!(
1309            "next index {} exceeds row count {}",
1310            state.next_index,
1311            state.order.len()
1312        )));
1313    }
1314    state.adjacency = adjacency;
1315    state.inserted.fill(u8::from(phase != BuildPhase::Build));
1316    if phase == BuildPhase::Build {
1317        for node_id in state.order.iter().take(state.next_index) {
1318            let node = vectors.validate_node(*node_id)?;
1319            let marker = state.inserted.get_mut(node).ok_or_else(|| {
1320                GraphBuildError::CheckpointCorrupt("inserted marker is unavailable".to_owned())
1321            })?;
1322            *marker = 1;
1323        }
1324    }
1325    Ok(state)
1326}
1327
1328/// Validates arbitrary resumable-build checkpoint bytes without allocating a graph.
1329///
1330/// This is the shared parser seam used by the dependency-free fuzz target.
1331pub fn validate_graph_build_checkpoint(bytes: &[u8]) -> Result<(), GraphBuildError> {
1332    let minimum = CHECKPOINT_HEADER_BYTES
1333        .checked_add(CHECKPOINT_CHECKSUM_BYTES)
1334        .ok_or_else(|| GraphBuildError::CheckpointCorrupt("minimum length overflow".to_owned()))?;
1335    if bytes.len() < minimum {
1336        return Err(GraphBuildError::CheckpointCorrupt(format!(
1337            "truncated: need at least {minimum} bytes, got {}",
1338            bytes.len()
1339        )));
1340    }
1341    if bytes.get(..8) != Some(CHECKPOINT_MAGIC.as_slice()) {
1342        return Err(GraphBuildError::CheckpointCorrupt(
1343            "magic does not match ZEVAMCP1".to_owned(),
1344        ));
1345    }
1346    let checksum_start = bytes
1347        .len()
1348        .checked_sub(CHECKPOINT_CHECKSUM_BYTES)
1349        .ok_or_else(|| GraphBuildError::CheckpointCorrupt("checksum underflow".to_owned()))?;
1350    let stored_checksum = read_checkpoint_u64(bytes, checksum_start, "checksum")?;
1351    let checksummed = bytes.get(..checksum_start).ok_or_else(|| {
1352        GraphBuildError::CheckpointCorrupt("checksummed prefix is unavailable".to_owned())
1353    })?;
1354    let actual_checksum = xxh3_64(checksummed);
1355    if stored_checksum != actual_checksum {
1356        return Err(GraphBuildError::CheckpointCorrupt(format!(
1357            "xxh3-64 expected {stored_checksum:#018x}, computed {actual_checksum:#018x}"
1358        )));
1359    }
1360    let version = read_checkpoint_u16(bytes, 8, "version")?;
1361    if version != CHECKPOINT_VERSION {
1362        return Err(GraphBuildError::CheckpointCorrupt(format!(
1363            "version {version}, expected {CHECKPOINT_VERSION}"
1364        )));
1365    }
1366    let _ = decode_phase(read_checkpoint_u8(bytes, 10, "phase")?)?;
1367    let _ = decode_passes(read_checkpoint_u8(bytes, 11, "passes")?)?;
1368    let r_target = read_checkpoint_u8(bytes, 12, "target degree")?;
1369    let r_max = read_checkpoint_u8(bytes, 13, "maximum degree")?;
1370    let reserved_14 = read_checkpoint_u16(bytes, 14, "reserved degree bytes")?;
1371    let l_build = read_checkpoint_u16(bytes, 16, "construction width")?;
1372    let reserved_18 = read_checkpoint_u16(bytes, 18, "reserved width bytes")?;
1373    let checkpoint_batch = read_checkpoint_u32(bytes, 20, "checkpoint batch")?;
1374    let node_count = read_checkpoint_u32(bytes, 24, "node count")?;
1375    let dimensions = read_checkpoint_u32(bytes, 28, "dimensions")?;
1376    let next_index = read_checkpoint_u32(bytes, 32, "next position")?;
1377    let reserved_36 = read_checkpoint_u32(bytes, 36, "reserved position bytes")?;
1378    if reserved_14 != 0 || reserved_18 != 0 || reserved_36 != 0 {
1379        return Err(GraphBuildError::CheckpointCorrupt(
1380            "reserved header bytes are nonzero".to_owned(),
1381        ));
1382    }
1383    if r_target == 0 || r_target > r_max || l_build < u16::from(r_target) {
1384        return Err(GraphBuildError::CheckpointCorrupt(
1385            "stored degree and construction width are invalid".to_owned(),
1386        ));
1387    }
1388    if checkpoint_batch == 0 || node_count == 0 || dimensions == 0 || next_index > node_count {
1389        return Err(GraphBuildError::CheckpointCorrupt(
1390            "stored batch, shape, or next position is invalid".to_owned(),
1391        ));
1392    }
1393    let alpha_build = f32::from_bits(read_checkpoint_u32(bytes, 48, "build alpha")?);
1394    let alpha_refine = f32::from_bits(read_checkpoint_u32(bytes, 52, "refine alpha")?);
1395    if !alpha_build.is_finite()
1396        || !alpha_refine.is_finite()
1397        || alpha_build < 1.0
1398        || alpha_refine < alpha_build
1399    {
1400        return Err(GraphBuildError::CheckpointCorrupt(
1401            "stored alpha values are invalid".to_owned(),
1402        ));
1403    }
1404    let payload_bytes = usize::try_from(read_checkpoint_u64(bytes, 72, "payload bytes")?)
1405        .map_err(|_| GraphBuildError::CheckpointCorrupt("payload exceeds usize".to_owned()))?;
1406    let node_count_usize = usize::try_from(node_count)
1407        .map_err(|_| GraphBuildError::CheckpointCorrupt("node count exceeds usize".to_owned()))?;
1408    let slot_count = node_count_usize
1409        .checked_mul(usize::from(r_max))
1410        .ok_or_else(|| GraphBuildError::CheckpointCorrupt("slot count overflow".to_owned()))?;
1411    let expected_payload = slot_count
1412        .checked_mul(std::mem::size_of::<u32>())
1413        .and_then(|slot_bytes| slot_bytes.checked_add(node_count_usize))
1414        .ok_or_else(|| {
1415            GraphBuildError::CheckpointCorrupt("payload geometry overflow".to_owned())
1416        })?;
1417    if payload_bytes != expected_payload {
1418        return Err(GraphBuildError::CheckpointCorrupt(format!(
1419            "payload bytes {payload_bytes}, expected {expected_payload}"
1420        )));
1421    }
1422    let expected_length = CHECKPOINT_HEADER_BYTES
1423        .checked_add(payload_bytes)
1424        .and_then(|length| length.checked_add(CHECKPOINT_CHECKSUM_BYTES))
1425        .ok_or_else(|| GraphBuildError::CheckpointCorrupt("file length overflow".to_owned()))?;
1426    if bytes.len() != expected_length {
1427        return Err(GraphBuildError::CheckpointCorrupt(format!(
1428            "file length {}, expected {expected_length}",
1429            bytes.len()
1430        )));
1431    }
1432    let degrees_start = CHECKPOINT_HEADER_BYTES;
1433    let slots_start = degrees_start
1434        .checked_add(node_count_usize)
1435        .ok_or_else(|| GraphBuildError::CheckpointCorrupt("slot offset overflow".to_owned()))?;
1436    for owner in 0..node_count_usize {
1437        let degree = usize::from(read_checkpoint_u8(
1438            bytes,
1439            degrees_start + owner,
1440            "node degree",
1441        )?);
1442        if degree > usize::from(r_max) {
1443            return Err(GraphBuildError::CheckpointCorrupt(format!(
1444                "node {owner} degree {degree} exceeds {r_max}"
1445            )));
1446        }
1447        let row_start = owner
1448            .checked_mul(usize::from(r_max))
1449            .and_then(|slot| slot.checked_mul(std::mem::size_of::<u32>()))
1450            .and_then(|offset| slots_start.checked_add(offset))
1451            .ok_or_else(|| GraphBuildError::CheckpointCorrupt("row offset overflow".to_owned()))?;
1452        let mut active = Vec::with_capacity(degree);
1453        for slot in 0..usize::from(r_max) {
1454            let offset = slot
1455                .checked_mul(std::mem::size_of::<u32>())
1456                .and_then(|offset| row_start.checked_add(offset))
1457                .ok_or_else(|| {
1458                    GraphBuildError::CheckpointCorrupt("neighbor offset overflow".to_owned())
1459                })?;
1460            let neighbor = read_checkpoint_u32(bytes, offset, "neighbor")?;
1461            if slot < degree {
1462                if neighbor >= node_count
1463                    || neighbor as usize == owner
1464                    || active.contains(&neighbor)
1465                {
1466                    return Err(GraphBuildError::CheckpointCorrupt(format!(
1467                        "node {owner} has invalid active neighbor {neighbor}"
1468                    )));
1469                }
1470                active.push(neighbor);
1471            } else if neighbor != u32::MAX {
1472                return Err(GraphBuildError::CheckpointCorrupt(format!(
1473                    "node {owner} inactive slot {slot} is not the sentinel"
1474                )));
1475            }
1476        }
1477    }
1478    Ok(())
1479}
1480
1481fn validate_checkpoint_adjacency(adjacency: &Adjacency) -> Result<(), GraphBuildError> {
1482    for node_id in 0..adjacency.node_count {
1483        let neighbors = adjacency.neighbors(node_id)?;
1484        if neighbors.len() > usize::from(adjacency.r_max) {
1485            return Err(GraphBuildError::CheckpointCorrupt(format!(
1486                "node {node_id} exceeds maximum degree"
1487            )));
1488        }
1489        let mut seen = Vec::with_capacity(neighbors.len());
1490        for neighbor in neighbors {
1491            if *neighbor >= adjacency.node_count || *neighbor == node_id || seen.contains(neighbor)
1492            {
1493                return Err(GraphBuildError::CheckpointCorrupt(format!(
1494                    "node {node_id} has an out-of-range, self, or duplicate neighbor {neighbor}"
1495                )));
1496            }
1497            seen.push(*neighbor);
1498        }
1499        let range = adjacency.row_range(node_id)?;
1500        let inactive_start = range.start.checked_add(neighbors.len()).ok_or_else(|| {
1501            GraphBuildError::CheckpointCorrupt("inactive offset overflow".to_owned())
1502        })?;
1503        let inactive = adjacency
1504            .slots
1505            .get(inactive_start..range.end)
1506            .ok_or_else(|| {
1507                GraphBuildError::CheckpointCorrupt("inactive slots are unavailable".to_owned())
1508            })?;
1509        if inactive.iter().any(|neighbor| *neighbor != u32::MAX) {
1510            return Err(GraphBuildError::CheckpointCorrupt(format!(
1511                "node {node_id} has non-sentinel inactive slots"
1512            )));
1513        }
1514    }
1515    Ok(())
1516}
1517
1518fn phase_code(phase: BuildPhase) -> u8 {
1519    match phase {
1520        BuildPhase::Build => 1,
1521        BuildPhase::Refine => 2,
1522        BuildPhase::Complete => 3,
1523    }
1524}
1525
1526fn decode_phase(value: u8) -> Result<BuildPhase, GraphBuildError> {
1527    match value {
1528        1 => Ok(BuildPhase::Build),
1529        2 => Ok(BuildPhase::Refine),
1530        3 => Ok(BuildPhase::Complete),
1531        _ => Err(GraphBuildError::CheckpointCorrupt(format!(
1532            "unknown phase {value}"
1533        ))),
1534    }
1535}
1536
1537fn pass_code(passes: GraphBuildPasses) -> u8 {
1538    match passes {
1539        GraphBuildPasses::One => 1,
1540        GraphBuildPasses::Two => 2,
1541    }
1542}
1543
1544fn decode_passes(value: u8) -> Result<GraphBuildPasses, GraphBuildError> {
1545    match value {
1546        1 => Ok(GraphBuildPasses::One),
1547        2 => Ok(GraphBuildPasses::Two),
1548        _ => Err(GraphBuildError::CheckpointCorrupt(format!(
1549            "unknown pass count {value}"
1550        ))),
1551    }
1552}
1553
1554fn read_checkpoint_u8(bytes: &[u8], offset: usize, field: &str) -> Result<u8, GraphBuildError> {
1555    bytes.get(offset).copied().ok_or_else(|| {
1556        GraphBuildError::CheckpointCorrupt(format!("{field} is truncated at byte {offset}"))
1557    })
1558}
1559
1560fn read_checkpoint_u16(bytes: &[u8], offset: usize, field: &str) -> Result<u16, GraphBuildError> {
1561    let end = offset
1562        .checked_add(2)
1563        .ok_or_else(|| GraphBuildError::CheckpointCorrupt(format!("{field} offset overflow")))?;
1564    let raw: [u8; 2] = bytes
1565        .get(offset..end)
1566        .ok_or_else(|| GraphBuildError::CheckpointCorrupt(format!("{field} is truncated")))?
1567        .try_into()
1568        .map_err(|_| GraphBuildError::CheckpointCorrupt(format!("{field} width is invalid")))?;
1569    Ok(u16::from_le_bytes(raw))
1570}
1571
1572fn read_checkpoint_u32(bytes: &[u8], offset: usize, field: &str) -> Result<u32, GraphBuildError> {
1573    let end = offset
1574        .checked_add(4)
1575        .ok_or_else(|| GraphBuildError::CheckpointCorrupt(format!("{field} offset overflow")))?;
1576    let raw: [u8; 4] = bytes
1577        .get(offset..end)
1578        .ok_or_else(|| GraphBuildError::CheckpointCorrupt(format!("{field} is truncated")))?
1579        .try_into()
1580        .map_err(|_| GraphBuildError::CheckpointCorrupt(format!("{field} width is invalid")))?;
1581    Ok(u32::from_le_bytes(raw))
1582}
1583
1584fn read_checkpoint_u64(bytes: &[u8], offset: usize, field: &str) -> Result<u64, GraphBuildError> {
1585    let end = offset
1586        .checked_add(8)
1587        .ok_or_else(|| GraphBuildError::CheckpointCorrupt(format!("{field} offset overflow")))?;
1588    let raw: [u8; 8] = bytes
1589        .get(offset..end)
1590        .ok_or_else(|| GraphBuildError::CheckpointCorrupt(format!("{field} is truncated")))?
1591        .try_into()
1592        .map_err(|_| GraphBuildError::CheckpointCorrupt(format!("{field} width is invalid")))?;
1593    Ok(u64::from_le_bytes(raw))
1594}
1595
1596#[derive(Clone, Copy, Debug)]
1597struct ScoredNode {
1598    node_id: u32,
1599    distance: f64,
1600}
1601
1602#[allow(clippy::too_many_arguments)]
1603fn process_node(
1604    vectors: &SegmentVectors<'_>,
1605    params: GraphParams,
1606    seed: u64,
1607    alpha: f32,
1608    node_id: u32,
1609    entries: &[u32],
1610    adjacency: &mut Adjacency,
1611    inserted: &mut [u8],
1612    visited: &mut [u32],
1613    visit_epoch: &mut u32,
1614) -> Result<(), GraphBuildError> {
1615    vectors.validate_node(node_id)?;
1616    let candidates = search_candidates(
1617        vectors,
1618        adjacency,
1619        inserted,
1620        node_id,
1621        entries,
1622        usize::from(params.l_build()),
1623        seed,
1624        visited,
1625        visit_epoch,
1626    )?;
1627    let mut candidate_ids = candidates
1628        .into_iter()
1629        .map(|candidate| candidate.node_id)
1630        .collect::<Vec<_>>();
1631    candidate_ids.extend_from_slice(adjacency.neighbors(node_id)?);
1632    let pruned = robust_prune(
1633        vectors,
1634        node_id,
1635        &candidate_ids,
1636        alpha,
1637        usize::from(params.r_target()),
1638    )?;
1639    adjacency.set_neighbors(node_id, &pruned)?;
1640    let node = vectors.validate_node(node_id)?;
1641    let inserted_node = inserted.get_mut(node).ok_or_else(|| {
1642        GraphBuildError::Geometry(format!("inserted marker {node_id} is unavailable"))
1643    })?;
1644    *inserted_node = 1;
1645    add_reciprocal_edges(vectors, adjacency, node_id, &pruned, params, alpha)?;
1646    Ok(())
1647}
1648
1649#[allow(clippy::too_many_arguments)]
1650fn search_candidates(
1651    vectors: &SegmentVectors<'_>,
1652    adjacency: &Adjacency,
1653    inserted: &[u8],
1654    query_id: u32,
1655    entries: &[u32],
1656    width: usize,
1657    seed: u64,
1658    visited: &mut [u32],
1659    visit_epoch: &mut u32,
1660) -> Result<Vec<ScoredNode>, GraphBuildError> {
1661    let query = vectors.f32_row(query_id)?;
1662    let query_norm_squared = query
1663        .iter()
1664        .map(|value| f64::from(*value) * f64::from(*value))
1665        .sum::<f64>();
1666    let prepared = prepare_bit4_query(query, seed ^ u64::from(query_id))?;
1667    *visit_epoch = visit_epoch.wrapping_add(1);
1668    if *visit_epoch == 0 {
1669        visited.fill(0);
1670        *visit_epoch = 1;
1671    }
1672    let epoch = *visit_epoch;
1673    let mut result = Vec::with_capacity(width);
1674    let mut frontier = Vec::with_capacity(width.saturating_mul(2));
1675    for &entry in entries {
1676        let entry_index = vectors.validate_node(entry)?;
1677        if entry == query_id
1678            || inserted.get(entry_index).copied() != Some(1)
1679            || visited.get(entry_index).copied() == Some(epoch)
1680        {
1681            continue;
1682        }
1683        if let Some(marker) = visited.get_mut(entry_index) {
1684            *marker = epoch;
1685        }
1686        let scored = score_node(vectors, &prepared, query_norm_squared, entry)?;
1687        insert_result(&mut result, scored, width);
1688        frontier.push(scored);
1689    }
1690    if frontier.is_empty()
1691        && let Some(start) = inserted.iter().position(|present| *present == 1)
1692    {
1693        let start = u32::try_from(start)
1694            .map_err(|_| GraphBuildError::Geometry("inserted start exceeds u32".to_owned()))?;
1695        let scored = score_node(vectors, &prepared, query_norm_squared, start)?;
1696        if let Some(marker) = visited.get_mut(start as usize) {
1697            *marker = epoch;
1698        }
1699        insert_result(&mut result, scored, width);
1700        frontier.push(scored);
1701    }
1702    while !frontier.is_empty() {
1703        frontier.sort_unstable_by(scored_worst_first);
1704        let Some(candidate) = frontier.pop() else {
1705            break;
1706        };
1707        let worst = result.last().map(|node| node.distance);
1708        if result.len() >= width && worst.is_some_and(|distance| candidate.distance > distance) {
1709            break;
1710        }
1711        for &neighbor in adjacency.neighbors(candidate.node_id)? {
1712            let neighbor_index = vectors.validate_node(neighbor)?;
1713            if neighbor == query_id
1714                || inserted.get(neighbor_index).copied() != Some(1)
1715                || visited.get(neighbor_index).copied() == Some(epoch)
1716            {
1717                continue;
1718            }
1719            if let Some(marker) = visited.get_mut(neighbor_index) {
1720                *marker = epoch;
1721            }
1722            let scored = score_node(vectors, &prepared, query_norm_squared, neighbor)?;
1723            let qualifies = result.len() < width
1724                || result
1725                    .last()
1726                    .is_some_and(|current_worst| scored.distance < current_worst.distance);
1727            if qualifies {
1728                insert_result(&mut result, scored, width);
1729                frontier.push(scored);
1730            }
1731        }
1732    }
1733    Ok(result)
1734}
1735
1736fn score_node(
1737    vectors: &SegmentVectors<'_>,
1738    query: &crate::quant::Bit4Query,
1739    query_norm_squared: f64,
1740    node_id: u32,
1741) -> Result<ScoredNode, GraphBuildError> {
1742    vectors.validate_node(node_id)?;
1743    let factors = vectors.factor(node_id)?;
1744    let dot = f64::from(est_dot_bit4(query, vectors.code_row(node_id)?, factors)?);
1745    let norm = factors.norm();
1746    Ok(ScoredNode {
1747        node_id,
1748        distance: (query_norm_squared + norm * norm - 2.0 * dot).max(0.0),
1749    })
1750}
1751
1752fn insert_result(result: &mut Vec<ScoredNode>, node: ScoredNode, width: usize) {
1753    result.push(node);
1754    result.sort_unstable_by(scored_best_first);
1755    if result.len() > width {
1756        let _ = result.pop();
1757    }
1758}
1759
1760fn scored_best_first(left: &ScoredNode, right: &ScoredNode) -> std::cmp::Ordering {
1761    left.distance
1762        .total_cmp(&right.distance)
1763        .then_with(|| left.node_id.cmp(&right.node_id))
1764}
1765
1766fn scored_worst_first(left: &ScoredNode, right: &ScoredNode) -> std::cmp::Ordering {
1767    scored_best_first(right, left)
1768}
1769
1770fn robust_prune(
1771    vectors: &SegmentVectors<'_>,
1772    owner: u32,
1773    candidates: &[u32],
1774    alpha: f32,
1775    target: usize,
1776) -> Result<Vec<u32>, GraphBuildError> {
1777    robust_prune_rows(
1778        vectors.rescore,
1779        vectors.dimensions,
1780        vectors.node_count,
1781        owner,
1782        candidates,
1783        alpha,
1784        target,
1785    )
1786}
1787
1788/// Applies the production alpha-prune rule to exact row-major vectors.
1789pub(crate) fn robust_prune_rows(
1790    rescore: &[f32],
1791    dimensions: usize,
1792    node_count: u32,
1793    owner: u32,
1794    candidates: &[u32],
1795    alpha: f32,
1796    target: usize,
1797) -> Result<Vec<u32>, GraphBuildError> {
1798    validate_exact_rows(rescore, dimensions, node_count)?;
1799    validate_exact_node(owner, node_count)?;
1800    let mut remaining = Vec::with_capacity(candidates.len());
1801    for &candidate in candidates {
1802        validate_exact_node(candidate, node_count)?;
1803        if candidate != owner && !remaining.contains(&candidate) {
1804            remaining.push(candidate);
1805        }
1806    }
1807    let mut owner_distances = remaining
1808        .iter()
1809        .map(|candidate| {
1810            exact_row_distance(rescore, dimensions, owner, *candidate).map(|distance| ScoredNode {
1811                node_id: *candidate,
1812                distance,
1813            })
1814        })
1815        .collect::<Result<Vec<_>, _>>()?;
1816    owner_distances.sort_unstable_by(scored_best_first);
1817    let mut selected = Vec::with_capacity(target);
1818    let alpha_squared = f64::from(alpha) * f64::from(alpha);
1819    while selected.len() < target {
1820        let Some(best) = owner_distances.first().copied() else {
1821            break;
1822        };
1823        selected.push(best.node_id);
1824        owner_distances.retain(|candidate| {
1825            if candidate.node_id == best.node_id {
1826                return false;
1827            }
1828            exact_row_distance(rescore, dimensions, best.node_id, candidate.node_id)
1829                .map_or(true, |between| alpha_squared * between > candidate.distance)
1830        });
1831    }
1832    Ok(selected)
1833}
1834
1835fn validate_exact_rows(
1836    rescore: &[f32],
1837    dimensions: usize,
1838    node_count: u32,
1839) -> Result<(), GraphBuildError> {
1840    if dimensions == 0 {
1841        return Err(GraphBuildError::Geometry(
1842            "alpha prune requires non-zero dimensions".to_owned(),
1843        ));
1844    }
1845    let expected = (node_count as usize)
1846        .checked_mul(dimensions)
1847        .ok_or_else(|| GraphBuildError::Geometry("exact row length overflow".to_owned()))?;
1848    if rescore.len() != expected {
1849        return Err(GraphBuildError::Geometry(format!(
1850            "exact rows contain {} values, expected {expected}",
1851            rescore.len()
1852        )));
1853    }
1854    Ok(())
1855}
1856
1857fn validate_exact_node(node_id: u32, node_count: u32) -> Result<(), GraphBuildError> {
1858    if node_id >= node_count {
1859        return Err(GraphBuildError::NodeIdOutOfRange {
1860            node_id,
1861            node_count,
1862        });
1863    }
1864    Ok(())
1865}
1866
1867fn exact_row_distance(
1868    rescore: &[f32],
1869    dimensions: usize,
1870    left: u32,
1871    right: u32,
1872) -> Result<f64, GraphBuildError> {
1873    let row = |node_id: u32| {
1874        let start = (node_id as usize)
1875            .checked_mul(dimensions)
1876            .ok_or_else(|| GraphBuildError::Geometry("exact row offset overflow".to_owned()))?;
1877        let end = start
1878            .checked_add(dimensions)
1879            .ok_or_else(|| GraphBuildError::Geometry("exact row end overflow".to_owned()))?;
1880        rescore
1881            .get(start..end)
1882            .ok_or_else(|| GraphBuildError::Geometry(format!("exact row {node_id} is unavailable")))
1883    };
1884    Ok(row(left)?
1885        .iter()
1886        .zip(row(right)?)
1887        .map(|(left, right)| {
1888            let difference = f64::from(*left) - f64::from(*right);
1889            difference * difference
1890        })
1891        .sum())
1892}
1893
1894fn add_reciprocal_edges(
1895    vectors: &SegmentVectors<'_>,
1896    adjacency: &mut Adjacency,
1897    owner: u32,
1898    neighbors: &[u32],
1899    params: GraphParams,
1900    alpha: f32,
1901) -> Result<(), GraphBuildError> {
1902    for &neighbor in neighbors {
1903        vectors.validate_node(neighbor)?;
1904        let existing = adjacency.neighbors(neighbor)?.to_vec();
1905        if existing.contains(&owner) {
1906            continue;
1907        }
1908        let updated = if existing.len() < usize::from(params.r_max()) {
1909            let mut updated = existing;
1910            updated.push(owner);
1911            updated
1912        } else {
1913            let mut candidates = existing;
1914            candidates.push(owner);
1915            robust_prune(
1916                vectors,
1917                neighbor,
1918                &candidates,
1919                alpha,
1920                usize::from(params.r_target()),
1921            )?
1922        };
1923        adjacency.set_neighbors(neighbor, &updated)?;
1924    }
1925    Ok(())
1926}
1927
1928fn refined_entry_points(
1929    vectors: &SegmentVectors<'_>,
1930    shuffled: &[u32],
1931) -> Result<Vec<u32>, GraphBuildError> {
1932    let sample = shuffled
1933        .get(..shuffled.len().min(MEDOID_SAMPLE_ROWS))
1934        .ok_or_else(|| GraphBuildError::Geometry("medoid sample is unavailable".to_owned()))?;
1935    let mut medoid_scores = sample
1936        .iter()
1937        .map(|candidate| {
1938            sample
1939                .iter()
1940                .try_fold(0.0_f64, |sum, other| {
1941                    vectors
1942                        .exact_distance(*candidate, *other)
1943                        .map(|distance| sum + distance)
1944                })
1945                .map(|distance| ScoredNode {
1946                    node_id: *candidate,
1947                    distance,
1948                })
1949        })
1950        .collect::<Result<Vec<_>, _>>()?;
1951    medoid_scores.sort_unstable_by(scored_best_first);
1952    let medoid = medoid_scores
1953        .first()
1954        .map(|node| node.node_id)
1955        .ok_or_else(|| GraphBuildError::Geometry("medoid sample contains no rows".to_owned()))?;
1956    let mut entries = vec![medoid];
1957    while entries.len() < ENTRY_POINT_COUNT && entries.len() < sample.len() {
1958        let mut farthest = None::<ScoredNode>;
1959        for &candidate in sample {
1960            if entries.contains(&candidate) {
1961                continue;
1962            }
1963            let nearest = entries.iter().try_fold(f64::INFINITY, |nearest, entry| {
1964                vectors
1965                    .exact_distance(candidate, *entry)
1966                    .map(|distance| nearest.min(distance))
1967            })?;
1968            let scored = ScoredNode {
1969                node_id: candidate,
1970                distance: nearest,
1971            };
1972            if farthest.is_none_or(|current| {
1973                scored.distance > current.distance
1974                    || (scored.distance == current.distance && scored.node_id < current.node_id)
1975            }) {
1976                farthest = Some(scored);
1977            }
1978        }
1979        let Some(next) = farthest else {
1980            break;
1981        };
1982        entries.push(next.node_id);
1983    }
1984    Ok(entries)
1985}
1986
1987fn move_entries_to_front(order: &mut Vec<u32>, entries: &[u32]) {
1988    order.retain(|node_id| !entries.contains(node_id));
1989    for &entry in entries.iter().rev() {
1990        order.insert(0, entry);
1991    }
1992}
1993
1994fn encode_artifact(
1995    vectors: &SegmentVectors<'_>,
1996    params: GraphParams,
1997    entries: &[u32],
1998    adjacency: &Adjacency,
1999    mut memory: Option<AccountedCounter>,
2000) -> Result<GraphBuildArtifact, GraphBuildError> {
2001    let dimensions = u32::try_from(vectors.dimensions)
2002        .map_err(|_| GraphBuildError::Geometry("dimensions exceed u32".to_owned()))?;
2003    let padded_dimensions = dimensions
2004        .checked_add(127)
2005        .map(|value| value / 128 * 128)
2006        .ok_or_else(|| GraphBuildError::Geometry("padded dimensions overflow".to_owned()))?;
2007    let layout = GraphNodeLayout::new(dimensions, padded_dimensions, params.r_max())?;
2008    let padded_row_bytes = layout.code_bytes();
2009    let padded_length = (vectors.node_count as usize)
2010        .checked_mul(padded_row_bytes)
2011        .ok_or_else(|| GraphBuildError::Geometry("padded code bytes overflow".to_owned()))?;
2012    let encoded_length = (vectors.node_count as usize)
2013        .checked_mul(layout.stride() as usize)
2014        .and_then(|bytes| bytes.checked_add(NODE_BLOCK_TRAILER_LEN))
2015        .ok_or_else(|| GraphBuildError::Geometry("encoded graph bytes overflow".to_owned()))?;
2016    let node_inputs = (vectors.node_count as usize)
2017        .checked_mul(std::mem::size_of::<GraphNodeBlockInput<'_>>())
2018        .ok_or_else(|| GraphBuildError::Geometry("node input bytes overflow".to_owned()))?;
2019    let entry_bytes = entries
2020        .len()
2021        .checked_mul(std::mem::size_of::<u32>())
2022        .ok_or_else(|| GraphBuildError::Geometry("entry bytes overflow".to_owned()))?;
2023    if let Some(charge) = memory.as_mut() {
2024        let existing = usize::try_from(charge.bytes())
2025            .map_err(|_| GraphBuildError::Geometry("accounted bytes exceed usize".to_owned()))?;
2026        let peak = existing
2027            .checked_add(padded_length)
2028            .and_then(|bytes| bytes.checked_add(node_inputs))
2029            .and_then(|bytes| bytes.checked_add(encoded_length))
2030            .and_then(|bytes| bytes.checked_add(entry_bytes))
2031            .ok_or_else(|| GraphBuildError::Geometry("encode arena overflow".to_owned()))?;
2032        charge.set(peak)?;
2033    }
2034    let mut padded_codes = vec![0_u8; padded_length];
2035    for node_id in 0..vectors.node_count {
2036        let source = vectors.code_row(node_id)?;
2037        let start = (node_id as usize)
2038            .checked_mul(padded_row_bytes)
2039            .ok_or_else(|| GraphBuildError::Geometry("padded code offset overflow".to_owned()))?;
2040        let end = start
2041            .checked_add(source.len())
2042            .ok_or_else(|| GraphBuildError::Geometry("padded code end overflow".to_owned()))?;
2043        let destination = padded_codes.get_mut(start..end).ok_or_else(|| {
2044            GraphBuildError::Geometry(format!("padded code row {node_id} is unavailable"))
2045        })?;
2046        destination.copy_from_slice(source);
2047    }
2048    let mut nodes = Vec::with_capacity(vectors.node_count as usize);
2049    for node_id in 0..vectors.node_count {
2050        let start = (node_id as usize)
2051            .checked_mul(padded_row_bytes)
2052            .ok_or_else(|| GraphBuildError::Geometry("encoded code offset overflow".to_owned()))?;
2053        let end = start
2054            .checked_add(padded_row_bytes)
2055            .ok_or_else(|| GraphBuildError::Geometry("encoded code end overflow".to_owned()))?;
2056        let codes = padded_codes.get(start..end).ok_or_else(|| {
2057            GraphBuildError::Geometry(format!("encoded code row {node_id} is unavailable"))
2058        })?;
2059        nodes.push(GraphNodeBlockInput {
2060            codes,
2061            factors: vectors.factor(node_id)?,
2062            flags: u8::from(entries.contains(&node_id)),
2063            neighbors: adjacency.neighbors(node_id)?,
2064        });
2065    }
2066    let encoded = encode_node_blocks(GraphNodeBlockBuild {
2067        layout,
2068        nodes: &nodes,
2069    })?;
2070    let encoded_region = encoded.into_bytes();
2071    let entry_points = entries.to_vec();
2072    if let Some(charge) = memory.as_mut() {
2073        let retained = encoded_region
2074            .capacity()
2075            .checked_add(
2076                entry_points
2077                    .capacity()
2078                    .checked_mul(std::mem::size_of::<u32>())
2079                    .ok_or_else(|| {
2080                        GraphBuildError::Geometry("retained entry bytes overflow".to_owned())
2081                    })?,
2082            )
2083            .ok_or_else(|| GraphBuildError::Geometry("retained graph bytes overflow".to_owned()))?;
2084        charge.set(retained)?;
2085    }
2086    Ok(GraphBuildArtifact {
2087        encoded_region,
2088        entry_points,
2089        work_rows_completed: 0,
2090        _memory: memory,
2091    })
2092}
2093
2094fn shuffle(values: &mut [u32], random: &mut SplitMix64) -> Result<(), GraphBuildError> {
2095    for upper in (1..values.len()).rev() {
2096        let modulus = u64::try_from(upper + 1)
2097            .map_err(|_| GraphBuildError::Geometry("shuffle width exceeds u64".to_owned()))?;
2098        let selected = usize::try_from(random.next_u64() % modulus)
2099            .map_err(|_| GraphBuildError::Geometry("shuffle index exceeds usize".to_owned()))?;
2100        values.swap(upper, selected);
2101    }
2102    Ok(())
2103}
2104
2105#[derive(Clone, Copy, Debug)]
2106struct SplitMix64 {
2107    state: u64,
2108}
2109
2110impl SplitMix64 {
2111    const fn new(seed: u64) -> Self {
2112        Self { state: seed }
2113    }
2114
2115    fn next_u64(&mut self) -> u64 {
2116        self.state = self.state.wrapping_add(0x9e37_79b9_7f4a_7c15);
2117        let mut value = self.state;
2118        value = (value ^ (value >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
2119        value = (value ^ (value >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
2120        value ^ (value >> 31)
2121    }
2122}
2123
2124#[cfg(test)]
2125#[allow(
2126    clippy::expect_used,
2127    clippy::indexing_slicing,
2128    clippy::panic,
2129    clippy::unwrap_used
2130)]
2131mod tests {
2132    use proptest::prelude::*;
2133    use proptest::test_runner::{Config, RngSeed, TestRunner};
2134    use rand::{Rng, RngCore};
2135    use xxhash_rust::xxh3::xxh3_64;
2136
2137    use crate::graph::block::decode_node_blocks;
2138    use crate::lifecycle::QueryCancellation;
2139    use crate::lifecycle::durability::{CommitTier, DurabilityMode, DurabilityPolicy};
2140    use crate::lifecycle::{
2141        CancelToken, ManualMonotonicClock, OpenOptions, QueryControl, Store, StoreTestDependencies,
2142    };
2143    use crate::meta::{AliveSet, ColumnStoreBuilder, Schema};
2144    use crate::quant::{Bit4Factors, quantize_bit4};
2145    use crate::segment::SegmentId;
2146    use crate::segment::reader::SegmentReader;
2147    use crate::segment::writer::{SegmentBuild, SegmentFactors, write_segment};
2148    use crate::vfs::{CountingVfs, StdVfs, Vfs};
2149    use std::sync::Arc;
2150
2151    use super::{
2152        Adjacency, CheckpointedGraphBuild, GraphBuildError, GraphBuildPasses, GraphBuildSession,
2153        SegmentVectors, build_graph, build_graph_checkpointed, decode_checkpoint,
2154        validate_graph_build_checkpoint,
2155    };
2156    use crate::graph::GraphParams;
2157
2158    fn fixture_reader(directory: &std::path::Path, rows: usize) -> SegmentReader {
2159        let dimensions = 128_usize;
2160        let mut random = crate::test_support::seeded_rng(
2161            "graph::build::vamana_build_is_deterministic_under_seed::fixture",
2162        );
2163        let mut rescore = Vec::with_capacity(rows * dimensions);
2164        let mut codes = Vec::with_capacity(rows * dimensions.div_ceil(2));
2165        let mut factors = Vec::<Bit4Factors>::with_capacity(rows);
2166        for _ in 0..rows {
2167            let row = (0..dimensions)
2168                .map(|_| random.random_range(-2.0_f32..2.0_f32))
2169                .collect::<Vec<_>>();
2170            let start = codes.len();
2171            codes.resize(start + dimensions.div_ceil(2), 0);
2172            factors.push(quantize_bit4(&row, &mut codes[start..]).expect("fixture row quantizes"));
2173            rescore.extend(row);
2174        }
2175        let mut columns = ColumnStoreBuilder::new(Schema::new(Vec::new()).expect("empty schema"));
2176        for row in 0..rows {
2177            columns
2178                .push_row(row as i64, &[])
2179                .expect("timestamp-only fixture row");
2180        }
2181        let columns = columns.finish().expect("fixture columns");
2182        let alive = AliveSet::new(rows as u32);
2183        let id = SegmentId::new(19, [7; 10]);
2184        let policy = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
2185            .expect("derived policy");
2186        write_segment(
2187            &StdVfs,
2188            directory,
2189            SegmentBuild {
2190                id,
2191                scheme: 4,
2192                dims: dimensions as u32,
2193                codes: &codes,
2194                factors: SegmentFactors::Bit4(&factors),
2195                rescore: &rescore,
2196                columns: &columns,
2197                alive: &alive,
2198            },
2199            policy,
2200        )
2201        .expect("sealed Bit4 fixture");
2202        SegmentReader::open(&StdVfs, &directory.join(id.file_name()), id).expect("fixture reader")
2203    }
2204
2205    #[test]
2206    fn graph_checkpoint_io_goes_through_the_store_vfs() {
2207        let segment_directory = tempfile::tempdir().expect("checkpoint segment directory");
2208        let reader = fixture_reader(segment_directory.path(), 4);
2209        let store_directory = tempfile::tempdir().expect("checkpoint store directory");
2210        let checkpoint = store_directory.path().join("graph-build.checkpoint");
2211        let vfs = Arc::new(CountingVfs::new(StdVfs).with_read_path_filter(checkpoint.clone()));
2212        let store = Store::open_with_test_dependencies(
2213            store_directory.path(),
2214            OpenOptions::default(),
2215            StoreTestDependencies::new(vfs.clone(), Arc::new(ManualMonotonicClock::new())),
2216        )
2217        .expect("checkpoint store");
2218        let lease = store.snapshot().expect("checkpoint snapshot");
2219        let params = GraphParams::new(2, 3, 1.0, 1.0, 3, 1).expect("checkpoint params");
2220        let control = QueryControl::Cancel(CancelToken::new());
2221
2222        vfs.reset();
2223        let interrupted = build_graph_checkpointed(
2224            &store,
2225            &reader,
2226            CheckpointedGraphBuild::new(params, 0x06, GraphBuildPasses::One, &checkpoint, &control)
2227                .with_max_work_rows(1),
2228            &lease,
2229        );
2230        assert!(matches!(
2231            interrupted,
2232            Err(GraphBuildError::BudgetExhausted { rows_completed: 1 })
2233        ));
2234        build_graph_checkpointed(
2235            &store,
2236            &reader,
2237            CheckpointedGraphBuild::new(params, 0x06, GraphBuildPasses::One, &checkpoint, &control),
2238            &lease,
2239        )
2240        .expect("resumed graph build");
2241
2242        assert!(vfs.read_calls() >= 1, "checkpoint read bypassed Vfs");
2243        assert_eq!(
2244            vfs.filtered_read_calls(),
2245            1,
2246            "graph-build.checkpoint read bypassed Vfs"
2247        );
2248        assert!(vfs.write_calls() >= 1, "checkpoint write bypassed Vfs");
2249        assert!(vfs.rename_calls() >= 1, "checkpoint rename bypassed Vfs");
2250        assert!(vfs.delete_calls() >= 1, "checkpoint delete bypassed Vfs");
2251        drop(lease);
2252        store.close().expect("close checkpoint store");
2253    }
2254
2255    #[test]
2256    fn graph_checkpoint_publish_syncs_the_temp_file_and_its_directory() {
2257        let segment_directory = tempfile::tempdir().expect("checkpoint segment directory");
2258        let reader = fixture_reader(segment_directory.path(), 4);
2259        let store_directory = tempfile::tempdir().expect("checkpoint store directory");
2260        let checkpoint = store_directory.path().join("graph-build.checkpoint");
2261        let vfs = Arc::new(CountingVfs::new(StdVfs));
2262        let store = Store::open_with_test_dependencies(
2263            store_directory.path(),
2264            OpenOptions::default().with_durability(DurabilityMode::Durable, CommitTier::Durable),
2265            StoreTestDependencies::new(vfs.clone(), Arc::new(ManualMonotonicClock::new())),
2266        )
2267        .expect("durable checkpoint store");
2268        let lease = store.snapshot().expect("checkpoint snapshot");
2269        let params = GraphParams::new(2, 3, 1.0, 1.0, 3, 1).expect("checkpoint params");
2270        let control = QueryControl::Cancel(CancelToken::new());
2271
2272        vfs.reset();
2273        let interrupted = build_graph_checkpointed(
2274            &store,
2275            &reader,
2276            CheckpointedGraphBuild::new(params, 0x03, GraphBuildPasses::One, &checkpoint, &control)
2277                .with_max_work_rows(1),
2278            &lease,
2279        );
2280        assert!(matches!(
2281            interrupted,
2282            Err(GraphBuildError::BudgetExhausted { rows_completed: 1 })
2283        ));
2284        assert!(
2285            vfs.full_sync_calls() >= 2,
2286            "checkpoint publish skipped syncs"
2287        );
2288        assert!(vfs.rename_calls() >= 1, "checkpoint publish skipped rename");
2289    }
2290
2291    fn checkpoint_with_mutation(
2292        mut bytes: Vec<u8>,
2293        mutation: impl FnOnce(&mut Vec<u8>),
2294    ) -> Vec<u8> {
2295        mutation(&mut bytes);
2296        let checksum_start = bytes.len() - super::CHECKPOINT_CHECKSUM_BYTES;
2297        let checksum = xxh3_64(&bytes[..checksum_start]);
2298        bytes[checksum_start..].copy_from_slice(&checksum.to_le_bytes());
2299        bytes
2300    }
2301
2302    #[test]
2303    fn graph_build_errors_preserve_actionable_values_and_sources() {
2304        let io_error = GraphBuildError::CheckpointIo {
2305            path: std::path::PathBuf::from("blocked.checkpoint"),
2306            source: std::io::Error::new(std::io::ErrorKind::PermissionDenied, "blocked"),
2307        };
2308        assert!(io_error.to_string().contains("blocked.checkpoint"));
2309        assert!(std::error::Error::source(&io_error).is_some());
2310
2311        let errors = [
2312            GraphBuildError::CheckpointCorrupt("bad header".to_owned()),
2313            GraphBuildError::BudgetExhausted { rows_completed: 16 },
2314            GraphBuildError::Cancelled { partial: false },
2315            GraphBuildError::Timeout { partial: false },
2316            GraphBuildError::ReadCancelled { partial: false },
2317            GraphBuildError::Geometry("bad shape".to_owned()),
2318            GraphBuildError::NodeIdOutOfRange {
2319                node_id: 9,
2320                node_count: 3,
2321            },
2322        ];
2323        for error in errors {
2324            assert!(!error.to_string().is_empty());
2325            assert!(std::error::Error::source(&error).is_none());
2326        }
2327
2328        let sourced_errors = [
2329            GraphBuildError::from(crate::segment::SegmentError::Geometry(
2330                "bad segment".to_owned(),
2331            )),
2332            GraphBuildError::from(crate::quant::QuantError::EmptyVector),
2333            GraphBuildError::from(crate::graph::block::GraphNodeError::InvalidHeader(
2334                "bad node header".to_owned(),
2335            )),
2336            GraphBuildError::from(crate::lifecycle::StoreError::NotDirectory {
2337                path: std::path::PathBuf::from("not-a-store"),
2338            }),
2339        ];
2340        for error in sourced_errors {
2341            assert!(!error.to_string().is_empty());
2342            assert!(std::error::Error::source(&error).is_some());
2343        }
2344    }
2345
2346    #[test]
2347    fn built_graph_publishes_through_the_frozen_m2_writer() {
2348        let directory = tempfile::tempdir().expect("publication fixture directory");
2349        let reader = fixture_reader(directory.path(), 32);
2350        let params = GraphParams::new(8, 12, 1.0, 1.2, 24, 32).expect("publication params");
2351        let artifact = build_graph(
2352            &reader,
2353            params,
2354            0x0001_9000_3a71_fac7,
2355            GraphBuildPasses::Two,
2356        )
2357        .expect("publication graph");
2358        let output_id = SegmentId::new(19, [8; 10]);
2359        let policy = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
2360            .expect("publication policy");
2361        let meta = artifact
2362            .write_segment_with_graph(&StdVfs, directory.path(), &reader, output_id, policy)
2363            .expect("published graph segment");
2364        assert_eq!(meta.id, output_id);
2365        let published = SegmentReader::open(
2366            &StdVfs,
2367            &directory.path().join(output_id.file_name()),
2368            output_id,
2369        )
2370        .expect("published reader");
2371        let graph = published
2372            .graph_node_blocks()
2373            .expect("published graph region");
2374        assert_eq!(graph.node_count(), 32);
2375        assert_eq!(graph.layout().max_degree(), 12);
2376    }
2377
2378    #[test]
2379    fn checked_adjacency_rejects_invalid_node_and_degree_inputs() {
2380        let directory = tempfile::tempdir().expect("checked vector directory");
2381        let reader = fixture_reader(directory.path(), 3);
2382        let vectors = SegmentVectors::new(&reader).expect("checked vectors");
2383        assert!(matches!(
2384            vectors.validate_node(3),
2385            Err(GraphBuildError::NodeIdOutOfRange { .. })
2386        ));
2387
2388        let mut adjacency = Adjacency::new(3, 2).expect("small adjacency");
2389        assert!(matches!(
2390            adjacency.neighbors(3),
2391            Err(GraphBuildError::NodeIdOutOfRange { .. })
2392        ));
2393        assert!(matches!(
2394            adjacency.set_neighbors(0, &[1, 2, 1]),
2395            Err(GraphBuildError::Geometry(_))
2396        ));
2397        assert!(matches!(
2398            adjacency.set_neighbors(0, &[3]),
2399            Err(GraphBuildError::NodeIdOutOfRange { .. })
2400        ));
2401        adjacency
2402            .set_neighbors(0, &[1, 2])
2403            .expect("valid adjacency row");
2404        assert_eq!(adjacency.neighbors(0).expect("active row"), &[1, 2]);
2405
2406        let mut self_edge = Adjacency::new(3, 2).expect("self-edge adjacency");
2407        self_edge.degrees[0] = 1;
2408        self_edge.slots[0] = 0;
2409        assert!(matches!(
2410            super::validate_checkpoint_adjacency(&self_edge),
2411            Err(GraphBuildError::CheckpointCorrupt(_))
2412        ));
2413
2414        let mut inactive_value = Adjacency::new(3, 2).expect("inactive-slot adjacency");
2415        inactive_value.slots[0] = 1;
2416        assert!(matches!(
2417            super::validate_checkpoint_adjacency(&inactive_value),
2418            Err(GraphBuildError::CheckpointCorrupt(_))
2419        ));
2420        assert!(matches!(
2421            super::decode_phase(3),
2422            Ok(super::BuildPhase::Complete)
2423        ));
2424        assert!(matches!(
2425            super::read_checkpoint_u8(&[], 0, "truncated"),
2426            Err(GraphBuildError::CheckpointCorrupt(_))
2427        ));
2428    }
2429
2430    #[test]
2431    fn checkpoint_parser_rejects_semantic_corruption_and_identity_drift() {
2432        let directory = tempfile::tempdir().expect("checkpoint parser directory");
2433        let reader = fixture_reader(directory.path(), 16);
2434        let checkpoint = directory.path().join("semantic.checkpoint");
2435        let params = GraphParams::new(4, 6, 1.0, 1.2, 8, 4).expect("checkpoint params");
2436        let store_directory = tempfile::tempdir().expect("checkpoint store directory");
2437        let store = Store::open(store_directory.path(), OpenOptions::default()).expect("store");
2438        let lease = store.snapshot().expect("checkpoint lease");
2439        let control = QueryControl::Cancel(CancelToken::new());
2440        let cancellation = QueryCancellation::new(&control, &lease);
2441        let policy = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
2442            .expect("derived policy");
2443        let mut session = GraphBuildSession::new(
2444            &StdVfs,
2445            &reader,
2446            params,
2447            0x0001_9000_3c0d_ec01,
2448            GraphBuildPasses::Two,
2449            &checkpoint,
2450            policy,
2451            None,
2452        )
2453        .expect("checkpoint session");
2454        assert!(
2455            !session
2456                .advance_batch(&cancellation)
2457                .expect("checkpoint batch")
2458        );
2459        let valid = StdVfs.read(&checkpoint).expect("valid checkpoint bytes");
2460        validate_graph_build_checkpoint(&valid).expect("valid checkpoint parses");
2461
2462        let mut bad_checksum = valid.clone();
2463        bad_checksum[40] ^= 1;
2464        let corruptions = [
2465            bad_checksum,
2466            checkpoint_with_mutation(valid.clone(), |bytes| {
2467                bytes[8..10].copy_from_slice(&2_u16.to_le_bytes());
2468            }),
2469            checkpoint_with_mutation(valid.clone(), |bytes| bytes[10] = 9),
2470            checkpoint_with_mutation(valid.clone(), |bytes| bytes[11] = 9),
2471            checkpoint_with_mutation(valid.clone(), |bytes| bytes[14] = 1),
2472            checkpoint_with_mutation(valid.clone(), |bytes| bytes[12] = 0),
2473            checkpoint_with_mutation(valid.clone(), |bytes| bytes[20..24].fill(0)),
2474            checkpoint_with_mutation(valid.clone(), |bytes| {
2475                bytes[32..36].copy_from_slice(&17_u32.to_le_bytes());
2476            }),
2477            checkpoint_with_mutation(valid.clone(), |bytes| {
2478                bytes[48..52].copy_from_slice(&f32::NAN.to_bits().to_le_bytes());
2479            }),
2480            checkpoint_with_mutation(valid.clone(), |bytes| bytes[72..80].fill(0)),
2481            checkpoint_with_mutation(valid.clone(), |bytes| {
2482                bytes[80] = 1;
2483                bytes[96..100].copy_from_slice(&0_u32.to_le_bytes());
2484            }),
2485            checkpoint_with_mutation(valid.clone(), |bytes| {
2486                bytes[80] = 0;
2487                bytes[96..100].copy_from_slice(&1_u32.to_le_bytes());
2488            }),
2489        ];
2490        for corrupt in corruptions {
2491            assert!(matches!(
2492                validate_graph_build_checkpoint(&corrupt),
2493                Err(GraphBuildError::CheckpointCorrupt(_))
2494            ));
2495        }
2496
2497        let vectors = SegmentVectors::new(&reader).expect("checkpoint vectors");
2498        assert!(matches!(
2499            decode_checkpoint(
2500                &valid,
2501                &vectors,
2502                params,
2503                0x0001_9000_3c0d_ec02,
2504                GraphBuildPasses::Two,
2505                None,
2506            ),
2507            Err(GraphBuildError::CheckpointCorrupt(_))
2508        ));
2509        assert!(matches!(
2510            decode_checkpoint(
2511                &valid,
2512                &vectors,
2513                params,
2514                0x0001_9000_3c0d_ec01,
2515                GraphBuildPasses::One,
2516                None,
2517            ),
2518            Err(GraphBuildError::CheckpointCorrupt(_))
2519        ));
2520        let one_pass_refine = checkpoint_with_mutation(valid, |bytes| {
2521            bytes[10] = 2;
2522            bytes[11] = 1;
2523        });
2524        assert!(matches!(
2525            decode_checkpoint(
2526                &one_pass_refine,
2527                &vectors,
2528                params,
2529                0x0001_9000_3c0d_ec01,
2530                GraphBuildPasses::One,
2531                None,
2532            ),
2533            Err(GraphBuildError::CheckpointCorrupt(_))
2534        ));
2535    }
2536
2537    #[test]
2538    fn corrupt_checkpoint_refuses_once_clears_the_file_and_the_next_session_starts_fresh() {
2539        let directory = tempfile::tempdir().expect("corrupt checkpoint directory");
2540        let reader = fixture_reader(directory.path(), 16);
2541        let checkpoint = directory.path().join("corrupt.checkpoint");
2542        let temporary = super::checkpoint_temp_path(&checkpoint);
2543        let params = GraphParams::new(4, 6, 1.0, 1.2, 8, 4).expect("checkpoint params");
2544        let store_directory = tempfile::tempdir().expect("checkpoint store directory");
2545        let store = Store::open(store_directory.path(), OpenOptions::default()).expect("store");
2546        let lease = store.snapshot().expect("checkpoint lease");
2547        let control = QueryControl::Cancel(CancelToken::new());
2548        let cancellation = QueryCancellation::new(&control, &lease);
2549        let mut session = GraphBuildSession::new(
2550            &StdVfs,
2551            &reader,
2552            params,
2553            0x0001_9000_c077_0001,
2554            GraphBuildPasses::Two,
2555            &checkpoint,
2556            DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
2557                .expect("derived policy"),
2558            None,
2559        )
2560        .expect("checkpoint session");
2561        assert!(
2562            !session
2563                .advance_batch(&cancellation)
2564                .expect("checkpoint batch")
2565        );
2566        drop(session);
2567
2568        let mut corrupt = StdVfs.read(&checkpoint).expect("valid checkpoint bytes");
2569        corrupt[40] ^= 1;
2570        StdVfs
2571            .write(&checkpoint, &corrupt)
2572            .expect("write corrupt checkpoint");
2573        StdVfs
2574            .write(&temporary, b"stale temporary checkpoint")
2575            .expect("write stale temporary checkpoint");
2576
2577        assert!(matches!(
2578            GraphBuildSession::new(
2579                &StdVfs,
2580                &reader,
2581                params,
2582                0x0001_9000_c077_0001,
2583                GraphBuildPasses::Two,
2584                &checkpoint,
2585                DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
2586                    .expect("derived policy"),
2587                None,
2588            ),
2589            Err(GraphBuildError::CheckpointCorrupt(_))
2590        ));
2591        assert_eq!(
2592            StdVfs
2593                .read(&checkpoint)
2594                .expect_err("corrupt checkpoint was retained")
2595                .kind(),
2596            std::io::ErrorKind::NotFound
2597        );
2598        assert_eq!(
2599            StdVfs
2600                .read(&temporary)
2601                .expect_err("stale checkpoint temporary was retained")
2602                .kind(),
2603            std::io::ErrorKind::NotFound
2604        );
2605
2606        let fresh = GraphBuildSession::new(
2607            &StdVfs,
2608            &reader,
2609            params,
2610            0x0001_9000_c077_0001,
2611            GraphBuildPasses::Two,
2612            &checkpoint,
2613            DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
2614                .expect("derived policy"),
2615            None,
2616        )
2617        .expect("next session starts fresh");
2618        assert_eq!(fresh.completed_work_rows().expect("fresh work rows"), 0);
2619    }
2620
2621    #[test]
2622    fn accounting_rejection_on_resume_preserves_the_valid_checkpoint() {
2623        let directory = tempfile::tempdir().expect("accounting checkpoint directory");
2624        let reader = fixture_reader(directory.path(), 16);
2625        let checkpoint = directory.path().join("accounting.checkpoint");
2626        let params = GraphParams::new(4, 6, 1.0, 1.2, 8, 4).expect("checkpoint params");
2627        let store_directory = tempfile::tempdir().expect("checkpoint store directory");
2628        let store = Store::open(store_directory.path(), OpenOptions::default()).expect("store");
2629        let lease = store.snapshot().expect("checkpoint lease");
2630        let control = QueryControl::Cancel(CancelToken::new());
2631        let cancellation = QueryCancellation::new(&control, &lease);
2632        let mut session = GraphBuildSession::new(
2633            &StdVfs,
2634            &reader,
2635            params,
2636            0x0001_9000_acc0_0001,
2637            GraphBuildPasses::Two,
2638            &checkpoint,
2639            DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
2640                .expect("derived policy"),
2641            None,
2642        )
2643        .expect("checkpoint session");
2644        assert!(
2645            !session
2646                .advance_batch(&cancellation)
2647                .expect("checkpoint batch")
2648        );
2649        drop(session);
2650        let valid = StdVfs.read(&checkpoint).expect("valid checkpoint bytes");
2651
2652        let refused_directory = tempfile::tempdir().expect("refused store directory");
2653        let refused = Store::open(
2654            refused_directory.path(),
2655            OpenOptions::default().with_max_temp_bytes(1),
2656        )
2657        .expect("refused store");
2658        assert!(matches!(
2659            GraphBuildSession::new(
2660                &StdVfs,
2661                &reader,
2662                params,
2663                0x0001_9000_acc0_0001,
2664                GraphBuildPasses::Two,
2665                &checkpoint,
2666                DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
2667                    .expect("derived policy"),
2668                Some(&refused.accounting),
2669            ),
2670            Err(GraphBuildError::Store(_))
2671        ));
2672        assert_eq!(
2673            StdVfs
2674                .read(&checkpoint)
2675                .expect("accounting rejection removed the valid checkpoint"),
2676            valid
2677        );
2678    }
2679
2680    #[test]
2681    fn incomplete_session_and_checkpoint_io_fail_typed() {
2682        let directory = tempfile::tempdir().expect("typed failure directory");
2683        let reader = fixture_reader(directory.path(), 16);
2684        let params = GraphParams::new(4, 6, 1.0, 1.2, 8, 4).expect("typed failure params");
2685        let control = QueryControl::Cancel(CancelToken::new());
2686        let store_directory = tempfile::tempdir().expect("typed failure store");
2687        let store = Store::open(store_directory.path(), OpenOptions::default()).expect("store");
2688        let lease = store.snapshot().expect("typed failure lease");
2689        let cancellation = QueryCancellation::new(&control, &lease);
2690        let policy = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
2691            .expect("derived policy");
2692
2693        let unfinished = GraphBuildSession::new(
2694            &StdVfs,
2695            &reader,
2696            params,
2697            0x0001_9000_3100_0001,
2698            GraphBuildPasses::Two,
2699            &directory.path().join("unfinished.checkpoint"),
2700            policy,
2701            None,
2702        )
2703        .expect("unfinished session");
2704        assert!(matches!(
2705            unfinished.artifact(),
2706            Err(GraphBuildError::Geometry(_))
2707        ));
2708
2709        assert!(matches!(
2710            GraphBuildSession::new(
2711                &StdVfs,
2712                &reader,
2713                params,
2714                0x0001_9000_3100_0001,
2715                GraphBuildPasses::Two,
2716                directory.path(),
2717                policy,
2718                None,
2719            ),
2720            Err(GraphBuildError::CheckpointIo { .. })
2721        ));
2722
2723        let missing_parent = directory.path().join("missing").join("write.checkpoint");
2724        let mut unwritable = GraphBuildSession::new(
2725            &StdVfs,
2726            &reader,
2727            params,
2728            0x0001_9000_3100_0001,
2729            GraphBuildPasses::Two,
2730            &missing_parent,
2731            policy,
2732            None,
2733        )
2734        .expect("unwritable session starts before checkpoint write");
2735        assert!(matches!(
2736            unwritable.advance_batch(&cancellation),
2737            Err(GraphBuildError::CheckpointIo { .. })
2738        ));
2739
2740        let timeout = QueryControl::Cancel(CancelToken::new());
2741        timeout.mark_timed_out();
2742        let timed_out = QueryCancellation::new(&timeout, &lease);
2743        assert!(matches!(
2744            super::check_cancellation(&timed_out),
2745            Err(GraphBuildError::Timeout { partial: false })
2746        ));
2747    }
2748
2749    #[test]
2750    fn vamana_build_is_deterministic_under_seed() {
2751        let directory = tempfile::tempdir().expect("fixture directory");
2752        let reader = fixture_reader(directory.path(), 48);
2753        let params = GraphParams::new(8, 12, 1.0, 1.2, 24, 48).expect("test params");
2754        let mut random = crate::test_support::seeded_rng(
2755            "graph::build::vamana_build_is_deterministic_under_seed",
2756        );
2757        let seed = random.next_u64();
2758        let first = build_graph(&reader, params, seed, GraphBuildPasses::Two)
2759            .expect("first deterministic build");
2760        let second = build_graph(&reader, params, seed, GraphBuildPasses::Two)
2761            .expect("second deterministic build");
2762        assert_eq!(first.encoded_region(), second.encoded_region());
2763    }
2764
2765    #[test]
2766    fn cancelled_build_leaves_no_partial_artifact() {
2767        let directory = tempfile::tempdir().expect("sealed segment directory");
2768        let reader = fixture_reader(directory.path(), 48);
2769        let segment_path = directory.path().join(reader.meta().id.file_name());
2770        let before = StdVfs
2771            .read(&segment_path)
2772            .expect("sealed bytes before build");
2773        let checkpoint = directory.path().join("graph-build.checkpoint");
2774        let checkpoint_temp = directory.path().join("graph-build.checkpoint.tmp");
2775        let params = GraphParams::new(8, 12, 1.0, 1.2, 24, 8).expect("test params");
2776        let store_directory = tempfile::tempdir().expect("store directory");
2777        let store = Store::open(store_directory.path(), OpenOptions::default()).expect("store");
2778        let lease = store.snapshot().expect("snapshot lease");
2779        let token = CancelToken::new();
2780        let control = QueryControl::Cancel(token.clone());
2781        let cancellation = QueryCancellation::new(&control, &lease);
2782        let policy = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
2783            .expect("derived policy");
2784        let mut session = GraphBuildSession::new(
2785            &StdVfs,
2786            &reader,
2787            params,
2788            0x19_0003,
2789            GraphBuildPasses::Two,
2790            &checkpoint,
2791            policy,
2792            None,
2793        )
2794        .expect("build session");
2795
2796        assert!(!session.advance_batch(&cancellation).expect("first batch"));
2797        token.cancel();
2798        assert!(matches!(
2799            session.advance_batch(&cancellation),
2800            Err(GraphBuildError::Cancelled { partial: false })
2801        ));
2802        assert_eq!(
2803            StdVfs
2804                .read(&segment_path)
2805                .expect("sealed bytes after cancel"),
2806            before
2807        );
2808        assert!(
2809            checkpoint.exists(),
2810            "completed batch checkpoint must remain resumable"
2811        );
2812        assert!(
2813            !checkpoint_temp.exists(),
2814            "checkpoint temp must not survive"
2815        );
2816    }
2817
2818    #[test]
2819    fn build_resumes_from_checkpoint_to_the_same_graph() {
2820        let directory = tempfile::tempdir().expect("sealed segment directory");
2821        let reader = fixture_reader(directory.path(), 64);
2822        let checkpoint = directory.path().join("resume.checkpoint");
2823        let params = GraphParams::new(8, 12, 1.0, 1.2, 24, 8).expect("test params");
2824        let store_directory = tempfile::tempdir().expect("store directory");
2825        let store = Store::open(store_directory.path(), OpenOptions::default()).expect("store");
2826        let lease = store.snapshot().expect("snapshot lease");
2827        let control = QueryControl::Cancel(CancelToken::new());
2828        let cancellation = QueryCancellation::new(&control, &lease);
2829        let policy = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
2830            .expect("derived policy");
2831        let mut interrupted = GraphBuildSession::new(
2832            &StdVfs,
2833            &reader,
2834            params,
2835            0x19_0003_00c0_ffee,
2836            GraphBuildPasses::Two,
2837            &checkpoint,
2838            policy,
2839            None,
2840        )
2841        .expect("interrupted session");
2842        assert!(
2843            !interrupted
2844                .advance_batch(&cancellation)
2845                .expect("first batch")
2846        );
2847        drop(interrupted);
2848
2849        let mut resumed = GraphBuildSession::new(
2850            &StdVfs,
2851            &reader,
2852            params,
2853            0x19_0003_00c0_ffee,
2854            GraphBuildPasses::Two,
2855            &checkpoint,
2856            policy,
2857            None,
2858        )
2859        .expect("resumed session");
2860        while !resumed.advance_batch(&cancellation).expect("resumed batch") {}
2861        let resumed = resumed.artifact().expect("resumed artifact");
2862        let uninterrupted =
2863            build_graph(&reader, params, 0x19_0003_00c0_ffee, GraphBuildPasses::Two)
2864                .expect("uninterrupted artifact");
2865        assert_eq!(resumed.encoded_region(), uninterrupted.encoded_region());
2866        assert!(!checkpoint.exists(), "completed build removes checkpoint");
2867    }
2868
2869    #[test]
2870    fn checkpointed_build_reports_exact_work_for_each_declared_pass() {
2871        let directory = tempfile::tempdir().expect("work-count fixture directory");
2872        let rows = 24_usize;
2873        let reader = fixture_reader(directory.path(), rows);
2874        let params = GraphParams::new(6, 10, 1.0, 1.2, 16, 6).expect("work-count params");
2875        let store_directory = tempfile::tempdir().expect("work-count store directory");
2876        let store = Store::open(store_directory.path(), OpenOptions::default()).expect("store");
2877        let lease = store.snapshot().expect("work-count lease");
2878        let control = QueryControl::Cancel(CancelToken::new());
2879
2880        for (passes, expected_rows, suffix) in [
2881            (GraphBuildPasses::One, rows as u64, "one"),
2882            (GraphBuildPasses::Two, (rows * 2) as u64, "two"),
2883        ] {
2884            let checkpoint = directory
2885                .path()
2886                .join(format!("work-count-{suffix}.checkpoint"));
2887            let artifact = build_graph_checkpointed(
2888                &store,
2889                &reader,
2890                CheckpointedGraphBuild::new(params, 0x19_0003_c0a7, passes, &checkpoint, &control),
2891                &lease,
2892            )
2893            .expect("checkpointed build completes");
2894
2895            assert_eq!(artifact.work_rows_completed(), expected_rows);
2896            assert_eq!(
2897                decode_node_blocks(artifact.encoded_region())
2898                    .expect("work-count graph decodes")
2899                    .node_count(),
2900                rows as u32
2901            );
2902            assert!(
2903                !checkpoint.exists(),
2904                "completed build removes {suffix} checkpoint"
2905            );
2906        }
2907    }
2908
2909    #[test]
2910    fn graph_build_handles_the_minimum_shape_and_rejects_an_empty_segment() {
2911        let directory = tempfile::tempdir().expect("minimum-shape directory");
2912        let empty = fixture_reader(directory.path(), 0);
2913        assert!(matches!(
2914            SegmentVectors::new(&empty),
2915            Err(GraphBuildError::Geometry(detail)) if detail.contains("at least one row")
2916        ));
2917
2918        let one_row_directory = tempfile::tempdir().expect("one-row directory");
2919        let one_row = fixture_reader(one_row_directory.path(), 1);
2920        let params = GraphParams::new(1, 1, 1.0, 1.0, 1, 1).expect("one-row params");
2921        let artifact = build_graph(&one_row, params, 0x19_0003_0000_0001, GraphBuildPasses::One)
2922            .expect("one-row build succeeds");
2923        let graph = decode_node_blocks(artifact.encoded_region()).expect("one-row graph decodes");
2924        let node = graph.block(0).expect("row zero exists");
2925        assert_eq!(graph.node_count(), 1);
2926        assert_eq!(node.degree(), 0);
2927        assert_eq!(node.flags() & 1, 1);
2928        assert_eq!(artifact.entry_points(), &[0]);
2929    }
2930
2931    #[test]
2932    fn every_node_has_at_most_r_max_neighbours() {
2933        let mut seeded = crate::test_support::seeded_rng(
2934            "graph::build::every_node_has_at_most_r_max_neighbours",
2935        );
2936        let mut runner = TestRunner::new(Config {
2937            cases: 24,
2938            rng_seed: RngSeed::Fixed(seeded.next_u64()),
2939            ..Config::default()
2940        });
2941        let strategy = (6_usize..40, 2_u8..=8, any::<u64>());
2942        let result = runner.run(&strategy, |(rows, r_target, seed)| {
2943            let directory = tempfile::tempdir().expect("property directory");
2944            let reader = fixture_reader(directory.path(), rows);
2945            let r_max = r_target.saturating_add(4);
2946            let params = GraphParams::new(
2947                r_target,
2948                r_max,
2949                1.0,
2950                1.2,
2951                u16::from(r_max).saturating_add(8),
2952                rows as u32,
2953            )
2954            .expect("property params");
2955            let artifact =
2956                build_graph(&reader, params, seed, GraphBuildPasses::Two).expect("property graph");
2957            let graph = decode_node_blocks(artifact.encoded_region()).expect("decoded graph");
2958            prop_assert_eq!(graph.layout().max_degree(), r_max);
2959            for node_id in 0..graph.node_count() {
2960                let block = graph.block(node_id).expect("property node");
2961                prop_assert!(block.degree() <= r_max);
2962            }
2963            Ok(())
2964        });
2965        assert!(result.is_ok(), "property result: {result:?}");
2966    }
2967
2968    #[test]
2969    fn no_node_lists_itself_or_a_duplicate() {
2970        let mut seeded =
2971            crate::test_support::seeded_rng("graph::build::no_node_lists_itself_or_a_duplicate");
2972        let mut runner = TestRunner::new(Config {
2973            cases: 24,
2974            rng_seed: RngSeed::Fixed(seeded.next_u64()),
2975            ..Config::default()
2976        });
2977        let strategy = (6_usize..40, 2_u8..=8, any::<u64>());
2978        let result = runner.run(&strategy, |(rows, r_target, seed)| {
2979            let directory = tempfile::tempdir().expect("property directory");
2980            let reader = fixture_reader(directory.path(), rows);
2981            let r_max = r_target.saturating_add(4);
2982            let params = GraphParams::new(
2983                r_target,
2984                r_max,
2985                1.0,
2986                1.2,
2987                u16::from(r_max).saturating_add(8),
2988                rows as u32,
2989            )
2990            .expect("property params");
2991            let artifact =
2992                build_graph(&reader, params, seed, GraphBuildPasses::Two).expect("property graph");
2993            let graph = decode_node_blocks(artifact.encoded_region()).expect("decoded graph");
2994            for node_id in 0..graph.node_count() {
2995                let block = graph.block(node_id).expect("property node");
2996                let mut unique = std::collections::BTreeSet::new();
2997                for neighbor in block.neighbors_padded().take(usize::from(block.degree())) {
2998                    prop_assert_ne!(neighbor, node_id);
2999                    prop_assert!(unique.insert(neighbor));
3000                }
3001            }
3002            Ok(())
3003        });
3004        assert!(result.is_ok(), "property result: {result:?}");
3005    }
3006
3007    #[test]
3008    fn graph_is_connected_from_the_entry_points() {
3009        let directory = tempfile::tempdir().expect("connectivity directory");
3010        let reader = fixture_reader(directory.path(), 96);
3011        let params = GraphParams::new(8, 12, 1.0, 1.2, 24, 96).expect("connectivity params");
3012        let artifact = build_graph(
3013            &reader,
3014            params,
3015            0x0001_9000_3c01_1ec7,
3016            GraphBuildPasses::Two,
3017        )
3018        .expect("connectivity graph");
3019        let graph = decode_node_blocks(artifact.encoded_region()).expect("decoded graph");
3020        let mut reached = vec![false; graph.node_count() as usize];
3021        let mut pending = std::collections::VecDeque::new();
3022        for &entry in artifact.entry_points() {
3023            pending.push_back(entry);
3024        }
3025        while let Some(node_id) = pending.pop_front() {
3026            let node = node_id as usize;
3027            if reached[node] {
3028                continue;
3029            }
3030            reached[node] = true;
3031            let block = graph.block(node_id).expect("reachable node");
3032            for neighbor in block.neighbors_padded().take(usize::from(block.degree())) {
3033                if !reached[neighbor as usize] {
3034                    pending.push_back(neighbor);
3035                }
3036            }
3037        }
3038        let reached_count = reached.iter().filter(|seen| **seen).count();
3039        assert_eq!(reached_count, graph.node_count() as usize);
3040    }
3041
3042    #[test]
3043    fn graph_build_memory_is_budgeted_and_exactly_accounted() {
3044        let directory = tempfile::tempdir().expect("accounting fixture directory");
3045        let reader = fixture_reader(directory.path(), 48);
3046        let params = GraphParams::new(8, 12, 1.0, 1.2, 24, 48).expect("accounting params");
3047        let control = QueryControl::Cancel(CancelToken::new());
3048
3049        let refused_directory = tempfile::tempdir().expect("refused store directory");
3050        let refused = Store::open(
3051            refused_directory.path(),
3052            OpenOptions::default().with_max_temp_bytes(1),
3053        )
3054        .expect("refused store");
3055        let refused_lease = refused.snapshot().expect("refused lease");
3056        let refused_result = build_graph_checkpointed(
3057            &refused,
3058            &reader,
3059            CheckpointedGraphBuild::new(
3060                params,
3061                0x19_0003_acc0,
3062                GraphBuildPasses::One,
3063                &directory.path().join("refused.checkpoint"),
3064                &control,
3065            ),
3066            &refused_lease,
3067        );
3068        assert!(matches!(refused_result, Err(GraphBuildError::Store(_))));
3069        assert_eq!(refused.stats().expect("refused stats").temporary_bytes, 0);
3070
3071        let store_directory = tempfile::tempdir().expect("accounted store directory");
3072        let store = Store::open(store_directory.path(), OpenOptions::default()).expect("store");
3073        let lease = store.snapshot().expect("accounted lease");
3074        let artifact = build_graph_checkpointed(
3075            &store,
3076            &reader,
3077            CheckpointedGraphBuild::new(
3078                params,
3079                0x19_0003_acc0,
3080                GraphBuildPasses::One,
3081                &directory.path().join("accounted.checkpoint"),
3082                &control,
3083            ),
3084            &lease,
3085        )
3086        .expect("accounted graph");
3087        let expected =
3088            artifact.encoded_region().len() + std::mem::size_of_val(artifact.entry_points());
3089        assert_eq!(
3090            store.stats().expect("live graph stats").temporary_bytes,
3091            expected as u64
3092        );
3093        drop(artifact);
3094        assert_eq!(
3095            store.stats().expect("released graph stats").temporary_bytes,
3096            0
3097        );
3098    }
3099
3100    #[test]
3101    fn arbitrary_checkpoint_bytes_fail_typed_without_panicking() {
3102        let mut seeded = crate::test_support::seeded_rng(
3103            "graph::build::arbitrary_checkpoint_bytes_fail_typed_without_panicking",
3104        );
3105        let mut runner = TestRunner::new(Config {
3106            cases: 64,
3107            rng_seed: RngSeed::Fixed(seeded.next_u64()),
3108            ..Config::default()
3109        });
3110        let result = runner.run(&proptest::collection::vec(any::<u8>(), 0..2048), |bytes| {
3111            prop_assert!(matches!(
3112                validate_graph_build_checkpoint(&bytes),
3113                Err(GraphBuildError::CheckpointCorrupt(_))
3114            ));
3115            Ok(())
3116        });
3117        assert!(result.is_ok(), "property result: {result:?}");
3118    }
3119}