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_with_refinement_passes,
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    #[cfg(test)]
649    fn from_native(
650        dimensions: usize,
651        codes: &'a [u8],
652        factors: &'a [Bit4Factors],
653        rescore: &'a [f32],
654    ) -> Result<Self, GraphBuildError> {
655        if dimensions == 0 || factors.is_empty() {
656            return Err(GraphBuildError::Geometry(
657                "native flat Vamana requires dimensions and rows".to_owned(),
658            ));
659        }
660        let code_stride = dimensions.div_ceil(2);
661        let expected_codes = factors
662            .len()
663            .checked_mul(code_stride)
664            .ok_or_else(|| GraphBuildError::Geometry("native code length overflow".to_owned()))?;
665        let expected_rescore = factors.len().checked_mul(dimensions).ok_or_else(|| {
666            GraphBuildError::Geometry("native rescore length overflow".to_owned())
667        })?;
668        if codes.len() != expected_codes || rescore.len() != expected_rescore {
669            return Err(GraphBuildError::Geometry(
670                "native flat Vamana vector geometry".to_owned(),
671            ));
672        }
673        Ok(Self {
674            segment_id: [0; 16],
675            node_count: u32::try_from(factors.len()).map_err(|_| {
676                GraphBuildError::Geometry("native row count exceeds u32".to_owned())
677            })?,
678            dimensions,
679            code_stride,
680            codes,
681            factors,
682            rescore,
683        })
684    }
685
686    fn new(reader: &'a SegmentReader) -> Result<Self, GraphBuildError> {
687        if reader.meta().scheme != 4 {
688            return Err(GraphBuildError::Geometry(format!(
689                "flat Vamana requires Bit4 scheme 4, got {}",
690                reader.meta().scheme
691            )));
692        }
693        let dimensions = reader.meta().dims as usize;
694        let node_count = reader.meta().row_count;
695        if node_count == 0 {
696            return Err(GraphBuildError::Geometry(
697                "flat Vamana requires at least one row".to_owned(),
698            ));
699        }
700        Ok(Self {
701            segment_id: *reader.meta().id.as_bytes(),
702            node_count,
703            dimensions,
704            code_stride: dimensions.div_ceil(2),
705            codes: reader.bit4_codes()?,
706            factors: reader.bit4_factors()?,
707            rescore: reader.rescore_f32()?,
708        })
709    }
710
711    fn validate_node(&self, node_id: u32) -> Result<usize, GraphBuildError> {
712        if node_id >= self.node_count {
713            return Err(GraphBuildError::NodeIdOutOfRange {
714                node_id,
715                node_count: self.node_count,
716            });
717        }
718        usize::try_from(node_id)
719            .map_err(|_| GraphBuildError::Geometry("node id exceeds usize".to_owned()))
720    }
721
722    fn f32_row(&self, node_id: u32) -> Result<&'a [f32], GraphBuildError> {
723        let node = self.validate_node(node_id)?;
724        let start = node
725            .checked_mul(self.dimensions)
726            .ok_or_else(|| GraphBuildError::Geometry("f32 row offset overflow".to_owned()))?;
727        let end = start
728            .checked_add(self.dimensions)
729            .ok_or_else(|| GraphBuildError::Geometry("f32 row end overflow".to_owned()))?;
730        self.rescore.get(start..end).ok_or_else(|| {
731            GraphBuildError::Geometry(format!("f32 row {node_id} is outside validated rescore"))
732        })
733    }
734
735    fn code_row(&self, node_id: u32) -> Result<&'a [u8], GraphBuildError> {
736        let node = self.validate_node(node_id)?;
737        let start = node
738            .checked_mul(self.code_stride)
739            .ok_or_else(|| GraphBuildError::Geometry("Bit4 row offset overflow".to_owned()))?;
740        let end = start
741            .checked_add(self.code_stride)
742            .ok_or_else(|| GraphBuildError::Geometry("Bit4 row end overflow".to_owned()))?;
743        self.codes.get(start..end).ok_or_else(|| {
744            GraphBuildError::Geometry(format!("Bit4 row {node_id} is outside validated codes"))
745        })
746    }
747
748    fn factor(&self, node_id: u32) -> Result<Bit4Factors, GraphBuildError> {
749        let node = self.validate_node(node_id)?;
750        self.factors.get(node).copied().ok_or_else(|| {
751            GraphBuildError::Geometry(format!("Bit4 factor {node_id} is unavailable"))
752        })
753    }
754}
755
756struct Adjacency {
757    slots: Vec<u32>,
758    degrees: Vec<u8>,
759    node_count: u32,
760    r_max: u8,
761}
762
763impl Adjacency {
764    fn new(node_count: u32, r_max: u8) -> Result<Self, GraphBuildError> {
765        let slot_count = (node_count as usize)
766            .checked_mul(usize::from(r_max))
767            .ok_or_else(|| GraphBuildError::Geometry("adjacency size overflow".to_owned()))?;
768        let mut slots = Vec::new();
769
770        let capacity = slot_count;
771        let requested = capacity
772            .checked_mul(std::mem::size_of::<u32>())
773            .ok_or_else(|| GraphBuildError::Geometry("adjacency allocation overflow".to_owned()))?;
774        #[cfg(feature = "allocation-audit")]
775        let allocation = crate::allocation_audit::attributed(|| slots.try_reserve_exact(capacity));
776        #[cfg(not(feature = "allocation-audit"))]
777        let allocation = slots.try_reserve_exact(capacity);
778        allocation.map_err(|_| {
779            GraphBuildError::Store(StoreError::AllocationFailed {
780                needed: requested as u64,
781                component: "adjacency",
782            })
783        })?;
784        slots.resize(slot_count, u32::MAX);
785        let mut degrees = Vec::new();
786
787        let capacity = node_count as usize;
788        let requested = capacity
789            .checked_mul(std::mem::size_of::<u8>())
790            .ok_or_else(|| GraphBuildError::Geometry("degree allocation overflow".to_owned()))?;
791        #[cfg(feature = "allocation-audit")]
792        let allocation =
793            crate::allocation_audit::attributed(|| degrees.try_reserve_exact(capacity));
794        #[cfg(not(feature = "allocation-audit"))]
795        let allocation = degrees.try_reserve_exact(capacity);
796        allocation.map_err(|_| {
797            GraphBuildError::Store(StoreError::AllocationFailed {
798                needed: requested as u64,
799                component: "degree",
800            })
801        })?;
802        degrees.resize(node_count as usize, 0);
803        Ok(Self {
804            slots,
805            degrees,
806            node_count,
807            r_max,
808        })
809    }
810
811    fn row_range(&self, node_id: u32) -> Result<std::ops::Range<usize>, GraphBuildError> {
812        if node_id >= self.node_count {
813            return Err(GraphBuildError::NodeIdOutOfRange {
814                node_id,
815                node_count: self.node_count,
816            });
817        }
818        let start = (node_id as usize)
819            .checked_mul(usize::from(self.r_max))
820            .ok_or_else(|| GraphBuildError::Geometry("adjacency offset overflow".to_owned()))?;
821        let end = start
822            .checked_add(usize::from(self.r_max))
823            .ok_or_else(|| GraphBuildError::Geometry("adjacency end overflow".to_owned()))?;
824        Ok(start..end)
825    }
826
827    fn neighbors(&self, node_id: u32) -> Result<&[u32], GraphBuildError> {
828        let node = usize::try_from(node_id)
829            .map_err(|_| GraphBuildError::Geometry("node id exceeds usize".to_owned()))?;
830        let degree = usize::from(*self.degrees.get(node).ok_or(
831            GraphBuildError::NodeIdOutOfRange {
832                node_id,
833                node_count: self.node_count,
834            },
835        )?);
836        let range = self.row_range(node_id)?;
837        let start = range.start;
838        let active_end = start
839            .checked_add(degree)
840            .ok_or_else(|| GraphBuildError::Geometry("degree end overflow".to_owned()))?;
841        self.slots.get(start..active_end).ok_or_else(|| {
842            GraphBuildError::Geometry(format!("adjacency row {node_id} is unavailable"))
843        })
844    }
845
846    fn set_neighbors(&mut self, node_id: u32, neighbors: &[u32]) -> Result<(), GraphBuildError> {
847        if neighbors.len() > usize::from(self.r_max) {
848            return Err(GraphBuildError::Geometry(format!(
849                "node {node_id} degree {} exceeds hard cap {}",
850                neighbors.len(),
851                self.r_max
852            )));
853        }
854        for neighbor in neighbors {
855            if *neighbor >= self.node_count {
856                return Err(GraphBuildError::NodeIdOutOfRange {
857                    node_id: *neighbor,
858                    node_count: self.node_count,
859                });
860            }
861        }
862        let range = self.row_range(node_id)?;
863        let row = self.slots.get_mut(range).ok_or_else(|| {
864            GraphBuildError::Geometry(format!("adjacency row {node_id} is unavailable"))
865        })?;
866        row.fill(u32::MAX);
867        let destination = row.get_mut(..neighbors.len()).ok_or_else(|| {
868            GraphBuildError::Geometry(format!("adjacency row {node_id} active slice failed"))
869        })?;
870        destination.copy_from_slice(neighbors);
871        let node = usize::try_from(node_id)
872            .map_err(|_| GraphBuildError::Geometry("node id exceeds usize".to_owned()))?;
873        let degree = self
874            .degrees
875            .get_mut(node)
876            .ok_or(GraphBuildError::NodeIdOutOfRange {
877                node_id,
878                node_count: self.node_count,
879            })?;
880        *degree = u8::try_from(neighbors.len())
881            .map_err(|_| GraphBuildError::Geometry("degree exceeds u8".to_owned()))?;
882        Ok(())
883    }
884}
885
886#[derive(Clone, Copy, Debug, Eq, PartialEq)]
887enum BuildPhase {
888    Build,
889    Refine,
890    Complete,
891}
892
893fn build_arena_bytes(node_count: u32, params: GraphParams) -> Result<usize, GraphBuildError> {
894    let rows = usize::try_from(node_count)
895        .map_err(|_| GraphBuildError::Geometry("node count exceeds usize".to_owned()))?;
896    let slots = rows
897        .checked_mul(usize::from(params.r_max()))
898        .and_then(|count| count.checked_mul(std::mem::size_of::<u32>()))
899        .ok_or_else(|| GraphBuildError::Geometry("accounted adjacency overflow".to_owned()))?;
900    let row_state = rows
901        .checked_mul(
902            std::mem::size_of::<u32>()
903                .checked_mul(2)
904                .and_then(|bytes| bytes.checked_add(2))
905                .ok_or_else(|| GraphBuildError::Geometry("row state width overflow".to_owned()))?,
906        )
907        .ok_or_else(|| GraphBuildError::Geometry("accounted row state overflow".to_owned()))?;
908    let construction = usize::from(params.l_build());
909    let scratch_per_candidate = std::mem::size_of::<ScoredNode>()
910        .checked_mul(4)
911        .and_then(|bytes| bytes.checked_add(std::mem::size_of::<u32>() * 6))
912        .ok_or_else(|| GraphBuildError::Geometry("scratch width overflow".to_owned()))?;
913    let candidate_scratch = construction
914        .checked_add(usize::from(params.r_max()))
915        .and_then(|candidates| candidates.checked_mul(scratch_per_candidate))
916        .ok_or_else(|| GraphBuildError::Geometry("scratch arena overflow".to_owned()))?;
917    let medoid_scratch = MEDOID_SAMPLE_ROWS
918        .checked_mul(std::mem::size_of::<ScoredNode>())
919        .ok_or_else(|| GraphBuildError::Geometry("medoid scratch overflow".to_owned()))?;
920    let scratch = candidate_scratch.max(medoid_scratch);
921    slots
922        .checked_add(row_state)
923        .and_then(|bytes| bytes.checked_add(ENTRY_POINT_COUNT * std::mem::size_of::<u32>()))
924        .and_then(|bytes| bytes.checked_add(scratch))
925        .ok_or_else(|| GraphBuildError::Geometry("graph build arena overflow".to_owned()))
926}
927
928struct BuildState {
929    phase: BuildPhase,
930    next_index: usize,
931    order: Vec<u32>,
932    entries: Vec<u32>,
933    adjacency: Adjacency,
934    inserted: Vec<u8>,
935    visited: Vec<u32>,
936    visit_epoch: u32,
937    memory: Option<AccountedCounter>,
938}
939
940impl BuildState {
941    fn new(
942        vectors: &SegmentVectors<'_>,
943        params: GraphParams,
944        seed: u64,
945        accounting: Option<&std::sync::Arc<Accounting>>,
946    ) -> Result<Self, GraphBuildError> {
947        let mut memory = match accounting {
948            Some(accounting) => Some(AccountedCounter::new(
949                accounting,
950                AllocationComponent::Temporary,
951            )?),
952            None => None,
953        };
954        if let Some(charge) = memory.as_mut() {
955            charge.set(build_arena_bytes(vectors.node_count, params)?)?;
956        }
957        let mut random = SplitMix64::new(seed);
958        let mut order = Vec::new();
959
960        let capacity = vectors.node_count as usize;
961        let requested = capacity
962            .checked_mul(std::mem::size_of::<u32>())
963            .ok_or_else(|| {
964                GraphBuildError::Geometry("build order allocation overflow".to_owned())
965            })?;
966        #[cfg(feature = "allocation-audit")]
967        let allocation = crate::allocation_audit::attributed(|| order.try_reserve_exact(capacity));
968        #[cfg(not(feature = "allocation-audit"))]
969        let allocation = order.try_reserve_exact(capacity);
970        allocation.map_err(|_| {
971            GraphBuildError::Store(StoreError::AllocationFailed {
972                needed: requested as u64,
973                component: "build order",
974            })
975        })?;
976        for node in 0..vectors.node_count {
977            order.push(node);
978        }
979        shuffle(&mut order, &mut random)?;
980        let entries = refined_entry_points(vectors, &order)?;
981        move_entries_to_front(&mut order, &entries)?;
982        let mut inserted = Vec::new();
983
984        let capacity = vectors.node_count as usize;
985        let requested = capacity
986            .checked_mul(std::mem::size_of::<u8>())
987            .ok_or_else(|| GraphBuildError::Geometry("inserted allocation overflow".to_owned()))?;
988        #[cfg(feature = "allocation-audit")]
989        let allocation =
990            crate::allocation_audit::attributed(|| inserted.try_reserve_exact(capacity));
991        #[cfg(not(feature = "allocation-audit"))]
992        let allocation = inserted.try_reserve_exact(capacity);
993        allocation.map_err(|_| {
994            GraphBuildError::Store(StoreError::AllocationFailed {
995                needed: requested as u64,
996                component: "inserted",
997            })
998        })?;
999        inserted.resize(vectors.node_count as usize, 0);
1000        let mut visited = Vec::new();
1001
1002        let capacity = vectors.node_count as usize;
1003        let requested = capacity
1004            .checked_mul(std::mem::size_of::<u32>())
1005            .ok_or_else(|| GraphBuildError::Geometry("visited allocation overflow".to_owned()))?;
1006        #[cfg(feature = "allocation-audit")]
1007        let allocation =
1008            crate::allocation_audit::attributed(|| visited.try_reserve_exact(capacity));
1009        #[cfg(not(feature = "allocation-audit"))]
1010        let allocation = visited.try_reserve_exact(capacity);
1011        allocation.map_err(|_| {
1012            GraphBuildError::Store(StoreError::AllocationFailed {
1013                needed: requested as u64,
1014                component: "visited",
1015            })
1016        })?;
1017        visited.resize(vectors.node_count as usize, 0);
1018        Ok(Self {
1019            phase: BuildPhase::Build,
1020            next_index: 0,
1021            order,
1022            entries,
1023            adjacency: Adjacency::new(vectors.node_count, params.r_max())?,
1024            inserted,
1025            visited,
1026            visit_epoch: 0,
1027            memory,
1028        })
1029    }
1030
1031    fn is_complete(&self, _passes: GraphBuildPasses) -> bool {
1032        self.phase == BuildPhase::Complete
1033    }
1034
1035    fn advance_batch(
1036        &mut self,
1037        vectors: &SegmentVectors<'_>,
1038        params: GraphParams,
1039        seed: u64,
1040        passes: GraphBuildPasses,
1041        cancellation: Option<&QueryCancellation<'_>>,
1042    ) -> Result<bool, GraphBuildError> {
1043        if self.phase == BuildPhase::Complete {
1044            return Ok(true);
1045        }
1046        let batch_rows = usize::try_from(params.checkpoint_batch_rows()).map_err(|_| {
1047            GraphBuildError::Geometry("checkpoint batch rows exceed usize".to_owned())
1048        })?;
1049        let end = self
1050            .next_index
1051            .checked_add(batch_rows)
1052            .map(|end| end.min(self.order.len()))
1053            .ok_or_else(|| GraphBuildError::Geometry("batch end overflow".to_owned()))?;
1054        let alpha = match self.phase {
1055            BuildPhase::Build => params.alpha_build(),
1056            BuildPhase::Refine => params.alpha_refine(),
1057            BuildPhase::Complete => return Ok(true),
1058        };
1059        let pass_seed = match self.phase {
1060            BuildPhase::Build => seed,
1061            BuildPhase::Refine => seed ^ 0xa1fa_1200_0000_0002,
1062            BuildPhase::Complete => seed,
1063        };
1064        for position in self.next_index..end {
1065            if position.is_multiple_of(CANCELLATION_CHECK_ROWS)
1066                && let Some(cancellation) = cancellation
1067            {
1068                check_cancellation(cancellation)?;
1069            }
1070            let order_position = match self.phase {
1071                BuildPhase::Build => position,
1072                BuildPhase::Refine => {
1073                    self.order.len().checked_sub(position + 1).ok_or_else(|| {
1074                        GraphBuildError::Geometry("refinement order underflow".to_owned())
1075                    })?
1076                }
1077                BuildPhase::Complete => position,
1078            };
1079            let node_id = *self.order.get(order_position).ok_or_else(|| {
1080                GraphBuildError::Geometry(format!(
1081                    "build order position {order_position} is unavailable"
1082                ))
1083            })?;
1084            process_node(
1085                vectors,
1086                params,
1087                pass_seed,
1088                alpha,
1089                node_id,
1090                &self.entries,
1091                &mut self.adjacency,
1092                &mut self.inserted,
1093                &mut self.visited,
1094                &mut self.visit_epoch,
1095            )?;
1096        }
1097        self.next_index = end;
1098        if self.next_index == self.order.len() {
1099            match (self.phase, passes) {
1100                (BuildPhase::Build, GraphBuildPasses::Two) => {
1101                    self.phase = BuildPhase::Refine;
1102                    self.next_index = 0;
1103                    self.inserted.fill(1);
1104                }
1105                (BuildPhase::Build, GraphBuildPasses::One) | (BuildPhase::Refine, _) => {
1106                    self.phase = BuildPhase::Complete;
1107                }
1108                (BuildPhase::Complete, _) => {}
1109            }
1110        }
1111        Ok(self.phase == BuildPhase::Complete)
1112    }
1113}
1114
1115fn check_cancellation(cancellation: &QueryCancellation<'_>) -> Result<(), GraphBuildError> {
1116    match cancellation.check_graph() {
1117        Ok(()) => Ok(()),
1118        Err(ScanError::Cancelled { .. }) => Err(GraphBuildError::Cancelled { partial: false }),
1119        Err(ScanError::Timeout { .. }) => Err(GraphBuildError::Timeout { partial: false }),
1120        Err(ScanError::ReadCancelled { .. }) => {
1121            Err(GraphBuildError::ReadCancelled { partial: false })
1122        }
1123        Err(error) => Err(GraphBuildError::Geometry(format!(
1124            "query cancellation seam returned non-cancellation error: {error}"
1125        ))),
1126    }
1127}
1128
1129fn checkpoint_temp_path(path: &Path) -> PathBuf {
1130    let mut temporary = path.as_os_str().to_os_string();
1131    temporary.push(".tmp");
1132    PathBuf::from(temporary)
1133}
1134
1135#[allow(clippy::too_many_arguments)]
1136fn write_checkpoint(
1137    vfs: &dyn Vfs,
1138    policy: DurabilityPolicy,
1139    path: &Path,
1140    vectors: &SegmentVectors<'_>,
1141    params: GraphParams,
1142    seed: u64,
1143    passes: GraphBuildPasses,
1144    state: &BuildState,
1145) -> Result<(), GraphBuildError> {
1146    let directory = path.parent().ok_or_else(|| GraphBuildError::CheckpointIo {
1147        path: path.to_path_buf(),
1148        source: std::io::Error::new(
1149            std::io::ErrorKind::InvalidInput,
1150            "checkpoint path has no parent directory",
1151        ),
1152    })?;
1153    let slot_bytes = state
1154        .adjacency
1155        .slots
1156        .len()
1157        .checked_mul(std::mem::size_of::<u32>())
1158        .ok_or_else(|| GraphBuildError::Geometry("checkpoint slot bytes overflow".to_owned()))?;
1159    let payload_bytes = state
1160        .adjacency
1161        .degrees
1162        .len()
1163        .checked_add(slot_bytes)
1164        .ok_or_else(|| GraphBuildError::Geometry("checkpoint payload overflow".to_owned()))?;
1165    let total_bytes = CHECKPOINT_HEADER_BYTES
1166        .checked_add(payload_bytes)
1167        .and_then(|bytes| bytes.checked_add(CHECKPOINT_CHECKSUM_BYTES))
1168        .ok_or_else(|| GraphBuildError::Geometry("checkpoint length overflow".to_owned()))?;
1169    let mut bytes = Vec::with_capacity(total_bytes);
1170    bytes.extend_from_slice(&CHECKPOINT_MAGIC);
1171    bytes.extend_from_slice(&CHECKPOINT_VERSION.to_le_bytes());
1172    bytes.push(phase_code(state.phase));
1173    bytes.push(pass_code(passes));
1174    bytes.push(params.r_target());
1175    bytes.push(params.r_max());
1176    bytes.extend_from_slice(&0_u16.to_le_bytes());
1177    bytes.extend_from_slice(&params.l_build().to_le_bytes());
1178    bytes.extend_from_slice(&0_u16.to_le_bytes());
1179    bytes.extend_from_slice(&params.checkpoint_batch_rows().to_le_bytes());
1180    bytes.extend_from_slice(&vectors.node_count.to_le_bytes());
1181    let dimensions = u32::try_from(vectors.dimensions)
1182        .map_err(|_| GraphBuildError::Geometry("checkpoint dimensions exceed u32".to_owned()))?;
1183    bytes.extend_from_slice(&dimensions.to_le_bytes());
1184    let next_index = u32::try_from(state.next_index)
1185        .map_err(|_| GraphBuildError::Geometry("checkpoint position exceeds u32".to_owned()))?;
1186    bytes.extend_from_slice(&next_index.to_le_bytes());
1187    bytes.extend_from_slice(&0_u32.to_le_bytes());
1188    bytes.extend_from_slice(&seed.to_le_bytes());
1189    bytes.extend_from_slice(&params.alpha_build().to_bits().to_le_bytes());
1190    bytes.extend_from_slice(&params.alpha_refine().to_bits().to_le_bytes());
1191    bytes.extend_from_slice(&vectors.segment_id);
1192    bytes.extend_from_slice(
1193        &u64::try_from(payload_bytes)
1194            .map_err(|_| GraphBuildError::Geometry("checkpoint payload exceeds u64".to_owned()))?
1195            .to_le_bytes(),
1196    );
1197    if bytes.len() != CHECKPOINT_HEADER_BYTES {
1198        return Err(GraphBuildError::Geometry(format!(
1199            "checkpoint header encoded {} bytes, expected {CHECKPOINT_HEADER_BYTES}",
1200            bytes.len()
1201        )));
1202    }
1203    bytes.extend_from_slice(&state.adjacency.degrees);
1204    for neighbor in &state.adjacency.slots {
1205        bytes.extend_from_slice(&neighbor.to_le_bytes());
1206    }
1207    bytes.extend_from_slice(&xxh3_64(&bytes).to_le_bytes());
1208    let temporary = checkpoint_temp_path(path);
1209    if let Err(source) = vfs.write(&temporary, &bytes) {
1210        let _ = vfs.delete(&temporary);
1211        return Err(GraphBuildError::CheckpointIo {
1212            path: temporary,
1213            source,
1214        });
1215    }
1216    if let SyncRequirement::Sync(kind) = policy.data_file_sync()
1217        && let Err(source) = vfs.sync(&temporary, kind)
1218    {
1219        let _ = vfs.delete(&temporary);
1220        return Err(GraphBuildError::CheckpointIo {
1221            path: temporary,
1222            source,
1223        });
1224    }
1225    if let Err(source) = vfs.rename(&temporary, path) {
1226        let _ = vfs.delete(&temporary);
1227        return Err(GraphBuildError::CheckpointIo {
1228            path: path.to_path_buf(),
1229            source,
1230        });
1231    }
1232    match policy.directory_sync() {
1233        SyncRequirement::Skip => Ok(()),
1234        SyncRequirement::Sync(kind) => {
1235            vfs.sync(directory, kind)
1236                .map_err(|source| GraphBuildError::CheckpointIo {
1237                    path: directory.to_path_buf(),
1238                    source,
1239                })
1240        }
1241    }
1242}
1243
1244fn checkpoint_encoded_bytes(state: &BuildState) -> Result<usize, GraphBuildError> {
1245    let slot_bytes = state
1246        .adjacency
1247        .slots
1248        .len()
1249        .checked_mul(std::mem::size_of::<u32>())
1250        .ok_or_else(|| GraphBuildError::Geometry("checkpoint slot bytes overflow".to_owned()))?;
1251    CHECKPOINT_HEADER_BYTES
1252        .checked_add(state.adjacency.degrees.len())
1253        .and_then(|bytes| bytes.checked_add(slot_bytes))
1254        .and_then(|bytes| bytes.checked_add(CHECKPOINT_CHECKSUM_BYTES))
1255        .ok_or_else(|| GraphBuildError::Geometry("checkpoint encoded bytes overflow".to_owned()))
1256}
1257
1258fn remove_checkpoint(vfs: &dyn Vfs, path: &Path) -> Result<(), GraphBuildError> {
1259    let temporary = checkpoint_temp_path(path);
1260    match vfs.delete(&temporary) {
1261        Ok(()) => {}
1262        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
1263        Err(source) => {
1264            return Err(GraphBuildError::CheckpointIo {
1265                path: temporary,
1266                source,
1267            });
1268        }
1269    }
1270    match vfs.delete(path) {
1271        Ok(()) => Ok(()),
1272        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
1273        Err(source) => Err(GraphBuildError::CheckpointIo {
1274            path: path.to_path_buf(),
1275            source,
1276        }),
1277    }
1278}
1279
1280fn decode_checkpoint(
1281    bytes: &[u8],
1282    vectors: &SegmentVectors<'_>,
1283    params: GraphParams,
1284    seed: u64,
1285    passes: GraphBuildPasses,
1286    accounting: Option<&std::sync::Arc<Accounting>>,
1287) -> Result<BuildState, GraphBuildError> {
1288    validate_graph_build_checkpoint(bytes)?;
1289    let minimum = CHECKPOINT_HEADER_BYTES
1290        .checked_add(CHECKPOINT_CHECKSUM_BYTES)
1291        .ok_or_else(|| GraphBuildError::CheckpointCorrupt("minimum length overflow".to_owned()))?;
1292    if bytes.len() < minimum {
1293        return Err(GraphBuildError::CheckpointCorrupt(format!(
1294            "truncated: need at least {minimum} bytes, got {}",
1295            bytes.len()
1296        )));
1297    }
1298    if bytes.get(..8) != Some(CHECKPOINT_MAGIC.as_slice()) {
1299        return Err(GraphBuildError::CheckpointCorrupt(
1300            "magic does not match ZEVAMCP1".to_owned(),
1301        ));
1302    }
1303    let checksum_start = bytes
1304        .len()
1305        .checked_sub(CHECKPOINT_CHECKSUM_BYTES)
1306        .ok_or_else(|| GraphBuildError::CheckpointCorrupt("checksum underflow".to_owned()))?;
1307    let stored_checksum = read_checkpoint_u64(bytes, checksum_start, "checksum")?;
1308    let checksummed = bytes.get(..checksum_start).ok_or_else(|| {
1309        GraphBuildError::CheckpointCorrupt("checksummed prefix is unavailable".to_owned())
1310    })?;
1311    let actual_checksum = xxh3_64(checksummed);
1312    if stored_checksum != actual_checksum {
1313        return Err(GraphBuildError::CheckpointCorrupt(format!(
1314            "xxh3-64 expected {stored_checksum:#018x}, computed {actual_checksum:#018x}"
1315        )));
1316    }
1317    let version = read_checkpoint_u16(bytes, 8, "version")?;
1318    if version != CHECKPOINT_VERSION {
1319        return Err(GraphBuildError::CheckpointCorrupt(format!(
1320            "version {version}, expected {CHECKPOINT_VERSION}"
1321        )));
1322    }
1323    let phase = decode_phase(read_checkpoint_u8(bytes, 10, "phase")?)?;
1324    let stored_passes = decode_passes(read_checkpoint_u8(bytes, 11, "passes")?)?;
1325    if stored_passes != passes {
1326        return Err(GraphBuildError::CheckpointCorrupt(
1327            "pass count does not match requested build".to_owned(),
1328        ));
1329    }
1330    let stored_r_target = read_checkpoint_u8(bytes, 12, "target degree")?;
1331    let stored_r_max = read_checkpoint_u8(bytes, 13, "maximum degree")?;
1332    let reserved_14 = read_checkpoint_u16(bytes, 14, "reserved degree bytes")?;
1333    let stored_l_build = read_checkpoint_u16(bytes, 16, "construction width")?;
1334    let reserved_18 = read_checkpoint_u16(bytes, 18, "reserved width bytes")?;
1335    let stored_batch = read_checkpoint_u32(bytes, 20, "checkpoint batch")?;
1336    let node_count = read_checkpoint_u32(bytes, 24, "node count")?;
1337    let dimensions = read_checkpoint_u32(bytes, 28, "dimensions")?;
1338    let next_index = read_checkpoint_u32(bytes, 32, "next index")?;
1339    let reserved_36 = read_checkpoint_u32(bytes, 36, "reserved index bytes")?;
1340    let stored_seed = read_checkpoint_u64(bytes, 40, "seed")?;
1341    let stored_alpha_build = f32::from_bits(read_checkpoint_u32(bytes, 48, "build alpha")?);
1342    let stored_alpha_refine = f32::from_bits(read_checkpoint_u32(bytes, 52, "refine alpha")?);
1343    let segment_id = bytes
1344        .get(56..72)
1345        .ok_or_else(|| GraphBuildError::CheckpointCorrupt("segment id is truncated".to_owned()))?;
1346    let payload_bytes = read_checkpoint_u64(bytes, 72, "payload length")?;
1347    if reserved_14 != 0 || reserved_18 != 0 || reserved_36 != 0 {
1348        return Err(GraphBuildError::CheckpointCorrupt(
1349            "reserved header bytes are non-zero".to_owned(),
1350        ));
1351    }
1352    if stored_r_target != params.r_target()
1353        || stored_r_max != params.r_max()
1354        || stored_l_build != params.l_build()
1355        || stored_batch != params.checkpoint_batch_rows()
1356        || stored_alpha_build.to_bits() != params.alpha_build().to_bits()
1357        || stored_alpha_refine.to_bits() != params.alpha_refine().to_bits()
1358        || node_count != vectors.node_count
1359        || dimensions != vectors.dimensions as u32
1360        || stored_seed != seed
1361        || segment_id != vectors.segment_id
1362    {
1363        return Err(GraphBuildError::CheckpointCorrupt(
1364            "checkpoint identity or graph parameters do not match input".to_owned(),
1365        ));
1366    }
1367    if passes == GraphBuildPasses::One && phase == BuildPhase::Refine {
1368        return Err(GraphBuildError::CheckpointCorrupt(
1369            "one-pass build cannot resume refinement".to_owned(),
1370        ));
1371    }
1372    let actual_payload = checksum_start
1373        .checked_sub(CHECKPOINT_HEADER_BYTES)
1374        .ok_or_else(|| GraphBuildError::CheckpointCorrupt("payload underflow".to_owned()))?;
1375    if usize::try_from(payload_bytes).ok() != Some(actual_payload) {
1376        return Err(GraphBuildError::CheckpointCorrupt(format!(
1377            "declared payload {payload_bytes} bytes, actual {actual_payload}"
1378        )));
1379    }
1380    let degree_bytes = node_count as usize;
1381    let degrees_end = CHECKPOINT_HEADER_BYTES
1382        .checked_add(degree_bytes)
1383        .ok_or_else(|| GraphBuildError::CheckpointCorrupt("degree end overflow".to_owned()))?;
1384    let degrees = bytes
1385        .get(CHECKPOINT_HEADER_BYTES..degrees_end)
1386        .ok_or_else(|| GraphBuildError::CheckpointCorrupt("degree bytes are truncated".to_owned()))?
1387        .to_vec();
1388    let expected_slots = (node_count as usize)
1389        .checked_mul(usize::from(params.r_max()))
1390        .ok_or_else(|| GraphBuildError::CheckpointCorrupt("slot count overflow".to_owned()))?;
1391    let slot_payload = bytes.get(degrees_end..checksum_start).ok_or_else(|| {
1392        GraphBuildError::CheckpointCorrupt("neighbor payload is unavailable".to_owned())
1393    })?;
1394    let expected_slot_bytes = expected_slots
1395        .checked_mul(std::mem::size_of::<u32>())
1396        .ok_or_else(|| GraphBuildError::CheckpointCorrupt("slot bytes overflow".to_owned()))?;
1397    if slot_payload.len() != expected_slot_bytes {
1398        return Err(GraphBuildError::CheckpointCorrupt(format!(
1399            "neighbor payload {} bytes, expected {expected_slot_bytes}",
1400            slot_payload.len()
1401        )));
1402    }
1403    let slots = slot_payload
1404        .chunks_exact(4)
1405        .map(|chunk| {
1406            let raw: [u8; 4] = chunk.try_into().map_err(|_| {
1407                GraphBuildError::CheckpointCorrupt("neighbor width is not four bytes".to_owned())
1408            })?;
1409            Ok(u32::from_le_bytes(raw))
1410        })
1411        .collect::<Result<Vec<_>, GraphBuildError>>()?;
1412    let adjacency = Adjacency {
1413        slots,
1414        degrees,
1415        node_count,
1416        r_max: params.r_max(),
1417    };
1418    validate_checkpoint_adjacency(&adjacency)?;
1419    let mut state = BuildState::new(vectors, params, seed, accounting)?;
1420    state.phase = phase;
1421    state.next_index = usize::try_from(next_index)
1422        .map_err(|_| GraphBuildError::CheckpointCorrupt("next index exceeds usize".to_owned()))?;
1423    if state.next_index > state.order.len() {
1424        return Err(GraphBuildError::CheckpointCorrupt(format!(
1425            "next index {} exceeds row count {}",
1426            state.next_index,
1427            state.order.len()
1428        )));
1429    }
1430    state.adjacency = adjacency;
1431    state.inserted.fill(u8::from(phase != BuildPhase::Build));
1432    if phase == BuildPhase::Build {
1433        for node_id in state.order.iter().take(state.next_index) {
1434            let node = vectors.validate_node(*node_id)?;
1435            let marker = state.inserted.get_mut(node).ok_or_else(|| {
1436                GraphBuildError::CheckpointCorrupt("inserted marker is unavailable".to_owned())
1437            })?;
1438            *marker = 1;
1439        }
1440    }
1441    Ok(state)
1442}
1443
1444/// Validates arbitrary resumable-build checkpoint bytes without allocating a graph.
1445///
1446/// This is the shared parser seam used by the dependency-free fuzz target.
1447pub fn validate_graph_build_checkpoint(bytes: &[u8]) -> Result<(), GraphBuildError> {
1448    let minimum = CHECKPOINT_HEADER_BYTES
1449        .checked_add(CHECKPOINT_CHECKSUM_BYTES)
1450        .ok_or_else(|| GraphBuildError::CheckpointCorrupt("minimum length overflow".to_owned()))?;
1451    if bytes.len() < minimum {
1452        return Err(GraphBuildError::CheckpointCorrupt(format!(
1453            "truncated: need at least {minimum} bytes, got {}",
1454            bytes.len()
1455        )));
1456    }
1457    if bytes.get(..8) != Some(CHECKPOINT_MAGIC.as_slice()) {
1458        return Err(GraphBuildError::CheckpointCorrupt(
1459            "magic does not match ZEVAMCP1".to_owned(),
1460        ));
1461    }
1462    let checksum_start = bytes
1463        .len()
1464        .checked_sub(CHECKPOINT_CHECKSUM_BYTES)
1465        .ok_or_else(|| GraphBuildError::CheckpointCorrupt("checksum underflow".to_owned()))?;
1466    let stored_checksum = read_checkpoint_u64(bytes, checksum_start, "checksum")?;
1467    let checksummed = bytes.get(..checksum_start).ok_or_else(|| {
1468        GraphBuildError::CheckpointCorrupt("checksummed prefix is unavailable".to_owned())
1469    })?;
1470    let actual_checksum = xxh3_64(checksummed);
1471    if stored_checksum != actual_checksum {
1472        return Err(GraphBuildError::CheckpointCorrupt(format!(
1473            "xxh3-64 expected {stored_checksum:#018x}, computed {actual_checksum:#018x}"
1474        )));
1475    }
1476    let version = read_checkpoint_u16(bytes, 8, "version")?;
1477    if version != CHECKPOINT_VERSION {
1478        return Err(GraphBuildError::CheckpointCorrupt(format!(
1479            "version {version}, expected {CHECKPOINT_VERSION}"
1480        )));
1481    }
1482    let _ = decode_phase(read_checkpoint_u8(bytes, 10, "phase")?)?;
1483    let _ = decode_passes(read_checkpoint_u8(bytes, 11, "passes")?)?;
1484    let r_target = read_checkpoint_u8(bytes, 12, "target degree")?;
1485    let r_max = read_checkpoint_u8(bytes, 13, "maximum degree")?;
1486    let reserved_14 = read_checkpoint_u16(bytes, 14, "reserved degree bytes")?;
1487    let l_build = read_checkpoint_u16(bytes, 16, "construction width")?;
1488    let reserved_18 = read_checkpoint_u16(bytes, 18, "reserved width bytes")?;
1489    let checkpoint_batch = read_checkpoint_u32(bytes, 20, "checkpoint batch")?;
1490    let node_count = read_checkpoint_u32(bytes, 24, "node count")?;
1491    let dimensions = read_checkpoint_u32(bytes, 28, "dimensions")?;
1492    let next_index = read_checkpoint_u32(bytes, 32, "next position")?;
1493    let reserved_36 = read_checkpoint_u32(bytes, 36, "reserved position bytes")?;
1494    if reserved_14 != 0 || reserved_18 != 0 || reserved_36 != 0 {
1495        return Err(GraphBuildError::CheckpointCorrupt(
1496            "reserved header bytes are nonzero".to_owned(),
1497        ));
1498    }
1499    if r_target == 0 || r_target > r_max || l_build < u16::from(r_target) {
1500        return Err(GraphBuildError::CheckpointCorrupt(
1501            "stored degree and construction width are invalid".to_owned(),
1502        ));
1503    }
1504    if checkpoint_batch == 0 || node_count == 0 || dimensions == 0 || next_index > node_count {
1505        return Err(GraphBuildError::CheckpointCorrupt(
1506            "stored batch, shape, or next position is invalid".to_owned(),
1507        ));
1508    }
1509    let alpha_build = f32::from_bits(read_checkpoint_u32(bytes, 48, "build alpha")?);
1510    let alpha_refine = f32::from_bits(read_checkpoint_u32(bytes, 52, "refine alpha")?);
1511    if !alpha_build.is_finite()
1512        || !alpha_refine.is_finite()
1513        || alpha_build < 1.0
1514        || alpha_refine < alpha_build
1515    {
1516        return Err(GraphBuildError::CheckpointCorrupt(
1517            "stored alpha values are invalid".to_owned(),
1518        ));
1519    }
1520    let payload_bytes = usize::try_from(read_checkpoint_u64(bytes, 72, "payload bytes")?)
1521        .map_err(|_| GraphBuildError::CheckpointCorrupt("payload exceeds usize".to_owned()))?;
1522    let node_count_usize = usize::try_from(node_count)
1523        .map_err(|_| GraphBuildError::CheckpointCorrupt("node count exceeds usize".to_owned()))?;
1524    let slot_count = node_count_usize
1525        .checked_mul(usize::from(r_max))
1526        .ok_or_else(|| GraphBuildError::CheckpointCorrupt("slot count overflow".to_owned()))?;
1527    let expected_payload = slot_count
1528        .checked_mul(std::mem::size_of::<u32>())
1529        .and_then(|slot_bytes| slot_bytes.checked_add(node_count_usize))
1530        .ok_or_else(|| {
1531            GraphBuildError::CheckpointCorrupt("payload geometry overflow".to_owned())
1532        })?;
1533    if payload_bytes != expected_payload {
1534        return Err(GraphBuildError::CheckpointCorrupt(format!(
1535            "payload bytes {payload_bytes}, expected {expected_payload}"
1536        )));
1537    }
1538    let expected_length = CHECKPOINT_HEADER_BYTES
1539        .checked_add(payload_bytes)
1540        .and_then(|length| length.checked_add(CHECKPOINT_CHECKSUM_BYTES))
1541        .ok_or_else(|| GraphBuildError::CheckpointCorrupt("file length overflow".to_owned()))?;
1542    if bytes.len() != expected_length {
1543        return Err(GraphBuildError::CheckpointCorrupt(format!(
1544            "file length {}, expected {expected_length}",
1545            bytes.len()
1546        )));
1547    }
1548    let degrees_start = CHECKPOINT_HEADER_BYTES;
1549    let slots_start = degrees_start
1550        .checked_add(node_count_usize)
1551        .ok_or_else(|| GraphBuildError::CheckpointCorrupt("slot offset overflow".to_owned()))?;
1552    for owner in 0..node_count_usize {
1553        let degree = usize::from(read_checkpoint_u8(
1554            bytes,
1555            degrees_start + owner,
1556            "node degree",
1557        )?);
1558        if degree > usize::from(r_max) {
1559            return Err(GraphBuildError::CheckpointCorrupt(format!(
1560                "node {owner} degree {degree} exceeds {r_max}"
1561            )));
1562        }
1563        let row_start = owner
1564            .checked_mul(usize::from(r_max))
1565            .and_then(|slot| slot.checked_mul(std::mem::size_of::<u32>()))
1566            .and_then(|offset| slots_start.checked_add(offset))
1567            .ok_or_else(|| GraphBuildError::CheckpointCorrupt("row offset overflow".to_owned()))?;
1568        let mut active = Vec::with_capacity(degree);
1569        for slot in 0..usize::from(r_max) {
1570            let offset = slot
1571                .checked_mul(std::mem::size_of::<u32>())
1572                .and_then(|offset| row_start.checked_add(offset))
1573                .ok_or_else(|| {
1574                    GraphBuildError::CheckpointCorrupt("neighbor offset overflow".to_owned())
1575                })?;
1576            let neighbor = read_checkpoint_u32(bytes, offset, "neighbor")?;
1577            if slot < degree {
1578                if neighbor >= node_count
1579                    || neighbor as usize == owner
1580                    || active.contains(&neighbor)
1581                {
1582                    return Err(GraphBuildError::CheckpointCorrupt(format!(
1583                        "node {owner} has invalid active neighbor {neighbor}"
1584                    )));
1585                }
1586                active.push(neighbor);
1587            } else if neighbor != u32::MAX {
1588                return Err(GraphBuildError::CheckpointCorrupt(format!(
1589                    "node {owner} inactive slot {slot} is not the sentinel"
1590                )));
1591            }
1592        }
1593    }
1594    Ok(())
1595}
1596
1597fn validate_checkpoint_adjacency(adjacency: &Adjacency) -> Result<(), GraphBuildError> {
1598    for node_id in 0..adjacency.node_count {
1599        let neighbors = adjacency.neighbors(node_id)?;
1600        if neighbors.len() > usize::from(adjacency.r_max) {
1601            return Err(GraphBuildError::CheckpointCorrupt(format!(
1602                "node {node_id} exceeds maximum degree"
1603            )));
1604        }
1605        let mut seen = Vec::with_capacity(neighbors.len());
1606        for neighbor in neighbors {
1607            if *neighbor >= adjacency.node_count || *neighbor == node_id || seen.contains(neighbor)
1608            {
1609                return Err(GraphBuildError::CheckpointCorrupt(format!(
1610                    "node {node_id} has an out-of-range, self, or duplicate neighbor {neighbor}"
1611                )));
1612            }
1613            seen.push(*neighbor);
1614        }
1615        let range = adjacency.row_range(node_id)?;
1616        let inactive_start = range.start.checked_add(neighbors.len()).ok_or_else(|| {
1617            GraphBuildError::CheckpointCorrupt("inactive offset overflow".to_owned())
1618        })?;
1619        let inactive = adjacency
1620            .slots
1621            .get(inactive_start..range.end)
1622            .ok_or_else(|| {
1623                GraphBuildError::CheckpointCorrupt("inactive slots are unavailable".to_owned())
1624            })?;
1625        if inactive.iter().any(|neighbor| *neighbor != u32::MAX) {
1626            return Err(GraphBuildError::CheckpointCorrupt(format!(
1627                "node {node_id} has non-sentinel inactive slots"
1628            )));
1629        }
1630    }
1631    Ok(())
1632}
1633
1634fn phase_code(phase: BuildPhase) -> u8 {
1635    match phase {
1636        BuildPhase::Build => 1,
1637        BuildPhase::Refine => 2,
1638        BuildPhase::Complete => 3,
1639    }
1640}
1641
1642fn decode_phase(value: u8) -> Result<BuildPhase, GraphBuildError> {
1643    match value {
1644        1 => Ok(BuildPhase::Build),
1645        2 => Ok(BuildPhase::Refine),
1646        3 => Ok(BuildPhase::Complete),
1647        _ => Err(GraphBuildError::CheckpointCorrupt(format!(
1648            "unknown phase {value}"
1649        ))),
1650    }
1651}
1652
1653fn pass_code(passes: GraphBuildPasses) -> u8 {
1654    match passes {
1655        GraphBuildPasses::One => 1,
1656        GraphBuildPasses::Two => 2,
1657    }
1658}
1659
1660fn decode_passes(value: u8) -> Result<GraphBuildPasses, GraphBuildError> {
1661    match value {
1662        1 => Ok(GraphBuildPasses::One),
1663        2 => Ok(GraphBuildPasses::Two),
1664        _ => Err(GraphBuildError::CheckpointCorrupt(format!(
1665            "unknown pass count {value}"
1666        ))),
1667    }
1668}
1669
1670fn read_checkpoint_u8(bytes: &[u8], offset: usize, field: &str) -> Result<u8, GraphBuildError> {
1671    bytes.get(offset).copied().ok_or_else(|| {
1672        GraphBuildError::CheckpointCorrupt(format!("{field} is truncated at byte {offset}"))
1673    })
1674}
1675
1676fn read_checkpoint_u16(bytes: &[u8], offset: usize, field: &str) -> Result<u16, GraphBuildError> {
1677    let end = offset
1678        .checked_add(2)
1679        .ok_or_else(|| GraphBuildError::CheckpointCorrupt(format!("{field} offset overflow")))?;
1680    let raw: [u8; 2] = bytes
1681        .get(offset..end)
1682        .ok_or_else(|| GraphBuildError::CheckpointCorrupt(format!("{field} is truncated")))?
1683        .try_into()
1684        .map_err(|_| GraphBuildError::CheckpointCorrupt(format!("{field} width is invalid")))?;
1685    Ok(u16::from_le_bytes(raw))
1686}
1687
1688fn read_checkpoint_u32(bytes: &[u8], offset: usize, field: &str) -> Result<u32, GraphBuildError> {
1689    let end = offset
1690        .checked_add(4)
1691        .ok_or_else(|| GraphBuildError::CheckpointCorrupt(format!("{field} offset overflow")))?;
1692    let raw: [u8; 4] = bytes
1693        .get(offset..end)
1694        .ok_or_else(|| GraphBuildError::CheckpointCorrupt(format!("{field} is truncated")))?
1695        .try_into()
1696        .map_err(|_| GraphBuildError::CheckpointCorrupt(format!("{field} width is invalid")))?;
1697    Ok(u32::from_le_bytes(raw))
1698}
1699
1700fn read_checkpoint_u64(bytes: &[u8], offset: usize, field: &str) -> Result<u64, GraphBuildError> {
1701    let end = offset
1702        .checked_add(8)
1703        .ok_or_else(|| GraphBuildError::CheckpointCorrupt(format!("{field} offset overflow")))?;
1704    let raw: [u8; 8] = bytes
1705        .get(offset..end)
1706        .ok_or_else(|| GraphBuildError::CheckpointCorrupt(format!("{field} is truncated")))?
1707        .try_into()
1708        .map_err(|_| GraphBuildError::CheckpointCorrupt(format!("{field} width is invalid")))?;
1709    Ok(u64::from_le_bytes(raw))
1710}
1711
1712#[derive(Clone, Copy, Debug)]
1713struct ScoredNode {
1714    node_id: u32,
1715    distance: f64,
1716}
1717
1718#[allow(clippy::too_many_arguments)]
1719fn process_node(
1720    vectors: &SegmentVectors<'_>,
1721    params: GraphParams,
1722    seed: u64,
1723    alpha: f32,
1724    node_id: u32,
1725    entries: &[u32],
1726    adjacency: &mut Adjacency,
1727    inserted: &mut [u8],
1728    visited: &mut [u32],
1729    visit_epoch: &mut u32,
1730) -> Result<(), GraphBuildError> {
1731    vectors.validate_node(node_id)?;
1732    let candidates = search_candidates(
1733        vectors,
1734        adjacency,
1735        inserted,
1736        node_id,
1737        entries,
1738        usize::from(params.l_build()),
1739        seed,
1740        visited,
1741        visit_epoch,
1742    )?;
1743    let mut candidate_ids = Vec::new();
1744
1745    let capacity = candidates
1746        .len()
1747        .saturating_add(adjacency.neighbors(node_id)?.len());
1748    let requested = capacity
1749        .checked_mul(std::mem::size_of::<u32>())
1750        .ok_or_else(|| GraphBuildError::Geometry("candidate id allocation overflow".to_owned()))?;
1751    #[cfg(feature = "allocation-audit")]
1752    let allocation =
1753        crate::allocation_audit::attributed(|| candidate_ids.try_reserve_exact(capacity));
1754    #[cfg(not(feature = "allocation-audit"))]
1755    let allocation = candidate_ids.try_reserve_exact(capacity);
1756    allocation.map_err(|_| {
1757        GraphBuildError::Store(StoreError::AllocationFailed {
1758            needed: requested as u64,
1759            component: "candidate id",
1760        })
1761    })?;
1762    for candidate in candidates {
1763        candidate_ids.push(candidate.node_id);
1764    }
1765    candidate_ids.extend_from_slice(adjacency.neighbors(node_id)?);
1766    let pruned = robust_prune(
1767        vectors,
1768        node_id,
1769        &candidate_ids,
1770        alpha,
1771        usize::from(params.r_target()),
1772    )?;
1773    drop(candidate_ids);
1774    adjacency.set_neighbors(node_id, &pruned)?;
1775    let node = vectors.validate_node(node_id)?;
1776    let inserted_node = inserted.get_mut(node).ok_or_else(|| {
1777        GraphBuildError::Geometry(format!("inserted marker {node_id} is unavailable"))
1778    })?;
1779    *inserted_node = 1;
1780    add_reciprocal_edges(vectors, adjacency, node_id, &pruned, params, alpha)?;
1781    drop(pruned);
1782    Ok(())
1783}
1784
1785#[allow(clippy::too_many_arguments)]
1786fn search_candidates(
1787    vectors: &SegmentVectors<'_>,
1788    adjacency: &Adjacency,
1789    inserted: &[u8],
1790    query_id: u32,
1791    entries: &[u32],
1792    width: usize,
1793    seed: u64,
1794    visited: &mut [u32],
1795    visit_epoch: &mut u32,
1796) -> Result<Vec<ScoredNode>, GraphBuildError> {
1797    let query = vectors.f32_row(query_id)?;
1798    let mut query_norm_squared = 0.0_f64;
1799    for chunk in query.chunks(256) {
1800        for value in chunk {
1801            query_norm_squared += f64::from(*value) * f64::from(*value);
1802        }
1803    }
1804
1805    let prepared =
1806        prepare_bit4_query(query, seed ^ u64::from(query_id)).map_err(|error| match error {
1807            QuantError::Allocation => GraphBuildError::Store(StoreError::AllocationFailed {
1808                needed: query.len() as u64,
1809                component: "graph query codes",
1810            }),
1811            error => GraphBuildError::Quant(error),
1812        })?;
1813    *visit_epoch = visit_epoch.wrapping_add(1);
1814    if *visit_epoch == 0 {
1815        visited.fill(0);
1816        *visit_epoch = 1;
1817    }
1818    let epoch = *visit_epoch;
1819    let mut result = Vec::new();
1820
1821    let capacity = width.saturating_add(1);
1822    let requested = capacity
1823        .checked_mul(std::mem::size_of::<ScoredNode>())
1824        .ok_or_else(|| GraphBuildError::Geometry("result allocation overflow".to_owned()))?;
1825    #[cfg(feature = "allocation-audit")]
1826    let allocation = crate::allocation_audit::attributed(|| result.try_reserve_exact(capacity));
1827    #[cfg(not(feature = "allocation-audit"))]
1828    let allocation = result.try_reserve_exact(capacity);
1829    allocation.map_err(|_| {
1830        GraphBuildError::Store(StoreError::AllocationFailed {
1831            needed: requested as u64,
1832            component: "result",
1833        })
1834    })?;
1835    let frontier_capacity = vectors.node_count as usize;
1836    let mut frontier_slots = Vec::new();
1837
1838    let capacity = frontier_capacity;
1839    let requested = capacity
1840        .checked_mul(std::mem::size_of::<BestFirst>())
1841        .ok_or_else(|| GraphBuildError::Geometry("frontier allocation overflow".to_owned()))?;
1842    #[cfg(feature = "allocation-audit")]
1843    let allocation =
1844        crate::allocation_audit::attributed(|| frontier_slots.try_reserve_exact(capacity));
1845    #[cfg(not(feature = "allocation-audit"))]
1846    let allocation = frontier_slots.try_reserve_exact(capacity);
1847    allocation.map_err(|_| {
1848        GraphBuildError::Store(StoreError::AllocationFailed {
1849            needed: requested as u64,
1850            component: "frontier",
1851        })
1852    })?;
1853    let mut frontier = std::collections::BinaryHeap::from(frontier_slots);
1854    for &entry in entries {
1855        let entry_index = vectors.validate_node(entry)?;
1856        if entry == query_id
1857            || inserted.get(entry_index).copied() != Some(1)
1858            || visited.get(entry_index).copied() == Some(epoch)
1859        {
1860            continue;
1861        }
1862        if let Some(marker) = visited.get_mut(entry_index) {
1863            *marker = epoch;
1864        }
1865        let scored = score_node(vectors, &prepared, query_norm_squared, entry)?;
1866        insert_result(&mut result, scored, width)?;
1867        push_frontier(&mut frontier, scored)?;
1868    }
1869    if frontier.is_empty()
1870        && let Some(start) = inserted.iter().position(|present| *present == 1)
1871    {
1872        let start = u32::try_from(start)
1873            .map_err(|_| GraphBuildError::Geometry("inserted start exceeds u32".to_owned()))?;
1874        let scored = score_node(vectors, &prepared, query_norm_squared, start)?;
1875        if let Some(marker) = visited.get_mut(start as usize) {
1876            *marker = epoch;
1877        }
1878        insert_result(&mut result, scored, width)?;
1879        push_frontier(&mut frontier, scored)?;
1880    }
1881    while !frontier.is_empty() {
1882        let Some(BestFirst(candidate)) = frontier.pop() else {
1883            break;
1884        };
1885        let worst = result.last().map(|node| node.distance);
1886        if result.len() >= width && worst.is_some_and(|distance| candidate.distance > distance) {
1887            break;
1888        }
1889        for &neighbor in adjacency.neighbors(candidate.node_id)? {
1890            let neighbor_index = vectors.validate_node(neighbor)?;
1891            if neighbor == query_id
1892                || inserted.get(neighbor_index).copied() != Some(1)
1893                || visited.get(neighbor_index).copied() == Some(epoch)
1894            {
1895                continue;
1896            }
1897            if let Some(marker) = visited.get_mut(neighbor_index) {
1898                *marker = epoch;
1899            }
1900            let scored = score_node(vectors, &prepared, query_norm_squared, neighbor)?;
1901            let qualifies = result.len() < width
1902                || result
1903                    .last()
1904                    .is_some_and(|current_worst| scored.distance < current_worst.distance);
1905            if qualifies {
1906                insert_result(&mut result, scored, width)?;
1907                push_frontier(&mut frontier, scored)?;
1908            }
1909        }
1910    }
1911    drop(frontier);
1912    drop(prepared);
1913    Ok(result)
1914}
1915
1916fn score_node(
1917    vectors: &SegmentVectors<'_>,
1918    query: &crate::quant::Bit4Query,
1919    query_norm_squared: f64,
1920    node_id: u32,
1921) -> Result<ScoredNode, GraphBuildError> {
1922    vectors.validate_node(node_id)?;
1923    let factors = vectors.factor(node_id)?;
1924    let dot = f64::from(est_dot_bit4(query, vectors.code_row(node_id)?, factors)?);
1925    let norm = factors.norm();
1926    Ok(ScoredNode {
1927        node_id,
1928        distance: (query_norm_squared + norm * norm - 2.0 * dot).max(0.0),
1929    })
1930}
1931
1932/// Frontier entry whose `Ord` makes the std max-heap pop the best (nearest,
1933/// then lowest id) node first; `scored_best_first` is a strict total order,
1934/// so the pop sequence equals the former sort-worst-first-then-pop sequence.
1935#[derive(Clone, Copy)]
1936struct BestFirst(ScoredNode);
1937
1938impl PartialEq for BestFirst {
1939    fn eq(&self, other: &Self) -> bool {
1940        self.cmp(other).is_eq()
1941    }
1942}
1943
1944impl Eq for BestFirst {}
1945
1946impl PartialOrd for BestFirst {
1947    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1948        Some(self.cmp(other))
1949    }
1950}
1951
1952impl Ord for BestFirst {
1953    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1954        scored_worst_first(&self.0, &other.0)
1955    }
1956}
1957
1958fn push_frontier(
1959    frontier: &mut std::collections::BinaryHeap<BestFirst>,
1960    node: ScoredNode,
1961) -> Result<(), GraphBuildError> {
1962    if frontier.len() == frontier.capacity() {
1963        return Err(GraphBuildError::Geometry(
1964            "frontier fixed capacity exhausted".to_owned(),
1965        ));
1966    }
1967    frontier.push(BestFirst(node));
1968    Ok(())
1969}
1970
1971/// Inserts into the best-first sorted, width-bounded result list.
1972///
1973/// `scored_best_first` is a strict total order and a node is scored at most
1974/// once per search, so the binary-search insertion yields exactly the list a
1975/// full re-sort produced.
1976fn insert_result(
1977    result: &mut Vec<ScoredNode>,
1978    node: ScoredNode,
1979    width: usize,
1980) -> Result<(), GraphBuildError> {
1981    if result.len() == result.capacity() {
1982        return Err(GraphBuildError::Geometry(
1983            "result fixed capacity exhausted".to_owned(),
1984        ));
1985    }
1986    let position = result.partition_point(|existing| scored_best_first(existing, &node).is_lt());
1987    result.insert(position, node);
1988    if result.len() > width {
1989        let _ = result.pop();
1990    }
1991    Ok(())
1992}
1993
1994fn sort_scored(
1995    values: &mut [ScoredNode],
1996    compare: fn(&ScoredNode, &ScoredNode) -> std::cmp::Ordering,
1997) -> Result<(), GraphBuildError> {
1998    fn sift(
1999        values: &mut [ScoredNode],
2000        mut root: usize,
2001        end: usize,
2002        compare: fn(&ScoredNode, &ScoredNode) -> std::cmp::Ordering,
2003    ) -> Result<(), GraphBuildError> {
2004        loop {
2005            let child = root
2006                .checked_mul(2)
2007                .and_then(|value| value.checked_add(1))
2008                .ok_or_else(|| GraphBuildError::Geometry("sort offset overflow".to_owned()))?;
2009            if child >= end {
2010                return Ok(());
2011            }
2012            let mut selected = child;
2013            if child + 1 < end
2014                && compare(
2015                    values.get(child).ok_or_else(|| {
2016                        GraphBuildError::Geometry("sort child unavailable".to_owned())
2017                    })?,
2018                    values.get(child + 1).ok_or_else(|| {
2019                        GraphBuildError::Geometry("sort sibling unavailable".to_owned())
2020                    })?,
2021                )
2022                .is_lt()
2023            {
2024                selected = child + 1;
2025            }
2026            if !compare(
2027                values
2028                    .get(root)
2029                    .ok_or_else(|| GraphBuildError::Geometry("sort root unavailable".to_owned()))?,
2030                values.get(selected).ok_or_else(|| {
2031                    GraphBuildError::Geometry("sort selection unavailable".to_owned())
2032                })?,
2033            )
2034            .is_lt()
2035            {
2036                return Ok(());
2037            }
2038            values.swap(root, selected);
2039            root = selected;
2040        }
2041    }
2042    for root in (0..values.len() / 2).rev() {
2043        sift(values, root, values.len(), compare)?;
2044    }
2045    for end in (1..values.len()).rev() {
2046        values.swap(0, end);
2047        sift(values, 0, end, compare)?;
2048    }
2049    Ok(())
2050}
2051
2052fn scored_best_first(left: &ScoredNode, right: &ScoredNode) -> std::cmp::Ordering {
2053    left.distance
2054        .total_cmp(&right.distance)
2055        .then_with(|| left.node_id.cmp(&right.node_id))
2056}
2057
2058fn scored_worst_first(left: &ScoredNode, right: &ScoredNode) -> std::cmp::Ordering {
2059    scored_best_first(right, left)
2060}
2061
2062fn robust_prune(
2063    vectors: &SegmentVectors<'_>,
2064    owner: u32,
2065    candidates: &[u32],
2066    alpha: f32,
2067    target: usize,
2068) -> Result<Vec<u32>, GraphBuildError> {
2069    robust_prune_rows(
2070        vectors.rescore,
2071        vectors.dimensions,
2072        vectors.node_count,
2073        owner,
2074        candidates,
2075        alpha,
2076        target,
2077    )
2078}
2079#[allow(clippy::too_many_arguments)]
2080pub(crate) fn robust_prune_rows(
2081    rescore: &[f32],
2082    dimensions: usize,
2083    node_count: u32,
2084    owner: u32,
2085    candidates: &[u32],
2086    alpha: f32,
2087    target: usize,
2088) -> Result<Vec<u32>, GraphBuildError> {
2089    validate_exact_rows(rescore, dimensions, node_count)?;
2090    validate_exact_node(owner, node_count)?;
2091    let mut remaining = Vec::new();
2092
2093    let capacity = candidates.len();
2094    let requested = capacity
2095        .checked_mul(std::mem::size_of::<u32>())
2096        .ok_or_else(|| GraphBuildError::Geometry("prune allocation overflow".to_owned()))?;
2097    #[cfg(feature = "allocation-audit")]
2098    let allocation = crate::allocation_audit::attributed(|| remaining.try_reserve_exact(capacity));
2099    #[cfg(not(feature = "allocation-audit"))]
2100    let allocation = remaining.try_reserve_exact(capacity);
2101    allocation.map_err(|_| {
2102        GraphBuildError::Store(StoreError::AllocationFailed {
2103            needed: requested as u64,
2104            component: "prune",
2105        })
2106    })?;
2107    for &candidate in candidates {
2108        validate_exact_node(candidate, node_count)?;
2109        if candidate != owner && !remaining.contains(&candidate) {
2110            remaining.push(candidate);
2111        }
2112    }
2113    let mut owner_distances = Vec::new();
2114
2115    let capacity = remaining.len();
2116    let requested = capacity
2117        .checked_mul(std::mem::size_of::<ScoredNode>())
2118        .ok_or_else(|| GraphBuildError::Geometry("distance allocation overflow".to_owned()))?;
2119    #[cfg(feature = "allocation-audit")]
2120    let allocation =
2121        crate::allocation_audit::attributed(|| owner_distances.try_reserve_exact(capacity));
2122    #[cfg(not(feature = "allocation-audit"))]
2123    let allocation = owner_distances.try_reserve_exact(capacity);
2124    allocation.map_err(|_| {
2125        GraphBuildError::Store(StoreError::AllocationFailed {
2126            needed: requested as u64,
2127            component: "distance",
2128        })
2129    })?;
2130    for candidate in remaining {
2131        owner_distances.push(ScoredNode {
2132            node_id: candidate,
2133            distance: exact_row_distance(rescore, dimensions, owner, candidate)?,
2134        });
2135    }
2136
2137    sort_scored(&mut owner_distances, scored_best_first)?;
2138    let mut selected = Vec::new();
2139
2140    let capacity = target;
2141    let requested = capacity
2142        .checked_mul(std::mem::size_of::<u32>())
2143        .ok_or_else(|| GraphBuildError::Geometry("selected allocation overflow".to_owned()))?;
2144    #[cfg(feature = "allocation-audit")]
2145    let allocation = crate::allocation_audit::attributed(|| selected.try_reserve_exact(capacity));
2146    #[cfg(not(feature = "allocation-audit"))]
2147    let allocation = selected.try_reserve_exact(capacity);
2148    allocation.map_err(|_| {
2149        GraphBuildError::Store(StoreError::AllocationFailed {
2150            needed: requested as u64,
2151            component: "selected",
2152        })
2153    })?;
2154    let alpha_squared = f64::from(alpha) * f64::from(alpha);
2155    while selected.len() < target {
2156        let Some(best) = owner_distances.first().copied() else {
2157            break;
2158        };
2159        selected.push(best.node_id);
2160        let mut index = 0;
2161        while index < owner_distances.len() {
2162            let candidate = *owner_distances.get(index).ok_or_else(|| {
2163                GraphBuildError::Geometry("prune candidate unavailable".to_owned())
2164            })?;
2165            let keep = if candidate.node_id == best.node_id {
2166                false
2167            } else {
2168                let between =
2169                    exact_row_distance(rescore, dimensions, best.node_id, candidate.node_id)?;
2170                alpha_squared * between > candidate.distance
2171            };
2172            if keep {
2173                index += 1;
2174            } else {
2175                owner_distances.remove(index);
2176            }
2177        }
2178    }
2179    drop(owner_distances);
2180    Ok(selected)
2181}
2182
2183fn validate_exact_rows(
2184    rescore: &[f32],
2185    dimensions: usize,
2186    node_count: u32,
2187) -> Result<(), GraphBuildError> {
2188    if dimensions == 0 {
2189        return Err(GraphBuildError::Geometry(
2190            "alpha prune requires non-zero dimensions".to_owned(),
2191        ));
2192    }
2193    let expected = (node_count as usize)
2194        .checked_mul(dimensions)
2195        .ok_or_else(|| GraphBuildError::Geometry("exact row length overflow".to_owned()))?;
2196    if rescore.len() != expected {
2197        return Err(GraphBuildError::Geometry(format!(
2198            "exact rows contain {} values, expected {expected}",
2199            rescore.len()
2200        )));
2201    }
2202    Ok(())
2203}
2204
2205fn validate_exact_node(node_id: u32, node_count: u32) -> Result<(), GraphBuildError> {
2206    if node_id >= node_count {
2207        return Err(GraphBuildError::NodeIdOutOfRange {
2208            node_id,
2209            node_count,
2210        });
2211    }
2212    Ok(())
2213}
2214
2215fn exact_row_distance(
2216    rescore: &[f32],
2217    dimensions: usize,
2218    left: u32,
2219    right: u32,
2220) -> Result<f64, GraphBuildError> {
2221    let row = |node_id: u32| {
2222        let start = (node_id as usize)
2223            .checked_mul(dimensions)
2224            .ok_or_else(|| GraphBuildError::Geometry("exact row offset overflow".to_owned()))?;
2225        let end = start
2226            .checked_add(dimensions)
2227            .ok_or_else(|| GraphBuildError::Geometry("exact row end overflow".to_owned()))?;
2228        rescore
2229            .get(start..end)
2230            .ok_or_else(|| GraphBuildError::Geometry(format!("exact row {node_id} is unavailable")))
2231    };
2232    let mut sum = 0.0;
2233    for (left_chunk, right_chunk) in row(left)?.chunks(256).zip(row(right)?.chunks(256)) {
2234        for (left, right) in left_chunk.iter().zip(right_chunk) {
2235            let difference = f64::from(*left) - f64::from(*right);
2236            sum += difference * difference;
2237        }
2238    }
2239    Ok(sum)
2240}
2241
2242fn add_reciprocal_edges(
2243    vectors: &SegmentVectors<'_>,
2244    adjacency: &mut Adjacency,
2245    owner: u32,
2246    neighbors: &[u32],
2247    params: GraphParams,
2248    alpha: f32,
2249) -> Result<(), GraphBuildError> {
2250    for &neighbor in neighbors {
2251        vectors.validate_node(neighbor)?;
2252        let source = adjacency.neighbors(neighbor)?;
2253        let mut existing = Vec::new();
2254
2255        let capacity = source.len().saturating_add(1);
2256        let requested = capacity
2257            .checked_mul(std::mem::size_of::<u32>())
2258            .ok_or_else(|| {
2259                GraphBuildError::Geometry("reciprocal allocation overflow".to_owned())
2260            })?;
2261        #[cfg(feature = "allocation-audit")]
2262        let allocation =
2263            crate::allocation_audit::attributed(|| existing.try_reserve_exact(capacity));
2264        #[cfg(not(feature = "allocation-audit"))]
2265        let allocation = existing.try_reserve_exact(capacity);
2266        allocation.map_err(|_| {
2267            GraphBuildError::Store(StoreError::AllocationFailed {
2268                needed: requested as u64,
2269                component: "reciprocal",
2270            })
2271        })?;
2272        existing.extend_from_slice(source);
2273        if existing.contains(&owner) {
2274            drop(existing);
2275            continue;
2276        }
2277        let updated = if existing.len() < usize::from(params.r_max()) {
2278            let mut updated = existing;
2279            updated.push(owner);
2280            updated
2281        } else {
2282            let mut candidates = existing;
2283            candidates.push(owner);
2284            robust_prune(
2285                vectors,
2286                neighbor,
2287                &candidates,
2288                alpha,
2289                usize::from(params.r_target()),
2290            )?
2291        };
2292        adjacency.set_neighbors(neighbor, &updated)?;
2293        drop(updated);
2294    }
2295    Ok(())
2296}
2297
2298fn refined_entry_points(
2299    vectors: &SegmentVectors<'_>,
2300    shuffled: &[u32],
2301) -> Result<Vec<u32>, GraphBuildError> {
2302    let sample = shuffled
2303        .get(..shuffled.len().min(MEDOID_SAMPLE_ROWS))
2304        .ok_or_else(|| GraphBuildError::Geometry("medoid sample is unavailable".to_owned()))?;
2305    let mut medoid_scores = Vec::new();
2306
2307    let capacity = sample.len();
2308    let requested = capacity
2309        .checked_mul(std::mem::size_of::<ScoredNode>())
2310        .ok_or_else(|| GraphBuildError::Geometry("medoid allocation overflow".to_owned()))?;
2311    #[cfg(feature = "allocation-audit")]
2312    let allocation =
2313        crate::allocation_audit::attributed(|| medoid_scores.try_reserve_exact(capacity));
2314    #[cfg(not(feature = "allocation-audit"))]
2315    let allocation = medoid_scores.try_reserve_exact(capacity);
2316    allocation.map_err(|_| {
2317        GraphBuildError::Store(StoreError::AllocationFailed {
2318            needed: requested as u64,
2319            component: "medoid",
2320        })
2321    })?;
2322    for candidate in sample {
2323        let mut distance = 0.0;
2324        for other in sample {
2325            distance +=
2326                exact_row_distance(vectors.rescore, vectors.dimensions, *candidate, *other)?;
2327        }
2328        medoid_scores.push(ScoredNode {
2329            node_id: *candidate,
2330            distance,
2331        });
2332    }
2333    sort_scored(&mut medoid_scores, scored_best_first)?;
2334    let medoid = medoid_scores
2335        .first()
2336        .map(|node| node.node_id)
2337        .ok_or_else(|| GraphBuildError::Geometry("medoid sample contains no rows".to_owned()))?;
2338    drop(medoid_scores);
2339    let mut entries = Vec::new();
2340
2341    let capacity = ENTRY_POINT_COUNT;
2342    let requested = capacity
2343        .checked_mul(std::mem::size_of::<u32>())
2344        .ok_or_else(|| GraphBuildError::Geometry("entry allocation overflow".to_owned()))?;
2345    #[cfg(feature = "allocation-audit")]
2346    let allocation = crate::allocation_audit::attributed(|| entries.try_reserve_exact(capacity));
2347    #[cfg(not(feature = "allocation-audit"))]
2348    let allocation = entries.try_reserve_exact(capacity);
2349    allocation.map_err(|_| {
2350        GraphBuildError::Store(StoreError::AllocationFailed {
2351            needed: requested as u64,
2352            component: "entry",
2353        })
2354    })?;
2355    entries.push(medoid);
2356    while entries.len() < ENTRY_POINT_COUNT && entries.len() < sample.len() {
2357        let mut farthest = None::<ScoredNode>;
2358        for &candidate in sample {
2359            if entries.contains(&candidate) {
2360                continue;
2361            }
2362            let mut nearest = f64::INFINITY;
2363            for entry in &entries {
2364                nearest = nearest.min(exact_row_distance(
2365                    vectors.rescore,
2366                    vectors.dimensions,
2367                    candidate,
2368                    *entry,
2369                )?);
2370            }
2371            let scored = ScoredNode {
2372                node_id: candidate,
2373                distance: nearest,
2374            };
2375            if farthest.is_none_or(|current| {
2376                scored.distance > current.distance
2377                    || (scored.distance == current.distance && scored.node_id < current.node_id)
2378            }) {
2379                farthest = Some(scored);
2380            }
2381        }
2382        let Some(next) = farthest else {
2383            break;
2384        };
2385        entries.push(next.node_id);
2386    }
2387    Ok(entries)
2388}
2389
2390fn move_entries_to_front(order: &mut Vec<u32>, entries: &[u32]) -> Result<(), GraphBuildError> {
2391    let mut index = 0;
2392    while index < order.len() {
2393        let node = *order
2394            .get(index)
2395            .ok_or_else(|| GraphBuildError::Geometry("build order row unavailable".to_owned()))?;
2396        if entries.contains(&node) {
2397            order.remove(index);
2398        } else {
2399            index += 1;
2400        }
2401    }
2402    for &entry in entries.iter().rev() {
2403        order.insert(0, entry);
2404    }
2405    Ok(())
2406}
2407
2408fn encode_artifact(
2409    vectors: &SegmentVectors<'_>,
2410    params: GraphParams,
2411    entries: &[u32],
2412    adjacency: &Adjacency,
2413    mut memory: Option<AccountedCounter>,
2414) -> Result<GraphBuildArtifact, GraphBuildError> {
2415    let dimensions = u32::try_from(vectors.dimensions)
2416        .map_err(|_| GraphBuildError::Geometry("dimensions exceed u32".to_owned()))?;
2417    let padded_dimensions = dimensions
2418        .checked_add(127)
2419        .map(|value| value / 128 * 128)
2420        .ok_or_else(|| GraphBuildError::Geometry("padded dimensions overflow".to_owned()))?;
2421    let layout = GraphNodeLayout::new(dimensions, padded_dimensions, params.r_max())?;
2422    let padded_row_bytes = layout.code_bytes();
2423    let padded_length = (vectors.node_count as usize)
2424        .checked_mul(padded_row_bytes)
2425        .ok_or_else(|| GraphBuildError::Geometry("padded code bytes overflow".to_owned()))?;
2426    let encoded_length = (vectors.node_count as usize)
2427        .checked_mul(layout.stride() as usize)
2428        .and_then(|bytes| bytes.checked_add(NODE_BLOCK_TRAILER_LEN))
2429        .ok_or_else(|| GraphBuildError::Geometry("encoded graph bytes overflow".to_owned()))?;
2430    let node_inputs = (vectors.node_count as usize)
2431        .checked_mul(std::mem::size_of::<GraphNodeBlockInput<'_>>())
2432        .ok_or_else(|| GraphBuildError::Geometry("node input bytes overflow".to_owned()))?;
2433    let entry_bytes = entries
2434        .len()
2435        .checked_mul(std::mem::size_of::<u32>())
2436        .ok_or_else(|| GraphBuildError::Geometry("entry bytes overflow".to_owned()))?;
2437    if let Some(charge) = memory.as_mut() {
2438        let existing = usize::try_from(charge.bytes())
2439            .map_err(|_| GraphBuildError::Geometry("accounted bytes exceed usize".to_owned()))?;
2440        let peak = existing
2441            .checked_add(padded_length)
2442            .and_then(|bytes| bytes.checked_add(node_inputs))
2443            .and_then(|bytes| bytes.checked_add(encoded_length))
2444            .and_then(|bytes| bytes.checked_add(entry_bytes))
2445            .ok_or_else(|| GraphBuildError::Geometry("encode arena overflow".to_owned()))?;
2446        charge.set(peak)?;
2447    }
2448    let mut padded_codes = Vec::new();
2449
2450    let capacity = padded_length;
2451    let requested = capacity
2452        .checked_mul(std::mem::size_of::<u8>())
2453        .ok_or_else(|| GraphBuildError::Geometry("padded code allocation overflow".to_owned()))?;
2454    #[cfg(feature = "allocation-audit")]
2455    let allocation =
2456        crate::allocation_audit::attributed(|| padded_codes.try_reserve_exact(capacity));
2457    #[cfg(not(feature = "allocation-audit"))]
2458    let allocation = padded_codes.try_reserve_exact(capacity);
2459    allocation.map_err(|_| {
2460        GraphBuildError::Store(StoreError::AllocationFailed {
2461            needed: requested as u64,
2462            component: "padded code",
2463        })
2464    })?;
2465    padded_codes.resize(padded_length, 0);
2466    for node_id in 0..vectors.node_count {
2467        let source = vectors.code_row(node_id)?;
2468        let start = (node_id as usize)
2469            .checked_mul(padded_row_bytes)
2470            .ok_or_else(|| GraphBuildError::Geometry("padded code offset overflow".to_owned()))?;
2471        let end = start
2472            .checked_add(source.len())
2473            .ok_or_else(|| GraphBuildError::Geometry("padded code end overflow".to_owned()))?;
2474        let destination = padded_codes.get_mut(start..end).ok_or_else(|| {
2475            GraphBuildError::Geometry(format!("padded code row {node_id} is unavailable"))
2476        })?;
2477        for (output, input) in destination.chunks_mut(256).zip(source.chunks(256)) {
2478            output.copy_from_slice(input);
2479        }
2480    }
2481    let mut nodes = Vec::new();
2482
2483    let capacity = vectors.node_count as usize;
2484    let requested = capacity
2485        .checked_mul(std::mem::size_of::<GraphNodeBlockInput<'_>>())
2486        .ok_or_else(|| GraphBuildError::Geometry("node input allocation overflow".to_owned()))?;
2487    #[cfg(feature = "allocation-audit")]
2488    let allocation = crate::allocation_audit::attributed(|| nodes.try_reserve_exact(capacity));
2489    #[cfg(not(feature = "allocation-audit"))]
2490    let allocation = nodes.try_reserve_exact(capacity);
2491    allocation.map_err(|_| {
2492        GraphBuildError::Store(StoreError::AllocationFailed {
2493            needed: requested as u64,
2494            component: "node input",
2495        })
2496    })?;
2497    for node_id in 0..vectors.node_count {
2498        let start = (node_id as usize)
2499            .checked_mul(padded_row_bytes)
2500            .ok_or_else(|| GraphBuildError::Geometry("encoded code offset overflow".to_owned()))?;
2501        let end = start
2502            .checked_add(padded_row_bytes)
2503            .ok_or_else(|| GraphBuildError::Geometry("encoded code end overflow".to_owned()))?;
2504        let codes = padded_codes.get(start..end).ok_or_else(|| {
2505            GraphBuildError::Geometry(format!("encoded code row {node_id} is unavailable"))
2506        })?;
2507        nodes.push(GraphNodeBlockInput {
2508            codes,
2509            factors: vectors.factor(node_id)?,
2510            flags: u8::from(entries.contains(&node_id)),
2511            neighbors: adjacency.neighbors(node_id)?,
2512        });
2513    }
2514
2515    let encoded = encode_node_blocks_with_refinement_passes(
2516        GraphNodeBlockBuild {
2517            layout,
2518            nodes: &nodes,
2519        },
2520        crate::graph::refine::RefinementPasses::default(),
2521    )?;
2522    let encoded_region = encoded.into_bytes();
2523    let mut entry_points = Vec::new();
2524
2525    let capacity = entries.len();
2526    let requested = capacity
2527        .checked_mul(std::mem::size_of::<u32>())
2528        .ok_or_else(|| GraphBuildError::Geometry("entry copy allocation overflow".to_owned()))?;
2529    #[cfg(feature = "allocation-audit")]
2530    let allocation =
2531        crate::allocation_audit::attributed(|| entry_points.try_reserve_exact(capacity));
2532    #[cfg(not(feature = "allocation-audit"))]
2533    let allocation = entry_points.try_reserve_exact(capacity);
2534    allocation.map_err(|_| {
2535        GraphBuildError::Store(StoreError::AllocationFailed {
2536            needed: requested as u64,
2537            component: "entry copy",
2538        })
2539    })?;
2540    for chunk in entries.chunks(256) {
2541        entry_points.extend_from_slice(chunk);
2542    }
2543    drop(nodes);
2544    drop(padded_codes);
2545    if let Some(charge) = memory.as_mut() {
2546        let retained = encoded_region
2547            .capacity()
2548            .checked_add(
2549                entry_points
2550                    .capacity()
2551                    .checked_mul(std::mem::size_of::<u32>())
2552                    .ok_or_else(|| {
2553                        GraphBuildError::Geometry("retained entry bytes overflow".to_owned())
2554                    })?,
2555            )
2556            .ok_or_else(|| GraphBuildError::Geometry("retained graph bytes overflow".to_owned()))?;
2557        charge.set(retained)?;
2558    }
2559    Ok(GraphBuildArtifact {
2560        encoded_region,
2561        entry_points,
2562        work_rows_completed: 0,
2563        _memory: memory,
2564    })
2565}
2566
2567fn shuffle(values: &mut [u32], random: &mut SplitMix64) -> Result<(), GraphBuildError> {
2568    for upper in (1..values.len()).rev() {
2569        let modulus = u64::try_from(upper + 1)
2570            .map_err(|_| GraphBuildError::Geometry("shuffle width exceeds u64".to_owned()))?;
2571        let selected = usize::try_from(random.next_u64() % modulus)
2572            .map_err(|_| GraphBuildError::Geometry("shuffle index exceeds usize".to_owned()))?;
2573        values.swap(upper, selected);
2574    }
2575    Ok(())
2576}
2577
2578#[derive(Clone, Copy, Debug)]
2579struct SplitMix64 {
2580    state: u64,
2581}
2582
2583impl SplitMix64 {
2584    const fn new(seed: u64) -> Self {
2585        Self { state: seed }
2586    }
2587
2588    fn next_u64(&mut self) -> u64 {
2589        self.state = self.state.wrapping_add(0x9e37_79b9_7f4a_7c15);
2590        let mut value = self.state;
2591        value = (value ^ (value >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
2592        value = (value ^ (value >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
2593        value ^ (value >> 31)
2594    }
2595}
2596
2597#[cfg(test)]
2598#[allow(
2599    clippy::expect_used,
2600    clippy::indexing_slicing,
2601    clippy::panic,
2602    clippy::unwrap_used
2603)]
2604mod tests {
2605    use proptest::prelude::*;
2606    use proptest::test_runner::{Config, RngSeed, TestRunner};
2607    use rand::{Rng, RngCore};
2608    use xxhash_rust::xxh3::xxh3_64;
2609
2610    use crate::graph::block::decode_node_blocks;
2611    use crate::lifecycle::QueryCancellation;
2612    use crate::lifecycle::durability::{CommitTier, DurabilityMode, DurabilityPolicy};
2613    use crate::lifecycle::{
2614        CancelToken, ManualMonotonicClock, OpenOptions, QueryControl, Store, StoreTestDependencies,
2615    };
2616    use crate::meta::{AliveSet, ColumnStoreBuilder, Schema};
2617    use crate::quant::{Bit4Factors, quantize_bit4};
2618    use crate::segment::SegmentId;
2619    use crate::segment::reader::SegmentReader;
2620    use crate::segment::writer::{SegmentBuild, SegmentFactors, write_segment};
2621    use crate::vfs::{CountingVfs, StdVfs, Vfs};
2622    use std::sync::Arc;
2623
2624    use super::{
2625        Adjacency, CheckpointedGraphBuild, GraphBuildError, GraphBuildPasses, GraphBuildSession,
2626        SegmentVectors, build_graph, build_graph_checkpointed, decode_checkpoint,
2627        validate_graph_build_checkpoint,
2628    };
2629    use crate::graph::GraphParams;
2630
2631    fn fixture_reader(directory: &std::path::Path, rows: usize) -> SegmentReader {
2632        let dimensions = 128_usize;
2633        let mut random = crate::test_support::seeded_rng(
2634            "graph::build::vamana_build_is_deterministic_under_seed::fixture",
2635        );
2636        let mut rescore = Vec::with_capacity(rows * dimensions);
2637        let mut codes = Vec::with_capacity(rows * dimensions.div_ceil(2));
2638        let mut factors = Vec::<Bit4Factors>::with_capacity(rows);
2639        for _ in 0..rows {
2640            let row = (0..dimensions)
2641                .map(|_| random.random_range(-2.0_f32..2.0_f32))
2642                .collect::<Vec<_>>();
2643            let start = codes.len();
2644            codes.resize(start + dimensions.div_ceil(2), 0);
2645            factors.push(quantize_bit4(&row, &mut codes[start..]).expect("fixture row quantizes"));
2646            rescore.extend(row);
2647        }
2648        let mut columns = ColumnStoreBuilder::new(Schema::new(Vec::new()).expect("empty schema"));
2649        for row in 0..rows {
2650            columns
2651                .push_row(row as i64, &[])
2652                .expect("timestamp-only fixture row");
2653        }
2654        let columns = columns.finish().expect("fixture columns");
2655        let alive = AliveSet::new(rows as u32);
2656        let id = SegmentId::new(19, [7; 10]);
2657        let policy = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
2658            .expect("derived policy");
2659        write_segment(
2660            &StdVfs,
2661            directory,
2662            SegmentBuild {
2663                id,
2664                scheme: 4,
2665                dims: dimensions as u32,
2666                codes: &codes,
2667                factors: SegmentFactors::Bit4(&factors),
2668                rescore: &rescore,
2669                columns: &columns,
2670                alive: &alive,
2671            },
2672            policy,
2673        )
2674        .expect("sealed Bit4 fixture");
2675        SegmentReader::open(&StdVfs, &directory.join(id.file_name()), id).expect("fixture reader")
2676    }
2677
2678    #[test]
2679    fn graph_checkpoint_io_goes_through_the_store_vfs() {
2680        let segment_directory = tempfile::tempdir().expect("checkpoint segment directory");
2681        let reader = fixture_reader(segment_directory.path(), 4);
2682        let store_directory = tempfile::tempdir().expect("checkpoint store directory");
2683        let checkpoint = store_directory.path().join("graph-build.checkpoint");
2684        let vfs = Arc::new(CountingVfs::new(StdVfs).with_read_path_filter(checkpoint.clone()));
2685        let store = Store::open_with_test_dependencies(
2686            store_directory.path(),
2687            OpenOptions::default(),
2688            StoreTestDependencies::new(vfs.clone(), Arc::new(ManualMonotonicClock::new())),
2689        )
2690        .expect("checkpoint store");
2691        let lease = store.snapshot().expect("checkpoint snapshot");
2692        let params = GraphParams::new(2, 3, 1.0, 1.0, 3, 1).expect("checkpoint params");
2693        let control = QueryControl::Cancel(CancelToken::new());
2694
2695        vfs.reset();
2696        let interrupted = build_graph_checkpointed(
2697            &store,
2698            &reader,
2699            CheckpointedGraphBuild::new(params, 0x06, GraphBuildPasses::One, &checkpoint, &control)
2700                .with_max_work_rows(1),
2701            &lease,
2702        );
2703        assert!(matches!(
2704            interrupted,
2705            Err(GraphBuildError::BudgetExhausted { rows_completed: 1 })
2706        ));
2707        build_graph_checkpointed(
2708            &store,
2709            &reader,
2710            CheckpointedGraphBuild::new(params, 0x06, GraphBuildPasses::One, &checkpoint, &control),
2711            &lease,
2712        )
2713        .expect("resumed graph build");
2714
2715        assert!(vfs.read_calls() >= 1, "checkpoint read bypassed Vfs");
2716        assert_eq!(
2717            vfs.filtered_read_calls(),
2718            1,
2719            "graph-build.checkpoint read bypassed Vfs"
2720        );
2721        assert!(vfs.write_calls() >= 1, "checkpoint write bypassed Vfs");
2722        assert!(vfs.rename_calls() >= 1, "checkpoint rename bypassed Vfs");
2723        assert!(vfs.delete_calls() >= 1, "checkpoint delete bypassed Vfs");
2724        drop(lease);
2725        store.close().expect("close checkpoint store");
2726    }
2727
2728    #[test]
2729    fn graph_checkpoint_publish_syncs_the_temp_file_and_its_directory() {
2730        let segment_directory = tempfile::tempdir().expect("checkpoint segment directory");
2731        let reader = fixture_reader(segment_directory.path(), 4);
2732        let store_directory = tempfile::tempdir().expect("checkpoint store directory");
2733        let checkpoint = store_directory.path().join("graph-build.checkpoint");
2734        let vfs = Arc::new(CountingVfs::new(StdVfs));
2735        let store = Store::open_with_test_dependencies(
2736            store_directory.path(),
2737            OpenOptions::default().with_durability(DurabilityMode::Durable, CommitTier::Durable),
2738            StoreTestDependencies::new(vfs.clone(), Arc::new(ManualMonotonicClock::new())),
2739        )
2740        .expect("durable checkpoint store");
2741        let lease = store.snapshot().expect("checkpoint snapshot");
2742        let params = GraphParams::new(2, 3, 1.0, 1.0, 3, 1).expect("checkpoint params");
2743        let control = QueryControl::Cancel(CancelToken::new());
2744
2745        vfs.reset();
2746        let interrupted = build_graph_checkpointed(
2747            &store,
2748            &reader,
2749            CheckpointedGraphBuild::new(params, 0x03, GraphBuildPasses::One, &checkpoint, &control)
2750                .with_max_work_rows(1),
2751            &lease,
2752        );
2753        assert!(matches!(
2754            interrupted,
2755            Err(GraphBuildError::BudgetExhausted { rows_completed: 1 })
2756        ));
2757        assert!(
2758            vfs.full_sync_calls() >= 2,
2759            "checkpoint publish skipped syncs"
2760        );
2761        assert!(vfs.rename_calls() >= 1, "checkpoint publish skipped rename");
2762    }
2763
2764    fn checkpoint_with_mutation(
2765        mut bytes: Vec<u8>,
2766        mutation: impl FnOnce(&mut Vec<u8>),
2767    ) -> Vec<u8> {
2768        mutation(&mut bytes);
2769        let checksum_start = bytes.len() - super::CHECKPOINT_CHECKSUM_BYTES;
2770        let checksum = xxh3_64(&bytes[..checksum_start]);
2771        bytes[checksum_start..].copy_from_slice(&checksum.to_le_bytes());
2772        bytes
2773    }
2774
2775    #[test]
2776    fn graph_build_errors_preserve_actionable_values_and_sources() {
2777        let io_error = GraphBuildError::CheckpointIo {
2778            path: std::path::PathBuf::from("blocked.checkpoint"),
2779            source: std::io::Error::new(std::io::ErrorKind::PermissionDenied, "blocked"),
2780        };
2781        assert!(io_error.to_string().contains("blocked.checkpoint"));
2782        assert!(std::error::Error::source(&io_error).is_some());
2783
2784        let errors = [
2785            GraphBuildError::CheckpointCorrupt("bad header".to_owned()),
2786            GraphBuildError::BudgetExhausted { rows_completed: 16 },
2787            GraphBuildError::Cancelled { partial: false },
2788            GraphBuildError::Timeout { partial: false },
2789            GraphBuildError::ReadCancelled { partial: false },
2790            GraphBuildError::Geometry("bad shape".to_owned()),
2791            GraphBuildError::NodeIdOutOfRange {
2792                node_id: 9,
2793                node_count: 3,
2794            },
2795        ];
2796        for error in errors {
2797            assert!(!error.to_string().is_empty());
2798            assert!(std::error::Error::source(&error).is_none());
2799        }
2800
2801        let sourced_errors = [
2802            GraphBuildError::from(crate::segment::SegmentError::Geometry(
2803                "bad segment".to_owned(),
2804            )),
2805            GraphBuildError::from(crate::quant::QuantError::EmptyVector),
2806            GraphBuildError::from(crate::graph::block::GraphNodeError::InvalidHeader(
2807                "bad node header".to_owned(),
2808            )),
2809            GraphBuildError::from(crate::lifecycle::StoreError::NotDirectory {
2810                path: std::path::PathBuf::from("not-a-store"),
2811            }),
2812        ];
2813        for error in sourced_errors {
2814            assert!(!error.to_string().is_empty());
2815            assert!(std::error::Error::source(&error).is_some());
2816        }
2817    }
2818
2819    #[test]
2820    fn built_graph_publishes_through_the_frozen_m2_writer() {
2821        let directory = tempfile::tempdir().expect("publication fixture directory");
2822        let reader = fixture_reader(directory.path(), 32);
2823        let params = GraphParams::new(8, 12, 1.0, 1.2, 24, 32).expect("publication params");
2824        let artifact = build_graph(
2825            &reader,
2826            params,
2827            0x0001_9000_3a71_fac7,
2828            GraphBuildPasses::Two,
2829        )
2830        .expect("publication graph");
2831        let output_id = SegmentId::new(19, [8; 10]);
2832        let policy = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
2833            .expect("publication policy");
2834        let meta = artifact
2835            .write_segment_with_graph(&StdVfs, directory.path(), &reader, output_id, policy)
2836            .expect("published graph segment");
2837        assert_eq!(meta.id, output_id);
2838        let published = SegmentReader::open(
2839            &StdVfs,
2840            &directory.path().join(output_id.file_name()),
2841            output_id,
2842        )
2843        .expect("published reader");
2844        let graph = published
2845            .graph_node_blocks()
2846            .expect("published graph region");
2847        assert_eq!(graph.node_count(), 32);
2848        assert_eq!(graph.layout().max_degree(), 12);
2849    }
2850
2851    #[test]
2852    fn checked_adjacency_rejects_invalid_node_and_degree_inputs() {
2853        let directory = tempfile::tempdir().expect("checked vector directory");
2854        let reader = fixture_reader(directory.path(), 3);
2855        let vectors = SegmentVectors::new(&reader).expect("checked vectors");
2856        assert!(matches!(
2857            vectors.validate_node(3),
2858            Err(GraphBuildError::NodeIdOutOfRange { .. })
2859        ));
2860
2861        let mut adjacency = Adjacency::new(3, 2).expect("small adjacency");
2862        assert!(matches!(
2863            adjacency.neighbors(3),
2864            Err(GraphBuildError::NodeIdOutOfRange { .. })
2865        ));
2866        assert!(matches!(
2867            adjacency.set_neighbors(0, &[1, 2, 1]),
2868            Err(GraphBuildError::Geometry(_))
2869        ));
2870        assert!(matches!(
2871            adjacency.set_neighbors(0, &[3]),
2872            Err(GraphBuildError::NodeIdOutOfRange { .. })
2873        ));
2874        adjacency
2875            .set_neighbors(0, &[1, 2])
2876            .expect("valid adjacency row");
2877        assert_eq!(adjacency.neighbors(0).expect("active row"), &[1, 2]);
2878
2879        let mut self_edge = Adjacency::new(3, 2).expect("self-edge adjacency");
2880        self_edge.degrees[0] = 1;
2881        self_edge.slots[0] = 0;
2882        assert!(matches!(
2883            super::validate_checkpoint_adjacency(&self_edge),
2884            Err(GraphBuildError::CheckpointCorrupt(_))
2885        ));
2886
2887        let mut inactive_value = Adjacency::new(3, 2).expect("inactive-slot adjacency");
2888        inactive_value.slots[0] = 1;
2889        assert!(matches!(
2890            super::validate_checkpoint_adjacency(&inactive_value),
2891            Err(GraphBuildError::CheckpointCorrupt(_))
2892        ));
2893        assert!(matches!(
2894            super::decode_phase(3),
2895            Ok(super::BuildPhase::Complete)
2896        ));
2897        assert!(matches!(
2898            super::read_checkpoint_u8(&[], 0, "truncated"),
2899            Err(GraphBuildError::CheckpointCorrupt(_))
2900        ));
2901    }
2902
2903    #[test]
2904    fn checkpoint_parser_rejects_semantic_corruption_and_identity_drift() {
2905        let directory = tempfile::tempdir().expect("checkpoint parser directory");
2906        let reader = fixture_reader(directory.path(), 16);
2907        let checkpoint = directory.path().join("semantic.checkpoint");
2908        let params = GraphParams::new(4, 6, 1.0, 1.2, 8, 4).expect("checkpoint params");
2909        let store_directory = tempfile::tempdir().expect("checkpoint store directory");
2910        let store = Store::open(store_directory.path(), OpenOptions::default()).expect("store");
2911        let lease = store.snapshot().expect("checkpoint lease");
2912        let control = QueryControl::Cancel(CancelToken::new());
2913        let cancellation = QueryCancellation::new(&control, &lease);
2914        let policy = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
2915            .expect("derived policy");
2916        let mut session = GraphBuildSession::new(
2917            &StdVfs,
2918            &reader,
2919            params,
2920            0x0001_9000_3c0d_ec01,
2921            GraphBuildPasses::Two,
2922            &checkpoint,
2923            policy,
2924            None,
2925        )
2926        .expect("checkpoint session");
2927        assert!(
2928            !session
2929                .advance_batch(&cancellation)
2930                .expect("checkpoint batch")
2931        );
2932        let valid = StdVfs.read(&checkpoint).expect("valid checkpoint bytes");
2933        validate_graph_build_checkpoint(&valid).expect("valid checkpoint parses");
2934
2935        let mut bad_checksum = valid.clone();
2936        bad_checksum[40] ^= 1;
2937        let corruptions = [
2938            bad_checksum,
2939            checkpoint_with_mutation(valid.clone(), |bytes| {
2940                bytes[8..10].copy_from_slice(&2_u16.to_le_bytes());
2941            }),
2942            checkpoint_with_mutation(valid.clone(), |bytes| bytes[10] = 9),
2943            checkpoint_with_mutation(valid.clone(), |bytes| bytes[11] = 9),
2944            checkpoint_with_mutation(valid.clone(), |bytes| bytes[14] = 1),
2945            checkpoint_with_mutation(valid.clone(), |bytes| bytes[12] = 0),
2946            checkpoint_with_mutation(valid.clone(), |bytes| bytes[20..24].fill(0)),
2947            checkpoint_with_mutation(valid.clone(), |bytes| {
2948                bytes[32..36].copy_from_slice(&17_u32.to_le_bytes());
2949            }),
2950            checkpoint_with_mutation(valid.clone(), |bytes| {
2951                bytes[48..52].copy_from_slice(&f32::NAN.to_bits().to_le_bytes());
2952            }),
2953            checkpoint_with_mutation(valid.clone(), |bytes| bytes[72..80].fill(0)),
2954            checkpoint_with_mutation(valid.clone(), |bytes| {
2955                bytes[80] = 1;
2956                bytes[96..100].copy_from_slice(&0_u32.to_le_bytes());
2957            }),
2958            checkpoint_with_mutation(valid.clone(), |bytes| {
2959                bytes[80] = 0;
2960                bytes[96..100].copy_from_slice(&1_u32.to_le_bytes());
2961            }),
2962        ];
2963        for corrupt in corruptions {
2964            assert!(matches!(
2965                validate_graph_build_checkpoint(&corrupt),
2966                Err(GraphBuildError::CheckpointCorrupt(_))
2967            ));
2968        }
2969
2970        let vectors = SegmentVectors::new(&reader).expect("checkpoint vectors");
2971        assert!(matches!(
2972            decode_checkpoint(
2973                &valid,
2974                &vectors,
2975                params,
2976                0x0001_9000_3c0d_ec02,
2977                GraphBuildPasses::Two,
2978                None,
2979            ),
2980            Err(GraphBuildError::CheckpointCorrupt(_))
2981        ));
2982        assert!(matches!(
2983            decode_checkpoint(
2984                &valid,
2985                &vectors,
2986                params,
2987                0x0001_9000_3c0d_ec01,
2988                GraphBuildPasses::One,
2989                None,
2990            ),
2991            Err(GraphBuildError::CheckpointCorrupt(_))
2992        ));
2993        let one_pass_refine = checkpoint_with_mutation(valid, |bytes| {
2994            bytes[10] = 2;
2995            bytes[11] = 1;
2996        });
2997        assert!(matches!(
2998            decode_checkpoint(
2999                &one_pass_refine,
3000                &vectors,
3001                params,
3002                0x0001_9000_3c0d_ec01,
3003                GraphBuildPasses::One,
3004                None,
3005            ),
3006            Err(GraphBuildError::CheckpointCorrupt(_))
3007        ));
3008    }
3009
3010    #[test]
3011    fn corrupt_checkpoint_refuses_once_clears_the_file_and_the_next_session_starts_fresh() {
3012        let directory = tempfile::tempdir().expect("corrupt checkpoint directory");
3013        let reader = fixture_reader(directory.path(), 16);
3014        let checkpoint = directory.path().join("corrupt.checkpoint");
3015        let temporary = super::checkpoint_temp_path(&checkpoint);
3016        let params = GraphParams::new(4, 6, 1.0, 1.2, 8, 4).expect("checkpoint params");
3017        let store_directory = tempfile::tempdir().expect("checkpoint store directory");
3018        let store = Store::open(store_directory.path(), OpenOptions::default()).expect("store");
3019        let lease = store.snapshot().expect("checkpoint lease");
3020        let control = QueryControl::Cancel(CancelToken::new());
3021        let cancellation = QueryCancellation::new(&control, &lease);
3022        let mut session = GraphBuildSession::new(
3023            &StdVfs,
3024            &reader,
3025            params,
3026            0x0001_9000_c077_0001,
3027            GraphBuildPasses::Two,
3028            &checkpoint,
3029            DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
3030                .expect("derived policy"),
3031            None,
3032        )
3033        .expect("checkpoint session");
3034        assert!(
3035            !session
3036                .advance_batch(&cancellation)
3037                .expect("checkpoint batch")
3038        );
3039        drop(session);
3040
3041        let mut corrupt = StdVfs.read(&checkpoint).expect("valid checkpoint bytes");
3042        corrupt[40] ^= 1;
3043        StdVfs
3044            .write(&checkpoint, &corrupt)
3045            .expect("write corrupt checkpoint");
3046        StdVfs
3047            .write(&temporary, b"stale temporary checkpoint")
3048            .expect("write stale temporary checkpoint");
3049
3050        assert!(matches!(
3051            GraphBuildSession::new(
3052                &StdVfs,
3053                &reader,
3054                params,
3055                0x0001_9000_c077_0001,
3056                GraphBuildPasses::Two,
3057                &checkpoint,
3058                DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
3059                    .expect("derived policy"),
3060                None,
3061            ),
3062            Err(GraphBuildError::CheckpointCorrupt(_))
3063        ));
3064        assert_eq!(
3065            StdVfs
3066                .read(&checkpoint)
3067                .expect_err("corrupt checkpoint was retained")
3068                .kind(),
3069            std::io::ErrorKind::NotFound
3070        );
3071        assert_eq!(
3072            StdVfs
3073                .read(&temporary)
3074                .expect_err("stale checkpoint temporary was retained")
3075                .kind(),
3076            std::io::ErrorKind::NotFound
3077        );
3078
3079        let fresh = GraphBuildSession::new(
3080            &StdVfs,
3081            &reader,
3082            params,
3083            0x0001_9000_c077_0001,
3084            GraphBuildPasses::Two,
3085            &checkpoint,
3086            DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
3087                .expect("derived policy"),
3088            None,
3089        )
3090        .expect("next session starts fresh");
3091        assert_eq!(fresh.completed_work_rows().expect("fresh work rows"), 0);
3092    }
3093
3094    #[test]
3095    fn accounting_rejection_on_resume_preserves_the_valid_checkpoint() {
3096        let directory = tempfile::tempdir().expect("accounting checkpoint directory");
3097        let reader = fixture_reader(directory.path(), 16);
3098        let checkpoint = directory.path().join("accounting.checkpoint");
3099        let params = GraphParams::new(4, 6, 1.0, 1.2, 8, 4).expect("checkpoint params");
3100        let store_directory = tempfile::tempdir().expect("checkpoint store directory");
3101        let store = Store::open(store_directory.path(), OpenOptions::default()).expect("store");
3102        let lease = store.snapshot().expect("checkpoint lease");
3103        let control = QueryControl::Cancel(CancelToken::new());
3104        let cancellation = QueryCancellation::new(&control, &lease);
3105        let mut session = GraphBuildSession::new(
3106            &StdVfs,
3107            &reader,
3108            params,
3109            0x0001_9000_acc0_0001,
3110            GraphBuildPasses::Two,
3111            &checkpoint,
3112            DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
3113                .expect("derived policy"),
3114            None,
3115        )
3116        .expect("checkpoint session");
3117        assert!(
3118            !session
3119                .advance_batch(&cancellation)
3120                .expect("checkpoint batch")
3121        );
3122        drop(session);
3123        let valid = StdVfs.read(&checkpoint).expect("valid checkpoint bytes");
3124
3125        let refused_directory = tempfile::tempdir().expect("refused store directory");
3126        let refused = Store::open(
3127            refused_directory.path(),
3128            OpenOptions::default().with_max_temp_bytes(1),
3129        )
3130        .expect("refused store");
3131        assert!(matches!(
3132            GraphBuildSession::new(
3133                &StdVfs,
3134                &reader,
3135                params,
3136                0x0001_9000_acc0_0001,
3137                GraphBuildPasses::Two,
3138                &checkpoint,
3139                DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
3140                    .expect("derived policy"),
3141                Some(&refused.accounting),
3142            ),
3143            Err(GraphBuildError::Store(_))
3144        ));
3145        assert_eq!(
3146            StdVfs
3147                .read(&checkpoint)
3148                .expect("accounting rejection removed the valid checkpoint"),
3149            valid
3150        );
3151    }
3152
3153    #[test]
3154    fn incomplete_session_and_checkpoint_io_fail_typed() {
3155        let directory = tempfile::tempdir().expect("typed failure directory");
3156        let reader = fixture_reader(directory.path(), 16);
3157        let params = GraphParams::new(4, 6, 1.0, 1.2, 8, 4).expect("typed failure params");
3158        let control = QueryControl::Cancel(CancelToken::new());
3159        let store_directory = tempfile::tempdir().expect("typed failure store");
3160        let store = Store::open(store_directory.path(), OpenOptions::default()).expect("store");
3161        let lease = store.snapshot().expect("typed failure lease");
3162        let cancellation = QueryCancellation::new(&control, &lease);
3163        let policy = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
3164            .expect("derived policy");
3165
3166        let unfinished = GraphBuildSession::new(
3167            &StdVfs,
3168            &reader,
3169            params,
3170            0x0001_9000_3100_0001,
3171            GraphBuildPasses::Two,
3172            &directory.path().join("unfinished.checkpoint"),
3173            policy,
3174            None,
3175        )
3176        .expect("unfinished session");
3177        assert!(matches!(
3178            unfinished.artifact(),
3179            Err(GraphBuildError::Geometry(_))
3180        ));
3181
3182        assert!(matches!(
3183            GraphBuildSession::new(
3184                &StdVfs,
3185                &reader,
3186                params,
3187                0x0001_9000_3100_0001,
3188                GraphBuildPasses::Two,
3189                directory.path(),
3190                policy,
3191                None,
3192            ),
3193            Err(GraphBuildError::CheckpointIo { .. })
3194        ));
3195
3196        let missing_parent = directory.path().join("missing").join("write.checkpoint");
3197        let mut unwritable = GraphBuildSession::new(
3198            &StdVfs,
3199            &reader,
3200            params,
3201            0x0001_9000_3100_0001,
3202            GraphBuildPasses::Two,
3203            &missing_parent,
3204            policy,
3205            None,
3206        )
3207        .expect("unwritable session starts before checkpoint write");
3208        assert!(matches!(
3209            unwritable.advance_batch(&cancellation),
3210            Err(GraphBuildError::CheckpointIo { .. })
3211        ));
3212
3213        let timeout = QueryControl::Cancel(CancelToken::new());
3214        timeout.mark_timed_out();
3215        let timed_out = QueryCancellation::new(&timeout, &lease);
3216        assert!(matches!(
3217            super::check_cancellation(&timed_out),
3218            Err(GraphBuildError::Timeout { partial: false })
3219        ));
3220    }
3221
3222    #[test]
3223    fn vamana_build_is_deterministic_under_seed() {
3224        let directory = tempfile::tempdir().expect("fixture directory");
3225        let reader = fixture_reader(directory.path(), 48);
3226        let params = GraphParams::new(8, 12, 1.0, 1.2, 24, 48).expect("test params");
3227        let mut random = crate::test_support::seeded_rng(
3228            "graph::build::vamana_build_is_deterministic_under_seed",
3229        );
3230        let seed = random.next_u64();
3231        let first = build_graph(&reader, params, seed, GraphBuildPasses::Two)
3232            .expect("first deterministic build");
3233        let second = build_graph(&reader, params, seed, GraphBuildPasses::Two)
3234            .expect("second deterministic build");
3235        assert_eq!(first.encoded_region(), second.encoded_region());
3236
3237        // Preserve the original sequential f64 sum.
3238        // At 2^54 each following squared unit rounds away individually.
3239        let dimensions = 513;
3240        let mut rescore = vec![1.0_f32; dimensions * 2];
3241        rescore[0] = 134_217_728.0;
3242        rescore[dimensions..].fill(0.0);
3243        let code_stride = dimensions.div_ceil(2);
3244        let mut codes = vec![0_u8; code_stride * 2];
3245        let mut factors = Vec::new();
3246        for (row, output) in rescore
3247            .chunks_exact(dimensions)
3248            .zip(codes.chunks_exact_mut(code_stride))
3249        {
3250            factors.push(quantize_bit4(row, output).expect("513D arithmetic fixture quantizes"));
3251        }
3252        let vectors = SegmentVectors::from_native(dimensions, &codes, &factors, &rescore)
3253            .expect("513D arithmetic fixture geometry");
3254        let exact =
3255            super::exact_row_distance(&rescore, dimensions, 0, 1).expect("exact row distance");
3256        let adjacency = Adjacency::new(2, 1).expect("two-row arithmetic adjacency");
3257        let candidates = super::search_candidates(
3258            &vectors,
3259            &adjacency,
3260            &[0, 1],
3261            0,
3262            &[1],
3263            1,
3264            seed,
3265            &mut [0; 2],
3266            &mut 0,
3267        )
3268        .expect("candidate search");
3269        assert_eq!(candidates.len(), 1);
3270        assert_eq!(candidates[0].node_id, 1);
3271        assert_eq!(
3272            (exact.to_bits(), candidates[0].distance.to_bits()),
3273            (0x4350_0000_0000_0000, 0x4350_0000_0000_0000),
3274            "exact distance and candidate query norm retain sequential arithmetic",
3275        );
3276        #[cfg(feature = "allocation-audit")]
3277        {
3278            let mut allocation_observations = Vec::new();
3279            for ordinal in [1, 2] {
3280                let mut visited = [0; 2];
3281                let mut visit_epoch = 0;
3282                let (result, fires) =
3283                    crate::allocation_audit::fail_attributed_allocation(ordinal, || {
3284                        super::search_candidates(
3285                            &vectors,
3286                            &adjacency,
3287                            &[0, 1],
3288                            0,
3289                            &[1],
3290                            1,
3291                            seed,
3292                            &mut visited,
3293                            &mut visit_epoch,
3294                        )
3295                    });
3296                allocation_observations.push((
3297                    fires,
3298                    matches!(
3299                        result,
3300                        Err(GraphBuildError::Store(
3301                            crate::lifecycle::StoreError::AllocationFailed { needed: 513, .. }
3302                        ))
3303                    ),
3304                ));
3305            }
3306            assert_eq!(
3307                allocation_observations,
3308                [(1, true), (1, true)],
3309                "both actual query-code allocation failures retain their cause"
3310            );
3311            let restored = super::search_candidates(
3312                &vectors,
3313                &adjacency,
3314                &[0, 1],
3315                0,
3316                &[1],
3317                1,
3318                seed,
3319                &mut [0; 2],
3320                &mut 0,
3321            )
3322            .expect("restored query allocation control");
3323            assert_eq!(restored.len(), 1);
3324            assert_eq!(restored[0].node_id, 1);
3325            assert_eq!(restored[0].distance.to_bits(), 0x4350_0000_0000_0000);
3326        }
3327    }
3328
3329    #[test]
3330    fn cancelled_build_leaves_no_partial_artifact() {
3331        let directory = tempfile::tempdir().expect("sealed segment directory");
3332        let reader = fixture_reader(directory.path(), 48);
3333        let segment_path = directory.path().join(reader.meta().id.file_name());
3334        let before = StdVfs
3335            .read(&segment_path)
3336            .expect("sealed bytes before build");
3337        let checkpoint = directory.path().join("graph-build.checkpoint");
3338        let checkpoint_temp = directory.path().join("graph-build.checkpoint.tmp");
3339        let params = GraphParams::new(8, 12, 1.0, 1.2, 24, 8).expect("test params");
3340        let store_directory = tempfile::tempdir().expect("store directory");
3341        let store = Store::open(store_directory.path(), OpenOptions::default()).expect("store");
3342        let lease = store.snapshot().expect("snapshot lease");
3343        let token = CancelToken::new();
3344        let control = QueryControl::Cancel(token.clone());
3345        let cancellation = QueryCancellation::new(&control, &lease);
3346        let policy = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
3347            .expect("derived policy");
3348        let mut session = GraphBuildSession::new(
3349            &StdVfs,
3350            &reader,
3351            params,
3352            0x19_0003,
3353            GraphBuildPasses::Two,
3354            &checkpoint,
3355            policy,
3356            None,
3357        )
3358        .expect("build session");
3359
3360        assert!(!session.advance_batch(&cancellation).expect("first batch"));
3361        token.cancel();
3362        assert!(matches!(
3363            session.advance_batch(&cancellation),
3364            Err(GraphBuildError::Cancelled { partial: false })
3365        ));
3366        assert_eq!(
3367            StdVfs
3368                .read(&segment_path)
3369                .expect("sealed bytes after cancel"),
3370            before
3371        );
3372        assert!(
3373            checkpoint.exists(),
3374            "completed batch checkpoint must remain resumable"
3375        );
3376        assert!(
3377            !checkpoint_temp.exists(),
3378            "checkpoint temp must not survive"
3379        );
3380    }
3381
3382    #[test]
3383    fn build_resumes_from_checkpoint_to_the_same_graph() {
3384        let directory = tempfile::tempdir().expect("sealed segment directory");
3385        let reader = fixture_reader(directory.path(), 64);
3386        let checkpoint = directory.path().join("resume.checkpoint");
3387        let params = GraphParams::new(8, 12, 1.0, 1.2, 24, 8).expect("test params");
3388        let store_directory = tempfile::tempdir().expect("store directory");
3389        let store = Store::open(store_directory.path(), OpenOptions::default()).expect("store");
3390        let lease = store.snapshot().expect("snapshot lease");
3391        let control = QueryControl::Cancel(CancelToken::new());
3392        let cancellation = QueryCancellation::new(&control, &lease);
3393        let policy = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
3394            .expect("derived policy");
3395        let mut interrupted = GraphBuildSession::new(
3396            &StdVfs,
3397            &reader,
3398            params,
3399            0x19_0003_00c0_ffee,
3400            GraphBuildPasses::Two,
3401            &checkpoint,
3402            policy,
3403            None,
3404        )
3405        .expect("interrupted session");
3406        assert!(
3407            !interrupted
3408                .advance_batch(&cancellation)
3409                .expect("first batch")
3410        );
3411        drop(interrupted);
3412
3413        let mut resumed = GraphBuildSession::new(
3414            &StdVfs,
3415            &reader,
3416            params,
3417            0x19_0003_00c0_ffee,
3418            GraphBuildPasses::Two,
3419            &checkpoint,
3420            policy,
3421            None,
3422        )
3423        .expect("resumed session");
3424        while !resumed.advance_batch(&cancellation).expect("resumed batch") {}
3425        let resumed = resumed.artifact().expect("resumed artifact");
3426        let uninterrupted =
3427            build_graph(&reader, params, 0x19_0003_00c0_ffee, GraphBuildPasses::Two)
3428                .expect("uninterrupted artifact");
3429        assert_eq!(resumed.encoded_region(), uninterrupted.encoded_region());
3430        assert!(!checkpoint.exists(), "completed build removes checkpoint");
3431    }
3432
3433    #[test]
3434    fn checkpointed_build_reports_exact_work_for_each_declared_pass() {
3435        let directory = tempfile::tempdir().expect("work-count fixture directory");
3436        let rows = 24_usize;
3437        let reader = fixture_reader(directory.path(), rows);
3438        let params = GraphParams::new(6, 10, 1.0, 1.2, 16, 6).expect("work-count params");
3439        let store_directory = tempfile::tempdir().expect("work-count store directory");
3440        let store = Store::open(store_directory.path(), OpenOptions::default()).expect("store");
3441        let lease = store.snapshot().expect("work-count lease");
3442        let control = QueryControl::Cancel(CancelToken::new());
3443
3444        for (passes, expected_rows, suffix) in [
3445            (GraphBuildPasses::One, rows as u64, "one"),
3446            (GraphBuildPasses::Two, (rows * 2) as u64, "two"),
3447        ] {
3448            let checkpoint = directory
3449                .path()
3450                .join(format!("work-count-{suffix}.checkpoint"));
3451            let artifact = build_graph_checkpointed(
3452                &store,
3453                &reader,
3454                CheckpointedGraphBuild::new(params, 0x19_0003_c0a7, passes, &checkpoint, &control),
3455                &lease,
3456            )
3457            .expect("checkpointed build completes");
3458
3459            assert_eq!(artifact.work_rows_completed(), expected_rows);
3460            assert_eq!(
3461                decode_node_blocks(artifact.encoded_region())
3462                    .expect("work-count graph decodes")
3463                    .node_count(),
3464                rows as u32
3465            );
3466            assert!(
3467                !checkpoint.exists(),
3468                "completed build removes {suffix} checkpoint"
3469            );
3470        }
3471    }
3472
3473    #[test]
3474    fn graph_build_handles_the_minimum_shape_and_rejects_an_empty_segment() {
3475        let directory = tempfile::tempdir().expect("minimum-shape directory");
3476        let empty = fixture_reader(directory.path(), 0);
3477        assert!(matches!(
3478            SegmentVectors::new(&empty),
3479            Err(GraphBuildError::Geometry(detail)) if detail.contains("at least one row")
3480        ));
3481
3482        let one_row_directory = tempfile::tempdir().expect("one-row directory");
3483        let one_row = fixture_reader(one_row_directory.path(), 1);
3484        let params = GraphParams::new(1, 1, 1.0, 1.0, 1, 1).expect("one-row params");
3485        let artifact = build_graph(&one_row, params, 0x19_0003_0000_0001, GraphBuildPasses::One)
3486            .expect("one-row build succeeds");
3487        let graph = decode_node_blocks(artifact.encoded_region()).expect("one-row graph decodes");
3488        let node = graph.block(0).expect("row zero exists");
3489        assert_eq!(graph.node_count(), 1);
3490        assert_eq!(node.degree(), 0);
3491        assert_eq!(node.flags() & 1, 1);
3492        assert_eq!(artifact.entry_points(), &[0]);
3493    }
3494
3495    #[test]
3496    fn every_node_has_at_most_r_max_neighbours() {
3497        let mut seeded = crate::test_support::seeded_rng(
3498            "graph::build::every_node_has_at_most_r_max_neighbours",
3499        );
3500        let mut runner = TestRunner::new(Config {
3501            cases: 24,
3502            rng_seed: RngSeed::Fixed(seeded.next_u64()),
3503            ..Config::default()
3504        });
3505        let strategy = (6_usize..40, 2_u8..=8, any::<u64>());
3506        let result = runner.run(&strategy, |(rows, r_target, seed)| {
3507            let directory = tempfile::tempdir().expect("property directory");
3508            let reader = fixture_reader(directory.path(), rows);
3509            let r_max = r_target.saturating_add(4);
3510            let params = GraphParams::new(
3511                r_target,
3512                r_max,
3513                1.0,
3514                1.2,
3515                u16::from(r_max).saturating_add(8),
3516                rows as u32,
3517            )
3518            .expect("property params");
3519            let artifact =
3520                build_graph(&reader, params, seed, GraphBuildPasses::Two).expect("property graph");
3521            let graph = decode_node_blocks(artifact.encoded_region()).expect("decoded graph");
3522            prop_assert_eq!(graph.layout().max_degree(), r_max);
3523            for node_id in 0..graph.node_count() {
3524                let block = graph.block(node_id).expect("property node");
3525                prop_assert!(block.degree() <= r_max);
3526            }
3527            Ok(())
3528        });
3529        assert!(result.is_ok(), "property result: {result:?}");
3530    }
3531
3532    #[test]
3533    fn no_node_lists_itself_or_a_duplicate() {
3534        let mut seeded =
3535            crate::test_support::seeded_rng("graph::build::no_node_lists_itself_or_a_duplicate");
3536        let mut runner = TestRunner::new(Config {
3537            cases: 24,
3538            rng_seed: RngSeed::Fixed(seeded.next_u64()),
3539            ..Config::default()
3540        });
3541        let strategy = (6_usize..40, 2_u8..=8, any::<u64>());
3542        let result = runner.run(&strategy, |(rows, r_target, seed)| {
3543            let directory = tempfile::tempdir().expect("property directory");
3544            let reader = fixture_reader(directory.path(), rows);
3545            let r_max = r_target.saturating_add(4);
3546            let params = GraphParams::new(
3547                r_target,
3548                r_max,
3549                1.0,
3550                1.2,
3551                u16::from(r_max).saturating_add(8),
3552                rows as u32,
3553            )
3554            .expect("property params");
3555            let artifact =
3556                build_graph(&reader, params, seed, GraphBuildPasses::Two).expect("property graph");
3557            let graph = decode_node_blocks(artifact.encoded_region()).expect("decoded graph");
3558            for node_id in 0..graph.node_count() {
3559                let block = graph.block(node_id).expect("property node");
3560                let mut unique = std::collections::BTreeSet::new();
3561                for neighbor in block.neighbors_padded().take(usize::from(block.degree())) {
3562                    prop_assert_ne!(neighbor, node_id);
3563                    prop_assert!(unique.insert(neighbor));
3564                }
3565            }
3566            Ok(())
3567        });
3568        assert!(result.is_ok(), "property result: {result:?}");
3569    }
3570
3571    #[test]
3572    fn graph_is_connected_from_the_entry_points() {
3573        let directory = tempfile::tempdir().expect("connectivity directory");
3574        let reader = fixture_reader(directory.path(), 96);
3575        let params = GraphParams::new(8, 12, 1.0, 1.2, 24, 96).expect("connectivity params");
3576        let artifact = build_graph(
3577            &reader,
3578            params,
3579            0x0001_9000_3c01_1ec7,
3580            GraphBuildPasses::Two,
3581        )
3582        .expect("connectivity graph");
3583        let graph = decode_node_blocks(artifact.encoded_region()).expect("decoded graph");
3584        let mut reached = vec![false; graph.node_count() as usize];
3585        let mut pending = std::collections::VecDeque::new();
3586        for &entry in artifact.entry_points() {
3587            pending.push_back(entry);
3588        }
3589        while let Some(node_id) = pending.pop_front() {
3590            let node = node_id as usize;
3591            if reached[node] {
3592                continue;
3593            }
3594            reached[node] = true;
3595            let block = graph.block(node_id).expect("reachable node");
3596            for neighbor in block.neighbors_padded().take(usize::from(block.degree())) {
3597                if !reached[neighbor as usize] {
3598                    pending.push_back(neighbor);
3599                }
3600            }
3601        }
3602        let reached_count = reached.iter().filter(|seen| **seen).count();
3603        assert_eq!(reached_count, graph.node_count() as usize);
3604    }
3605
3606    #[test]
3607    fn graph_build_memory_is_budgeted_and_exactly_accounted() {
3608        let directory = tempfile::tempdir().expect("accounting fixture directory");
3609        let reader = fixture_reader(directory.path(), 48);
3610        let params = GraphParams::new(8, 12, 1.0, 1.2, 24, 48).expect("accounting params");
3611        let control = QueryControl::Cancel(CancelToken::new());
3612
3613        let refused_directory = tempfile::tempdir().expect("refused store directory");
3614        let refused = Store::open(
3615            refused_directory.path(),
3616            OpenOptions::default().with_max_temp_bytes(1),
3617        )
3618        .expect("refused store");
3619        let refused_lease = refused.snapshot().expect("refused lease");
3620        let refused_result = build_graph_checkpointed(
3621            &refused,
3622            &reader,
3623            CheckpointedGraphBuild::new(
3624                params,
3625                0x19_0003_acc0,
3626                GraphBuildPasses::One,
3627                &directory.path().join("refused.checkpoint"),
3628                &control,
3629            ),
3630            &refused_lease,
3631        );
3632        assert!(matches!(refused_result, Err(GraphBuildError::Store(_))));
3633        assert_eq!(refused.stats().expect("refused stats").temporary_bytes, 0);
3634
3635        let store_directory = tempfile::tempdir().expect("accounted store directory");
3636        let store = Store::open(store_directory.path(), OpenOptions::default()).expect("store");
3637        let lease = store.snapshot().expect("accounted lease");
3638        let artifact = build_graph_checkpointed(
3639            &store,
3640            &reader,
3641            CheckpointedGraphBuild::new(
3642                params,
3643                0x19_0003_acc0,
3644                GraphBuildPasses::One,
3645                &directory.path().join("accounted.checkpoint"),
3646                &control,
3647            ),
3648            &lease,
3649        )
3650        .expect("accounted graph");
3651        let expected =
3652            artifact.encoded_region().len() + std::mem::size_of_val(artifact.entry_points());
3653        assert_eq!(
3654            store.stats().expect("live graph stats").temporary_bytes,
3655            expected as u64
3656        );
3657        drop(artifact);
3658        assert_eq!(
3659            store.stats().expect("released graph stats").temporary_bytes,
3660            0
3661        );
3662    }
3663
3664    #[test]
3665    fn arbitrary_checkpoint_bytes_fail_typed_without_panicking() {
3666        let mut seeded = crate::test_support::seeded_rng(
3667            "graph::build::arbitrary_checkpoint_bytes_fail_typed_without_panicking",
3668        );
3669        let mut runner = TestRunner::new(Config {
3670            cases: 64,
3671            rng_seed: RngSeed::Fixed(seeded.next_u64()),
3672            ..Config::default()
3673        });
3674        let result = runner.run(&proptest::collection::vec(any::<u8>(), 0..2048), |bytes| {
3675            prop_assert!(matches!(
3676                validate_graph_build_checkpoint(&bytes),
3677                Err(GraphBuildError::CheckpointCorrupt(_))
3678            ));
3679            Ok(())
3680        });
3681        assert!(result.is_ok(), "property result: {result:?}");
3682    }
3683}