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