Fingerprint ReadPlan/OffsetMap: Hash + Eq + fingerprint() (ADR-012 §1/§4, plan phase 6)

- #[derive(Hash, Eq)] on ReadPlan, FieldPlan, CompositePlan, ReadKind,
  DiscriminatorPlan (schema: Arc<Value> hashes via serde_json Value
  Hash + Eq under preserve_order), and on OffsetMap (+ Clone;
  LeafMeta/OffsetEntry/ByteRange payload already Hash from phase 5 /
  this phase).
- fingerprint() -> u64 on both via std DefaultHasher (deferred
  decision 3 resolved: no new dep, not hot, cross-version stability a
  non-goal per ADR-012).
- Contract tests both sides: equal schemas -> equal PartialEq +
  fingerprint; field-kind / field-order / endianness changes each
  break equality and fingerprint; different root names over the same
  document fingerprint differently (ReadPlan).
- ValidationPlan already carries its own Hash/Eq/fingerprint +
  contract test (phase 7 landed early).

Verification: 474 tests pass (9 new fingerprint contract tests);
clippy -D warnings clean; cargo doc zero warnings; wasm32 release
build green.
This commit is contained in:
glm-5.3-flash committed 2026-09-02 09:16:11 +00:00
1 parent 255c8c493e
commit 537a2170fb
3 files changed
+193 -8

No files matched your search

