diff --git a/.claude/board/entries/2026-11-02-codegraph-bin-reader-timing-gate.md b/.claude/board/entries/2026-11-02-codegraph-bin-reader-timing-gate.md
new file mode 100644
index 000000000..39082804d
--- /dev/null
+++ b/.claude/board/entries/2026-11-02-codegraph-bin-reader-timing-gate.md
@@ -0,0 +1,17 @@
+# D-CODEGRAPH-0-BIN reader: unchanged refresh 2.58 µs; changed-crate intake 2.5 ms fails the 1 ms gate (2026-11-02)
+
+**Status:** MEASURED (release, `crates/lance-graph-codegraph/examples/bin_timing.rs`, mask-risc store of 17,155 rows written by `ruff_charon import`, N = 41 fixed in advance, 4 CPUs, load ≈ 0.3). Plan [`d-codegraph-0-binary-lanes-v1.md`](../plans/d-codegraph-0-binary-lanes-v1.md) §5.
+
+| measurement | median | gate |
+|---|---|---|
+| unchanged `refresh_crate` (`rows.bin`, 64-byte header read) | 2.58 µs (range 2.53–14.92) | ≤ 20 µs: met |
+| changed-crate intake from `rows.bin` | 2.54–2.84 ms | ≤ 1 ms: **NOT met** |
+| — `fs::read` / `CrateFile::parse` / `read_bin_as` | 0.16 / 0.35 / 2.04 ms | |
+| same crate from `rows.tsv` (baseline) | 5.08–7.41 ms | |
+
+- The file is 556 KB as `rows.bin` against 3.05 MB as `rows.tsv`.
+- **Where the gate fails:** `read_bin_as` spends about 2 ms in `Symbols::intern` (a hash plus an allocation per new name). This is the interner replacement the plan already assigns to chunk B3: `SYM_HASH` → global id, label locators into retained buffers, no intern at open or refresh, falsifiers F4 and F5.
+- Verification (`parse`) is 0.35 ms. After B3 it is the next item to look at.
+- Chunks B1 and B2 are in lance-graph PR #1929, with the gate reported as open. Merging them before B3 is the operator's decision.
+
+OPEN: B3 (interner) and the re-run of this gate; Quack counting (F7) behind the codec9-column gap.
diff --git a/.claude/board/entries/README.md b/.claude/board/entries/README.md
index 70c0197d3..a11d3bcf1 100644
--- a/.claude/board/entries/README.md
+++ b/.claude/board/entries/README.md
@@ -25,7 +25,7 @@ index row, (3) no duplicate entry id. Checks 1 and 2 are deliberately
opposite directions; the stranding this convention prevents shows up in
exactly one of them, never both.
-551 entries, 2026-08-06 .. 2026-11-02.
+552 entries, 2026-08-06 .. 2026-11-02.
| date | entry id | finding | file |
|---|---|---|---|
@@ -36,6 +36,7 @@ exactly one of them, never both.
| 2026-11-02 | `D-CODEGRAPH-0` | | [2026-11-02-d-codegraph-0-gap-map.md](2026-11-02-d-codegraph-0-gap-map.md) |
| 2026-11-02 | `codegraph-review-operator-decisions` | | [2026-11-02-codegraph-review-operator-decisions.md](2026-11-02-codegraph-review-operator-decisions.md) |
| 2026-11-02 | `codegraph-dr3-dt64-ruff-architecture-review` | | [2026-11-02-codegraph-dr3-dt64-ruff-architecture-review.md](2026-11-02-codegraph-dr3-dt64-ruff-architecture-review.md) |
+| 2026-11-02 | `D-CODEGRAPH-0-BIN` | | [2026-11-02-codegraph-bin-reader-timing-gate.md](2026-11-02-codegraph-bin-reader-timing-gate.md) |
| 2026-11-01 | `D-XLOG-NATIVE-7` | | [2026-11-01-xlog-resident-hypercube-context.md](2026-11-01-xlog-resident-hypercube-context.md) |
| 2026-11-01 | `D-XLOG-NATIVE-8` | | [2026-11-01-xlog-behaviour-lens-f100.md](2026-11-01-xlog-behaviour-lens-f100.md) |
| 2026-11-01 | `D-DT64-7-5` | | [2026-11-01-maptable-versioned-local-map.md](2026-11-01-maptable-versioned-local-map.md) |
diff --git a/crates/lance-graph-codegraph/examples/bin_timing.rs b/crates/lance-graph-codegraph/examples/bin_timing.rs
new file mode 100644
index 000000000..5d5917957
--- /dev/null
+++ b/crates/lance-graph-codegraph/examples/bin_timing.rs
@@ -0,0 +1,100 @@
+//! Timing gate for the binary rows format (plan `d-codegraph-0-binary-lanes-v1`
+//! §5): an unchanged `refresh_crate` against a `rows.bin` store, and the intake
+//! of one whole crate from `rows.bin` (read, verify, fill lanes) beside the
+//! same crate read from `rows.tsv`. N runs each, pre-registered at 41; median
+//! and range in microseconds. Run in release.
+//!
+//! Usage: `bin_timing
` where `` holds `.rows.bin`
+//! and `.rows.tsv` (as `ruff_charon import` writes them).
+
+use std::hint::black_box;
+use std::path::PathBuf;
+use std::time::Instant;
+
+use lance_graph_codegraph::artifact::{self, Lanes, Symbols};
+use lance_graph_codegraph::artifact_bin::CrateFile;
+use lance_graph_codegraph::Codegraph;
+
+const N: usize = 41;
+
+fn stats(name: &str, mut ns: Vec) {
+ ns.sort_unstable();
+ let us = |v: u128| v as f64 / 1000.0;
+ println!(
+ "{name}: median {:.2} us, min {:.2} us, max {:.2} us (N={})",
+ us(ns[ns.len() / 2]),
+ us(ns[0]),
+ us(ns[ns.len() - 1]),
+ ns.len()
+ );
+}
+
+fn main() {
+ let mut args = std::env::args().skip(1);
+ let dir = PathBuf::from(args.next().expect("usage: bin_timing "));
+ let krate = args.next().expect("usage: bin_timing ");
+ let bin = dir.join(format!("{krate}.rows.bin"));
+ let tsv = dir.join(format!("{krate}.rows.tsv"));
+
+ let mut g = Codegraph::open(&dir).expect("store opens");
+ let rows = {
+ let file = CrateFile::parse(std::fs::read(&bin).expect("bin")).expect("parse");
+ file.n_rows()
+ };
+ println!("crate {krate}: {rows} rows");
+
+ let mut unchanged = Vec::with_capacity(N);
+ for _ in 0..N {
+ let t = Instant::now();
+ let s = g.refresh_crate(&dir, &krate).expect("refresh");
+ unchanged.push(t.elapsed().as_nanos());
+ black_box(s);
+ }
+ stats("refresh_crate (unchanged, rows.bin)", unchanged);
+
+ let mut bin_intake = Vec::with_capacity(N);
+ for _ in 0..N {
+ let t = Instant::now();
+ let file = CrateFile::parse(std::fs::read(&bin).expect("bin")).expect("parse");
+ let mut syms = Symbols::default();
+ let mut lanes = Lanes::default();
+ artifact::read_bin_as(&file, &krate, &mut syms, &mut lanes).expect("lanes");
+ bin_intake.push(t.elapsed().as_nanos());
+ black_box(lanes.len());
+ }
+ stats(
+ "changed-crate intake (rows.bin: read+verify+lanes)",
+ bin_intake,
+ );
+
+ let (mut r, mut v, mut l) = (Vec::new(), Vec::new(), Vec::new());
+ for _ in 0..N {
+ let t = Instant::now();
+ let buf = std::fs::read(&bin).expect("bin");
+ r.push(t.elapsed().as_nanos());
+ let t = Instant::now();
+ let file = CrateFile::parse(buf).expect("parse");
+ v.push(t.elapsed().as_nanos());
+ let t = Instant::now();
+ let mut syms = Symbols::default();
+ let mut lanes = Lanes::default();
+ artifact::read_bin_as(&file, &krate, &mut syms, &mut lanes).expect("lanes");
+ l.push(t.elapsed().as_nanos());
+ black_box(lanes.len());
+ }
+ stats(" of which fs::read", r);
+ stats(" of which CrateFile::parse (verify)", v);
+ stats(" of which read_bin_as (intern + lanes)", l);
+
+ let mut tsv_intake = Vec::with_capacity(N);
+ for _ in 0..N {
+ let t = Instant::now();
+ let text = std::fs::read_to_string(&tsv).expect("tsv");
+ let mut syms = Symbols::default();
+ let mut lanes = Lanes::default();
+ artifact::read_text_as(&text, &krate, &mut syms, &mut lanes).expect("lanes");
+ tsv_intake.push(t.elapsed().as_nanos());
+ black_box(lanes.len());
+ }
+ stats("baseline intake (rows.tsv: read+parse+lanes)", tsv_intake);
+}
diff --git a/crates/lance-graph-codegraph/src/artifact.rs b/crates/lance-graph-codegraph/src/artifact.rs
index 31fa3f882..ce5c3604b 100644
--- a/crates/lance-graph-codegraph/src/artifact.rs
+++ b/crates/lance-graph-codegraph/src/artifact.rs
@@ -11,11 +11,12 @@
//! Columns are located by name from the `#schema` line, so a file with
//! permuted columns reads identically.
-use std::collections::HashMap;
+use std::collections::{BTreeMap, HashMap};
use std::fs;
use std::hash::{BuildHasherDefault, Hasher};
-use std::path::Path;
+use std::path::{Path, PathBuf};
+use crate::artifact_bin::{self, BinRefusal, CrateFile};
use crate::graph::IntakeRefusal;
/// The only artifact version this reader accepts.
@@ -97,7 +98,9 @@ pub fn pred_code(s: &str) -> u8 {
pub fn digest(bytes: &[u8]) -> u64 {
let mut h = FastHasher(0xcbf2_9ce4_8422_2325);
h.write(bytes);
- h.write_u64(bytes.len() as u64);
+ // Little-endian, never `write_u64`: its default folds native-endian
+ // bytes, and the ruff writer (`ruff_charon::store_bin::digest`) folds LE.
+ h.write(&(bytes.len() as u64).to_le_bytes());
h.finish()
}
@@ -427,6 +430,30 @@ impl Lanes {
const SUFFIX: &str = ".rows.tsv";
+/// File-name suffix of the binary artifact, preferred over [`SUFFIX`] when both
+/// exist for a crate.
+pub const BIN_SUFFIX: &str = ".rows.bin";
+
+/// Which file a crate is read from.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum CrateSource {
+ /// `.rows.bin`.
+ Bin(PathBuf),
+ /// `.rows.tsv`.
+ Tsv(PathBuf),
+}
+
+/// The file `` is read from in `dir`: `.rows.bin` when it
+/// exists, otherwise `.rows.tsv` when that exists, otherwise `None`.
+pub fn crate_source(dir: &Path, krate: &str) -> Option {
+ let bin = dir.join(format!("{krate}{BIN_SUFFIX}"));
+ if bin.is_file() {
+ return Some(CrateSource::Bin(bin));
+ }
+ let tsv = dir.join(format!("{krate}{SUFFIX}"));
+ tsv.is_file().then_some(CrateSource::Tsv(tsv))
+}
+
/// The text of the single file `.rows.tsv` of `dir`.
pub fn read_crate_text(dir: &Path, krate: &str) -> Result {
fs::read_to_string(dir.join(format!("{krate}{SUFFIX}"))).map_err(IntakeRefusal::Io)
@@ -440,34 +467,138 @@ pub fn read_crate(dir: &Path, krate: &str, symbols: &mut Symbols) -> Result Result {
- let mut paths = Vec::new();
+ // stem -> whether a binary file exists for it
+ let mut stems: BTreeMap = BTreeMap::new();
for entry in fs::read_dir(dir).map_err(IntakeRefusal::Io)? {
- let path = entry.map_err(IntakeRefusal::Io)?.path();
- let is_rows = path
- .file_name()
- .and_then(|n| n.to_str())
- .is_some_and(|n| n.ends_with(SUFFIX));
- if is_rows {
- paths.push(path);
+ let name = entry.map_err(IntakeRefusal::Io)?.file_name();
+ let Some(name) = name.to_str() else {
+ continue;
+ };
+ if let Some(stem) = name.strip_suffix(BIN_SUFFIX) {
+ stems.insert(stem.to_owned(), true);
+ } else if let Some(stem) = name.strip_suffix(SUFFIX) {
+ stems.entry(stem.to_owned()).or_insert(false);
}
}
- paths.sort();
let mut lanes = Lanes::default();
- for p in &paths {
- let text = fs::read_to_string(p).map_err(IntakeRefusal::Io)?;
- let stem = p
- .file_name()
- .and_then(|n| n.to_str())
- .and_then(|n| n.strip_suffix(SUFFIX))
- .unwrap_or("");
- read_text_as(&text, stem, symbols, &mut lanes)?;
+ for (stem, has_bin) in &stems {
+ if *has_bin {
+ let buf =
+ fs::read(dir.join(format!("{stem}{BIN_SUFFIX}"))).map_err(IntakeRefusal::Io)?;
+ let file = CrateFile::parse(buf).map_err(IntakeRefusal::Bin)?;
+ read_bin_as(&file, stem, symbols, &mut lanes)?;
+ } else {
+ let text = fs::read_to_string(dir.join(format!("{stem}{SUFFIX}")))
+ .map_err(IntakeRefusal::Io)?;
+ read_text_as(&text, stem, symbols, &mut lanes)?;
+ }
}
Ok(lanes)
}
+/// A symbol id of a verified file, translated into the interner's id space.
+fn translate(table: &[u32], id: u32, tag: u32, row: usize) -> Result {
+ usize::try_from(id)
+ .ok()
+ .and_then(|i| table.get(i))
+ .copied()
+ .ok_or(IntakeRefusal::Bin(BinRefusal::IdRange { tag, row }))
+}
+
+/// Append one verified binary file's rows to `lanes`, attributing each to
+/// `krate`, and record the file's header digest under `krate`.
+///
+/// The file's symbol table is interned once, in table order; rows then carry
+/// translated ids, so no text is parsed and each distinct symbol, grade, file
+/// and predicate label is interned once per file. Every lane [`read_text_as`]
+/// fills is filled here in the same row order.
+pub fn read_bin_as(
+ file: &CrateFile,
+ krate: &str,
+ sym: &mut impl Interner,
+ lanes: &mut Lanes,
+) -> Result<(), IntakeRefusal> {
+ let kid = sym.intern(krate);
+ let mut table: Vec = Vec::with_capacity(file.n_symbols());
+ for i in 0..file.header().n_symbols {
+ table.push(sym.intern(file.symbol(i)));
+ }
+ // predicate codeword -> (P_* code, interned label id for P_OTHER)
+ let mut preds: HashMap = HashMap::new();
+ let mut grades: HashMap<&str, u32> = HashMap::new();
+ let mut files: HashMap<&str, u32> = HashMap::new();
+ let mut dash: Option = None;
+ for row in 0..file.n_rows() {
+ let (code, label_id) = match preds.get(&file.predicate_id(row)) {
+ Some(&hit) => hit,
+ None => {
+ let label = file.predicate(row);
+ let code = pred_code(label);
+ let id = if code == P_OTHER {
+ sym.intern(label)
+ } else {
+ NO_ID
+ };
+ preds.insert(file.predicate_id(row), (code, id));
+ (code, id)
+ }
+ };
+ let subject = translate(&table, file.subject(row), artifact_bin::tag::SUBJ, row)?;
+ let object = translate(&table, file.object(row), artifact_bin::tag::OBJ, row)?;
+ let may = match file.may_target(row) {
+ Some(t) => translate(&table, t, artifact_bin::tag::MAYT, row)?,
+ None => NO_ID,
+ };
+ let grade = file.grade(row);
+ let grade_id = match grades.get(grade) {
+ Some(&id) => id,
+ None => {
+ let id = sym.intern(grade);
+ grades.insert(grade, id);
+ id
+ }
+ };
+ let file_id = match file.file(row) {
+ None => match dash {
+ Some(id) => id,
+ None => {
+ let id = sym.intern("-");
+ dash = Some(id);
+ id
+ }
+ },
+ Some(label) => match files.get(label) {
+ Some(&id) => id,
+ None => {
+ let id = sym.intern(label);
+ files.insert(label, id);
+ id
+ }
+ },
+ };
+ let (start, end) = file.span(row).unwrap_or((NO_SPAN, NO_SPAN));
+ // Every fallible step is done: from here the row is pushed whole.
+ if code == P_OTHER {
+ lanes.other_pred.push((lanes.pred.len() as u32, label_id));
+ }
+ lanes.pred.push(code);
+ lanes.subject.push(subject);
+ lanes.object.push(object);
+ lanes.grade.push(grade_id);
+ lanes.file.push(file_id);
+ lanes.start.push(start);
+ lanes.end.push(end);
+ lanes.krate.push(kid);
+ lanes.may_target.push(may);
+ }
+ lanes.set_digest(kid, file.header().digest);
+ Ok(())
+}
+
fn number(field: &str) -> Option {
if field == "-" {
Some(NO_SPAN)
diff --git a/crates/lance-graph-codegraph/src/artifact_bin.rs b/crates/lance-graph-codegraph/src/artifact_bin.rs
new file mode 100644
index 000000000..4ffea827c
--- /dev/null
+++ b/crates/lance-graph-codegraph/src/artifact_bin.rs
@@ -0,0 +1,968 @@
+//! The zero-copy reader and verifier for `.rows.bin` (chunk B1 of
+//! `.claude/plans/d-codegraph-0-binary-lanes-v1.md`).
+//!
+//! The byte layout is owned by the writer, `ruff_charon::store_bin`; this
+//! module is a separate implementation that accepts exactly what that writer
+//! writes and refuses exactly what its `read` refuses, in the same check
+//! order. There is no dependency on ruff.
+//!
+//! A [`CrateFile`] owns ONE `Vec`. [`CrateFile::parse`] verifies the whole
+//! file once (header, length, digest, directory, shapes, every id against its
+//! table, every codeword, UTF-8) and records the byte range of each lane;
+//! every accessor afterwards is a lens over that buffer: it reads
+//! little-endian integers in place and returns borrowed `&str`s. No `String`
+//! is built per row.
+//!
+//! **Validation invariant.** Every accessor assumes `parse` succeeded. After
+//! that, the ranges it records are inside the buffer and have the lane
+//! lengths the header promises, so the accessors use plain indexing (a row or
+//! symbol id out of range is a caller bug and panics). The only slice
+//! operations that can fail at parse time use `get` and return a refusal.
+//! `&str` accessors re-check UTF-8 over the one label they return (the bytes
+//! were validated at parse, so this cannot fail; the fallback is `""`).
+//! There is no `unsafe` and no native-endian cast.
+
+use std::collections::BTreeMap;
+use std::fmt;
+use std::ops::Range;
+
+use crate::artifact::digest;
+
+/// Length of the fixed header.
+pub const HEADER_LEN: usize = 64;
+/// An absent id, span, owner, dispatch target or function declaration.
+pub const NO_ID: u32 = u32::MAX;
+
+const MAGIC: [u8; 8] = *b"CHRNROWS";
+const MAJOR: u16 = 1;
+const MINOR: u16 = 0;
+const FLAG_PARTIAL: u32 = 1;
+const CODEC9_MAX: usize = 511;
+const KIND_MAX: usize = 3;
+const DIR_ENTRY_LEN: usize = 16;
+const PROV_FIELDS: usize = 7;
+
+const ENC_RAW: u16 = 0;
+const ENC_CODEC9: u16 = 1;
+const ENC_PRES: u16 = 2;
+const ENC_KV: u16 = 3;
+
+/// Section tags. Bit 31 means must-understand; every tag here has it set.
+pub mod tag {
+ /// Predicate magnitude lane.
+ pub const PRED_MAG: u32 = 0x8000_0001;
+ /// Predicate presence plane.
+ pub const PRED_PRES: u32 = 0x8000_0002;
+ /// Predicate codebook.
+ pub const PRED_CB: u32 = 0x8000_0003;
+ /// Grade magnitude lane.
+ pub const GRADE_MAG: u32 = 0x8000_0004;
+ /// Grade presence plane.
+ pub const GRADE_PRES: u32 = 0x8000_0005;
+ /// Grade codebook.
+ pub const GRADE_CB: u32 = 0x8000_0006;
+ /// File lane: codec9 magnitude bytes, or raw `u32` when there are more than 511 files.
+ pub const FILE: u32 = 0x8000_0007;
+ /// File presence plane, present only when `FILE` is codec9.
+ pub const FILE_PRES: u32 = 0x8000_0008;
+ /// File codebook.
+ pub const FILE_CB: u32 = 0x8000_0009;
+ /// Subject symbol ids.
+ pub const SUBJ: u32 = 0x8000_000A;
+ /// Object symbol ids.
+ pub const OBJ: u32 = 0x8000_000B;
+ /// Dispatch-target symbol ids (`MAY_DISPATCH` rows only).
+ pub const MAYT: u32 = 0x8000_000C;
+ /// Span starts.
+ pub const START: u32 = 0x8000_000D;
+ /// Span ends.
+ pub const END: u32 = 0x8000_000E;
+ /// Symbol blob offsets, `n_symbols + 1` entries.
+ pub const SYM_OFF: u32 = 0x8000_000F;
+ /// Symbol label bytes.
+ pub const SYM_BLOB: u32 = 0x8000_0010;
+ /// Per-symbol digest of the label.
+ pub const SYM_HASH: u32 = 0x8000_0011;
+ /// Per-symbol owner id.
+ pub const SYM_OWNER: u32 = 0x8000_0012;
+ /// Per-row Charon function declaration index.
+ pub const FUNDECL: u32 = 0x8000_0013;
+ /// Object-kind magnitude lane.
+ pub const OBJ_KIND_MAG: u32 = 0x8000_0014;
+ /// Object-kind presence plane.
+ pub const OBJ_KIND_PRES: u32 = 0x8000_0015;
+ /// Provenance record.
+ pub const PROV: u32 = 0x8000_0016;
+ /// Every tag a version-1 reader understands, in ascending order.
+ pub const ALL: [u32; 22] = [
+ PRED_MAG,
+ PRED_PRES,
+ PRED_CB,
+ GRADE_MAG,
+ GRADE_PRES,
+ GRADE_CB,
+ FILE,
+ FILE_PRES,
+ FILE_CB,
+ SUBJ,
+ OBJ,
+ MAYT,
+ START,
+ END,
+ SYM_OFF,
+ SYM_BLOB,
+ SYM_HASH,
+ SYM_OWNER,
+ FUNDECL,
+ OBJ_KIND_MAG,
+ OBJ_KIND_PRES,
+ PROV,
+ ];
+}
+
+/// Why a `.rows.bin` file was refused; one variant per refusal.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum BinRefusal {
+ /// Wrong magic.
+ Magic,
+ /// A major version this reader does not know.
+ Major(u16),
+ /// A minor version newer than this reader.
+ Minor(u16),
+ /// Non-zero reserved header bytes.
+ Reserved,
+ /// The file is shorter or longer than its header says.
+ Truncated {
+ /// Length recorded in the header (0 when the header itself is cut).
+ header: u64,
+ /// Actual byte length.
+ actual: usize,
+ },
+ /// The body does not match the header digest.
+ Digest {
+ /// Digest recorded in the header.
+ header: u64,
+ /// Digest of the body.
+ actual: u64,
+ },
+ /// The section directory does not fit in the file.
+ Directory,
+ /// A directory entry that is misaligned, out of order, overlapping,
+ /// outside the file or duplicated.
+ Section {
+ /// The entry's tag.
+ tag: u32,
+ },
+ /// An unknown section with the must-understand bit.
+ UnknownRequired {
+ /// The tag.
+ tag: u32,
+ },
+ /// A required section is absent.
+ MissingSection {
+ /// The tag.
+ tag: u32,
+ },
+ /// A section whose width, encoding or length is wrong.
+ LaneLength {
+ /// The tag.
+ tag: u32,
+ },
+ /// An id at or past the end of the table it names.
+ IdRange {
+ /// The lane.
+ tag: u32,
+ /// The row (or symbol) index.
+ row: usize,
+ },
+ /// A string that is not valid UTF-8, or an offset inside a character.
+ Utf8 {
+ /// The section.
+ tag: u32,
+ },
+ /// A codeword that is not allowed in its lane.
+ Codeword {
+ /// The lane.
+ tag: u32,
+ /// The row.
+ row: usize,
+ },
+}
+
+impl fmt::Display for BinRefusal {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::Magic => write!(f, "bad magic"),
+ Self::Major(v) => write!(f, "unsupported major version {v}"),
+ Self::Minor(v) => write!(f, "unsupported minor version {v}"),
+ Self::Reserved => write!(f, "reserved header bytes are not zero"),
+ Self::Truncated { header, actual } => {
+ write!(f, "file is {actual} bytes, header says {header}")
+ }
+ Self::Digest { header, actual } => {
+ write!(f, "digest mismatch: header {header:#x}, body {actual:#x}")
+ }
+ Self::Directory => write!(f, "section directory does not fit in the file"),
+ Self::Section { tag } => write!(f, "bad directory entry for section {tag:#x}"),
+ Self::UnknownRequired { tag } => write!(f, "unknown required section {tag:#x}"),
+ Self::MissingSection { tag } => write!(f, "missing section {tag:#x}"),
+ Self::LaneLength { tag } => write!(f, "section {tag:#x} has the wrong shape"),
+ Self::IdRange { tag, row } => write!(f, "id out of range in {tag:#x} at {row}"),
+ Self::Utf8 { tag } => write!(f, "invalid UTF-8 in section {tag:#x}"),
+ Self::Codeword { tag, row } => write!(f, "bad codeword in {tag:#x} at {row}"),
+ }
+ }
+}
+
+impl std::error::Error for BinRefusal {}
+
+// ---------------------------------------------------------------------------
+// header
+// ---------------------------------------------------------------------------
+
+/// The fixed 64-byte header, read without touching the body (the
+/// unchanged-refresh path).
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct Header {
+ /// Flag bits; bit 0 is "partial".
+ pub flags: u32,
+ /// Digest of every byte from offset 32 on.
+ pub digest: u64,
+ /// Total file length in bytes.
+ pub file_len: u64,
+ /// Row count.
+ pub n_rows: u32,
+ /// Symbol count.
+ pub n_symbols: u32,
+ /// File-name codebook size.
+ pub n_files: u32,
+ /// Directory entry count.
+ pub n_sections: u32,
+}
+
+fn le16(b: &[u8], o: usize) -> u16 {
+ b.get(o..o + 2)
+ .and_then(|s| s.first_chunk::<2>())
+ .map_or(0, |c| u16::from_le_bytes(*c))
+}
+
+fn le32(b: &[u8], o: usize) -> u32 {
+ b.get(o..o + 4)
+ .and_then(|s| s.first_chunk::<4>())
+ .map_or(0, |c| u32::from_le_bytes(*c))
+}
+
+fn le64(b: &[u8], o: usize) -> u64 {
+ b.get(o..o + 8)
+ .and_then(|s| s.first_chunk::<8>())
+ .map_or(0, |c| u64::from_le_bytes(*c))
+}
+
+fn len_u64(n: usize) -> u64 {
+ u64::try_from(n).unwrap_or(u64::MAX)
+}
+
+impl Header {
+ /// Validates magic, major, minor and reserved; never reads past byte 64.
+ ///
+ /// # Errors
+ /// [`BinRefusal::Magic`], [`BinRefusal::Major`], [`BinRefusal::Minor`] or
+ /// [`BinRefusal::Reserved`], checked in that order.
+ pub fn parse(bytes: &[u8; HEADER_LEN]) -> Result {
+ if bytes[0..8] != MAGIC {
+ return Err(BinRefusal::Magic);
+ }
+ let major = le16(bytes, 8);
+ if major != MAJOR {
+ return Err(BinRefusal::Major(major));
+ }
+ let minor = le16(bytes, 10);
+ if minor > MINOR {
+ return Err(BinRefusal::Minor(minor));
+ }
+ if bytes[48..64].iter().any(|&b| b != 0) {
+ return Err(BinRefusal::Reserved);
+ }
+ Ok(Self {
+ flags: le32(bytes, 12),
+ digest: le64(bytes, 16),
+ file_len: le64(bytes, 24),
+ n_rows: le32(bytes, 32),
+ n_symbols: le32(bytes, 36),
+ n_files: le32(bytes, 40),
+ n_sections: le32(bytes, 44),
+ })
+ }
+
+ /// `read_exact` of exactly 64 bytes from `r`, then [`Header::parse`]. No
+ /// body buffer is allocated.
+ ///
+ /// # Errors
+ /// The I/O error of `read_exact` (a file shorter than 64 bytes is
+ /// `UnexpectedEof`); the inner result carries the header refusal.
+ pub fn read(r: &mut impl std::io::Read) -> std::io::Result> {
+ let mut b = [0u8; HEADER_LEN];
+ r.read_exact(&mut b)?;
+ Ok(Self::parse(&b))
+ }
+
+ /// Whether the crate description was partial (Charon reported errors).
+ pub fn partial(&self) -> bool {
+ self.flags & FLAG_PARTIAL != 0
+ }
+}
+
+// ---------------------------------------------------------------------------
+// provenance
+// ---------------------------------------------------------------------------
+
+/// One provenance record. `None` is UNKNOWN and is distinct from every value,
+/// including the string `"UNKNOWN"`.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Prov {
+ /// Charon version.
+ pub charon_version: Option,
+ /// Charon commit.
+ pub charon_commit: Option,
+ /// LLBC schema version.
+ pub llbc_schema_version: Option,
+ /// Digest of the LLBC file.
+ pub llbc_digest: Option,
+ /// rustc commit.
+ pub rustc_commit: Option,
+ /// Target configuration.
+ pub target_config: Option,
+ /// Revision of the analysed source.
+ pub source_revision: Option,
+}
+
+// ---------------------------------------------------------------------------
+// layout of a verified file
+// ---------------------------------------------------------------------------
+
+type R = Range;
+
+/// A codec9 id lane: magnitude bytes plus a presence plane (byte ranges).
+#[derive(Debug, Clone)]
+struct Lane9 {
+ mag: R,
+ pres: R,
+}
+
+/// The file lane is codec9 up to 511 files, raw `u32` beyond.
+#[derive(Debug, Clone)]
+enum FileLane {
+ Codec9(Lane9),
+ Raw(R),
+}
+
+/// A string codebook: `n` entries, `n + 1` offsets, then the blob.
+#[derive(Debug, Clone)]
+struct Book {
+ n: usize,
+ offs: R,
+ blob: usize,
+}
+
+/// Everything a verified file's accessors need, as byte ranges into the buffer.
+#[derive(Debug, Clone)]
+struct Layout {
+ pred: Lane9,
+ grade: Lane9,
+ kind: Lane9,
+ file: FileLane,
+ pred_cb: Book,
+ grade_cb: Book,
+ file_cb: Book,
+ subj: R,
+ obj: R,
+ mayt: R,
+ start: R,
+ end: R,
+ fundecl: R,
+ sym_off: R,
+ sym_blob: R,
+ sym_hash: R,
+ sym_owner: R,
+ prov: Vec,
+}
+
+struct Sec {
+ width: u16,
+ enc: u16,
+ range: R,
+}
+
+type Secs = BTreeMap;
+
+fn find(secs: &Secs, tag: u32) -> Result<&Sec, BinRefusal> {
+ secs.get(&tag).ok_or(BinRefusal::MissingSection { tag })
+}
+
+fn lane_len(n: usize, width: usize, tag: u32) -> Result {
+ n.checked_mul(width).ok_or(BinRefusal::LaneLength { tag })
+}
+
+/// A fixed-shape section: exact element width, encoding and byte length.
+fn lane(secs: &Secs, tag: u32, width: u16, enc: u16, len: usize) -> Result {
+ let s = find(secs, tag)?;
+ if s.width != width || s.enc != enc || s.range.len() != len {
+ return Err(BinRefusal::LaneLength { tag });
+ }
+ Ok(s.range.clone())
+}
+
+fn lane9(secs: &Secs, mag: u32, pres: u32, n: usize) -> Result {
+ let m = lane(secs, mag, 1, ENC_CODEC9, n)?;
+ let p = lane(secs, pres, 8, ENC_PRES, lane_len(n.div_ceil(64), 8, pres)?)?;
+ Ok(Lane9 { mag: m, pres: p })
+}
+
+fn chunks4(b: &[u8]) -> &[[u8; 4]] {
+ b.as_chunks::<4>().0
+}
+
+fn chunks8(b: &[u8]) -> &[[u8; 8]] {
+ b.as_chunks::<8>().0
+}
+
+/// Only zero bytes, and fewer than 8 of them, may follow the last field.
+fn only_padding(rest: &[u8], tag: u32) -> Result<(), BinRefusal> {
+ if rest.len() < 8 && rest.iter().all(|&b| b == 0) {
+ Ok(())
+ } else {
+ Err(BinRefusal::LaneLength { tag })
+ }
+}
+
+/// Validate a KV codebook section and record where its offsets and blob are.
+fn book(buf: &[u8], tag: u32, sec: &Sec) -> Result {
+ let bad = BinRefusal::LaneLength { tag };
+ let data = &buf[sec.range.clone()];
+ if !data.len().is_multiple_of(8) {
+ return Err(bad);
+ }
+ let n = usize::try_from(le32(data, 0)).map_err(|_| bad.clone())?;
+ let table = n
+ .checked_add(1)
+ .and_then(|k| k.checked_mul(4))
+ .and_then(|k| k.checked_add(4))
+ .filter(|&k| k <= data.len())
+ .ok_or_else(|| bad.clone())?;
+ let offs = chunks4(&data[4..table])
+ .iter()
+ .map(|c| u32::from_le_bytes(*c))
+ .collect::>();
+ let blob = &data[table..];
+ let blob_len = offs
+ .last()
+ .and_then(|&o| usize::try_from(o).ok())
+ .ok_or_else(|| bad.clone())?;
+ if offs.first() != Some(&0) || blob_len > blob.len() || offs.windows(2).any(|w| w[0] > w[1]) {
+ return Err(bad);
+ }
+ only_padding(&blob[blob_len..], tag)?;
+ for w in offs.windows(2) {
+ let (a, b) = (w[0] as usize, w[1] as usize);
+ std::str::from_utf8(&blob[a..b]).map_err(|_| BinRefusal::Utf8 { tag })?;
+ }
+ Ok(Book {
+ n,
+ offs: sec.range.start + 4..sec.range.start + table,
+ blob: sec.range.start + table,
+ })
+}
+
+fn decode_prov(data: &[u8]) -> Result, BinRefusal> {
+ let tag = tag::PROV;
+ let bad = BinRefusal::LaneLength { tag };
+ if !data.len().is_multiple_of(8) || data.len() < 4 {
+ return Err(bad);
+ }
+ let n = le32(data, 0);
+ if n == 0 {
+ return Err(bad);
+ }
+ let mut pos = 4usize;
+ let mut out = Vec::new();
+ for _ in 0..n {
+ let mut f: Vec