Skip to main content

note_name

Function note_name 

Source
pub fn note_name(key: &str) -> String
Expand description

Map a node key to a filesystem- and wikilink-safe note stem that is unique per key even after case folding.

The name is always <hint>-<16 hex digits>: a lowercased, readable hint slugged from the key, then an unconditional 64-bit FNV-1a hash of the whole, exact key. Characters outside [a-z0-9._-] collapse to a single - in the hint; the hash carries everything the hint threw away.

§Why the hash is unconditional (issue #574)

It used to be applied only when the slug overran the filename limit, and the slug alone was lossy twice over. Measured on this repository — 8,239 nodes rendering to 8,135 notes, 104 of them silently overwritten:

mechanismlostwhere
every character outside the safe set becomes - and runs collapse, so …cytoscape.min.js#$a and …cytoscape.min.js#a are one name9everywhere
macOS and Windows fold filename case, so …#A and …#a are two names but one file95macOS, Windows

The second mechanism is the trap. A lossless-but-case-sensitive encoding fixes the 9, verifies clean on Linux CI, and still loses 95 notes on a Mac. So the requirement is stated after folding:

lower(note_name(k1)) == lower(note_name(k2))  implies  k1 == k2

This matters more than lossiness in a cache would, because the note names are the vault’s only stable interface: reset_vault_dir deletes and rebuilds the whole directory on every render, so the one thing that survives a render is a user’s own note outside the vault linking in by name (issue #442).

§The trade taken

Two decisions, and what each bought:

The hint is lowercased rather than case-preserved. Case-preserving would also satisfy the requirement — the hash differs for #A and #a, so the two names differ in their suffix and stay distinct under folding. It was rejected because lowercasing makes note_name(k) == note_name(k).to_lowercase() an invariant of the function, and that collapses the folded property into the literal one: there is then no way to write a version of this that is green on Linux and lossy on macOS, which is the defect shape this repository keeps finding. The cost is that parseHTTPHeader reads as parsehttpheader. That is affordable precisely because the hint is a hint — once a 17-character suffix is mandatory the name is not something anyone types from memory, so its job is to be recognisable in a file list, not to be transcribed.

Readability was spent, deliberately. Every name grows by 17 characters and hand-writing a link now needs Obsidian’s autocomplete. The alternatives that keep names short — hashing only the keys observed to collide — make the set of collisions platform-dependent, so one key would get one filename on macOS and another on Linux and a synced vault would churn. A name that is uglier everywhere beats a name that is different per platform.

The mapping is not reversible (the hint is lossy and the hash is one-way), but it does not need to be: every note’s frontmatter carries key: verbatim, so name → key is recoverable from the vault itself, which is the direction a reader actually needs.

§What “unique” rests on

Equal names imply equal hashes, not equal keys — this is a 64-bit hash, not a proof. Over this repository’s 8,239 keys there is no collision, and the birthday bound at that size is about 2e-12. Should one ever occur it is reported, not silent: NoteNames in the render path claims every filename case-insensitively and warns on a repeat. What is proved outright is the folding half — the output is lowercase by construction, so case folding is the identity on it.