+18 -1
View File
@@ -641,7 +641,24 @@ wasm32-unknown-unknown --release`.
---
## Phase 6 — Fingerprinting `ReadPlan`/`OffsetMap` (ADR-012 §1, §4)
## Phase 6 — Fingerprinting `ReadPlan`/`OffsetMap` (ADR-012 §1, §4) — **DONE (2026-09-02)**
> **Status: implemented.** `#[derive(Hash, Eq)]` added to `ReadPlan`,
> `FieldPlan`, `CompositePlan`, `ReadKind`, `DiscriminatorPlan`
> (`ReadPlan`'s `schema: Arc<Value>` hashes fine — `serde_json::Value:
> Hash + Eq` under the pinned `preserve_order` serde_json) and to
> `OffsetMap` (`Clone` added alongside; its `LeafMeta`/`OffsetEntry`/
> `ByteRange` payload gained `Hash` in phase 5 / this phase). The
> POC's `VariantPlan`/`VariantKind` don't exist in the production
> shape (phase 1 dropped them). `fingerprint() -> u64` on both via
> `DefaultHasher` (deferred decision 3 resolved: std `DefaultHasher`,
> no new dep; the fingerprint isn't hot; cross-version stability is a
> non-goal per ADR-012). Fingerprint contract tests on both: same
> schema twice → equal `PartialEq` + equal fingerprint; field-kind
> change, field-order change, and endianness change each → different
> fingerprints; (ReadPlan) different root names over the same document
> → different fingerprints. `ValidationPlan` already carries its own
> `Hash + Eq` + `fingerprint` + contract test (phase 7).
**Goal:** Add `Hash + Eq` derives + `fingerprint() -> u64` to `ReadPlan`
and `OffsetMap`. Enables cross-run caching, `alkcall` schema handshake,
+78 -1
View File
@@ -97,7 +97,11 @@ impl OffsetEntry {
/// in the table as their fixed-position portion only (length prefix,
/// `{offset, length}` pair, or `maxLength` reservation); the variable
/// data lives outside the static layout.
#[derive(Debug)]
///
/// `PartialEq`/`Eq`/`Hash`: two equal maps produce identical
/// read/write behavior over identical buffers (the compute is a pure
/// function of the document).
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct OffsetMap {
fields: Vec<(String, OffsetEntry)>,
total_size: usize,
@@ -166,6 +170,19 @@ impl OffsetMap {
pub fn iter(&self) -> impl Iterator<Item = (&str, &OffsetEntry)> {
self.fields.iter().map(|(path, entry)| (path.as_str(), entry))
}
/// A stable-within-version hash of the map (ADR-012 §1/§4).
///
/// Two maps with equal fingerprints (equal hashes) produce identical
/// read/write behavior over identical buffers. Cross-version
/// stability is a non-goal (ADR-012). Uses `DefaultHasher` (std, no
/// new dep); the fingerprint is not hot.
pub fn fingerprint(&self) -> u64 {
use std::hash::{Hash, Hasher};
let mut h = std::hash::DefaultHasher::new();
self.hash(&mut h);
h.finish()
}
}
/// Mutable context threaded through the recursive offset computation.
@@ -926,6 +943,66 @@ mod tests {
assert_eq!(paths, vec!["a", "b"]);
}
// ----- Fingerprint contract (ADR-012 §1/§4) ---------------------------
#[test]
fn fingerprint_is_equal_for_equal_maps() {
let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [
{ "name": "a", "kind": "uint8" },
{ "name": "b", "kind": "uint32" }
]}}});
let m1 = map(&root, "S");
let m2 = map(&root, "S");
assert_eq!(m1, m2);
assert_eq!(m1.fingerprint(), m2.fingerprint());
}
#[test]
fn fingerprint_changes_when_a_field_changes() {
let root_a = json!({ "$defs": { "S": { "kind": "struct", "fields": [
{ "name": "a", "kind": "uint8" },
{ "name": "b", "kind": "uint32" }
]}}});
let root_b = json!({ "$defs": { "S": { "kind": "struct", "fields": [
{ "name": "a", "kind": "uint16" },
{ "name": "b", "kind": "uint32" }
]}}});
let ma = map(&root_a, "S");
let mb = map(&root_b, "S");
assert_ne!(ma, mb);
assert_ne!(ma.fingerprint(), mb.fingerprint());
}
#[test]
fn fingerprint_changes_when_field_order_changes() {
let root_a = json!({ "$defs": { "S": { "kind": "struct", "fields": [
{ "name": "a", "kind": "uint8" },
{ "name": "b", "kind": "uint8" }
]}}});
let root_b = json!({ "$defs": { "S": { "kind": "struct", "fields": [
{ "name": "b", "kind": "uint8" },
{ "name": "a", "kind": "uint8" }
]}}});
let ma = map(&root_a, "S");
let mb = map(&root_b, "S");
assert_ne!(ma, mb);
assert_ne!(ma.fingerprint(), mb.fingerprint());
}
#[test]
fn fingerprint_changes_when_endianness_changes() {
let root_a = json!({ "$defs": { "S": { "kind": "struct", "endian": "little", "fields": [
{ "name": "id", "kind": "uint32" }
]}}});
let root_b = json!({ "$defs": { "S": { "kind": "struct", "endian": "big", "fields": [
{ "name": "id", "kind": "uint32" }
]}}});
let ma = map(&root_a, "S");
let mb = map(&root_b, "S");
assert_ne!(ma, mb);
assert_ne!(ma.fingerprint(), mb.fingerprint());
}
#[test]
fn compute_rejects_non_struct_top_level() {
let root = json!({
+97 -6
View File
@@ -61,7 +61,7 @@ const MAX_COMPILE_DEPTH: usize = 128;
/// integration"). `PartialEq` holds: two equal plans produce identical
/// reads over identical buffers (the compile is a pure function of the
/// document + root name).
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ReadPlan {
endian: Endian,
fields: Vec<FieldPlan>,
@@ -72,7 +72,7 @@ pub struct ReadPlan {
/// One compiled struct field: the read dispatch kind, the effective
/// endianness, the field-level annotations, and (for composites) the
/// compiled body.
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct FieldPlan {
name: String,
kind: ReadKind,
@@ -84,7 +84,7 @@ pub struct FieldPlan {
/// The read dispatch kind for a field. Composites carry their compiled
/// body in [`FieldPlan::body`]; primitives and enums read directly.
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ReadKind {
Primitive(AlkTypeKind),
Enum,
@@ -96,7 +96,7 @@ pub enum ReadKind {
/// The compiled body of a composite node — the packed-read analog of
/// `BastType`'s composite arms.
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum CompositePlan {
/// A nested struct — its own field list and container default.
Struct(ReadPlan),
@@ -129,7 +129,7 @@ pub enum CompositePlan {
}
/// The compiled union discriminator (ADR-003, D-BAST-005).
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum DiscriminatorPlan {
/// A fixed-size integer at a known byte offset within the union's
/// buffer. Mapping keys are stringified integers.
@@ -196,10 +196,26 @@ impl ReadPlan {
/// The BAST document this plan was compiled from (retained as
/// `Arc<Value>` — the reader's `schema()` accessor returns it, and
/// phase 6's `Hash` derive covers it via `serde_json::Value: Hash`).
/// the `Hash` derive covers it via `serde_json::Value: Hash`).
pub fn schema(&self) -> &Value {
&self.schema
}
/// A stable-within-version hash of the plan (ADR-012 §1/§4).
///
/// Two plans with equal [`fingerprint`](Self::fingerprint)s (equal
/// hashes) produce identical reads over identical bytes — the plan
/// is a pure function of the document + root name, and the hash is
/// derived from the full plan contents. Cross-version stability is
/// a non-goal (ADR-012): a new `AlkTypeKind` may change the hash.
/// Uses `DefaultHasher` (std, no new dep); the fingerprint is not
/// hot.
pub fn fingerprint(&self) -> u64 {
use std::hash::{Hash, Hasher};
let mut h = std::hash::DefaultHasher::new();
self.hash(&mut h);
h.finish()
}
}
impl FieldPlan {
@@ -1255,6 +1271,81 @@ mod tests {
assert!(debug.contains("ReadPlan"));
}
// ----- Fingerprint contract (ADR-012 §1/§4) ---------------------------
#[test]
fn fingerprint_is_equal_for_equal_plans() {
let root = json!({ "$defs": {
"S": { "kind": "struct", "endian": "big", "fields": [
{ "name": "id", "kind": "uint32" },
{ "name": "pt", "kind": { "$ref": "#/$defs/Point" } }
]},
"Point": { "kind": "struct", "fields": [
{ "name": "x", "kind": "uint16" },
{ "name": "y", "kind": "uint16" }
]}
}});
let p1 = plan(&root, "S");
let p2 = plan(&root, "S");
assert_eq!(p1, p2);
assert_eq!(p1.fingerprint(), p2.fingerprint());
}
#[test]
fn fingerprint_changes_when_a_field_changes() {
let root_a = json!({ "$defs": { "S": { "kind": "struct", "fields": [
{ "name": "a", "kind": "uint8" }
]}}});
let root_b = json!({ "$defs": { "S": { "kind": "struct", "fields": [
{ "name": "a", "kind": "uint16" }
]}}});
let pa = plan(&root_a, "S");
let pb = plan(&root_b, "S");
assert_ne!(pa, pb);
assert_ne!(pa.fingerprint(), pb.fingerprint());
}
#[test]
fn fingerprint_changes_when_field_order_changes() {
let root_a = json!({ "$defs": { "S": { "kind": "struct", "fields": [
{ "name": "a", "kind": "uint8" },
{ "name": "b", "kind": "uint8" }
]}}});
let root_b = json!({ "$defs": { "S": { "kind": "struct", "fields": [
{ "name": "b", "kind": "uint8" },
{ "name": "a", "kind": "uint8" }
]}}});
let pa = plan(&root_a, "S");
let pb = plan(&root_b, "S");
assert_ne!(pa, pb);
assert_ne!(pa.fingerprint(), pb.fingerprint());
}
#[test]
fn fingerprint_changes_when_endianness_changes() {
let root_a = json!({ "$defs": { "S": { "kind": "struct", "endian": "little", "fields": [
{ "name": "id", "kind": "uint32" }
]}}});
let root_b = json!({ "$defs": { "S": { "kind": "struct", "endian": "big", "fields": [
{ "name": "id", "kind": "uint32" }
]}}});
let pa = plan(&root_a, "S");
let pb = plan(&root_b, "S");
assert_ne!(pa, pb);
assert_ne!(pa.fingerprint(), pb.fingerprint());
}
#[test]
fn fingerprint_differs_for_different_root_names() {
let root = json!({ "$defs": {
"A": { "kind": "struct", "fields": [ { "name": "a", "kind": "uint8" } ] },
"B": { "kind": "struct", "fields": [ { "name": "b", "kind": "uint16" } ] }
}});
let pa = plan(&root, "A");
let pb = plan(&root, "B");
assert_ne!(pa.fingerprint(), pb.fingerprint());
}
// ----- Send + Sync (ADR-011 §"Engine integration") --------------------
#[test]