FNV-1a over a string → 64-bit hex (16 lowercase chars, zero-padded).
Stable across runs and platforms.
The width is load-bearing, not decoration. This hash is not only a
drift detector: diffModels(..., { matchUnpairedByContent: true }) treats
equality of the derived dataHash as identity — the 1:1 branch retires a
real added and a real deleted entry in favour of one match record. That
is an all-pairs birthday problem whose exposure grows with the square of the
number of distinct fingerprints compared, and a from-scratch re-export
leaves the whole model unpaired, which is the worst case. The previous
32-bit width was demonstrably too narrow for that: collisions between
plausible IFC content were findable by enumeration (issue #1962).
64-bit FNV-1a is much stronger but still not a cryptographic hash — it is
chosen for non-adversarial drift-catching and content matching, not for
verifier-facing claims. The guards in diff.ts (bucket by ifcType,
require components agreement) stay: they are what makes a residual
collision non-destructive, and they are unaffected by the width.
JS has no unsigned 64-bit integer type, so the state is a BigInt masked
back to 64 bits after every multiply. Characters are fed in as UTF-16 code
units (charCodeAt), the same construction as before — the Rust twin hashes
bytes, so the two agree on constants and mixing, not on output for a given
string, and neither is compared against the other.
FNV-1a over a string → 64-bit hex (16 lowercase chars, zero-padded). Stable across runs and platforms.
The width is load-bearing, not decoration. This hash is not only a drift detector:
diffModels(..., { matchUnpairedByContent: true })treats equality of the deriveddataHashas identity — the 1:1 branch retires a realaddedand a realdeletedentry in favour of one match record. That is an all-pairs birthday problem whose exposure grows with the square of the number of distinct fingerprints compared, and a from-scratch re-export leaves the whole model unpaired, which is the worst case. The previous 32-bit width was demonstrably too narrow for that: collisions between plausible IFC content were findable by enumeration (issue #1962).64-bit FNV-1a is much stronger but still not a cryptographic hash — it is chosen for non-adversarial drift-catching and content matching, not for verifier-facing claims. The guards in
diff.ts(bucket byifcType, requirecomponentsagreement) stay: they are what makes a residual collision non-destructive, and they are unaffected by the width.JS has no unsigned 64-bit integer type, so the state is a
BigIntmasked back to 64 bits after every multiply. Characters are fed in as UTF-16 code units (charCodeAt), the same construction as before — the Rust twin hashes bytes, so the two agree on constants and mixing, not on output for a given string, and neither is compared against the other.