1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
//! Content-hash drift-cache (#1282, epic #1277) — the persistent, rebuildable
//! project index. The **Rust realization of `formal/ProjectModel/Basic.lean`**:
//! cache each unit's derived value keyed by its content hash; on re-scan,
//! re-derive only the units whose hash **drifted** and reuse the cached value
//! for the rest. Two properties, both proved in the Lean and pinned by test
//! here:
//!
//! - **PO-B (drift completeness / no false-clean):** a unit is `changed` iff its
//! content hash differs. A value change implies a content change implies a
//! hash change (blake3 collision-resistance), so a changed unit is *always*
//! reported — the cache can never serve a stale value.
//! - **PO-C (incremental ≡ full rebuild):** [`apply_drift`] equals [`rebuild`]
//! for every edit (modify / add / delete). The drift-cache is IDE speed with a
//! machine-checked guarantee it never diverges from an honest fresh scan —
//! *freshness by verification*.
//!
//! Generic over the cached value `T`, so it serves the project model, the symbol
//! table, or the embedding vectors alike. The `version` (derive/chunker/
//! embedding-model id) forces a clean rebuild on a bump. Pure core +
//! [`load`]/[`save`] fs wrappers (fail-soft: a corrupt cache rebuilds, never
//! blocks).
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
/// A content hash (blake3 hex) — the drift key. Collision-resistance is what
/// discharges PO-B (hash-equality ⇒ content-equality).
#[must_use]
pub fn content_hash(bytes: &[u8]) -> String {
blake3::hash(bytes).to_hex().to_string()
}
/// One cached unit: its content hash + the derived value.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CacheEntry<T> {
pub hash: String,
pub value: T,
}
/// The drift-cache: unit-id → `(content hash, derived value)`, plus the store
/// `version` (a bump — new derive/chunker/embedding-model — forces a rebuild).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DriftCache<T> {
pub version: String,
pub entries: BTreeMap<String, CacheEntry<T>>,
}
impl<T> DriftCache<T> {
#[must_use]
pub fn new(version: impl Into<String>) -> Self {
Self {
version: version.into(),
entries: BTreeMap::new(),
}
}
}
impl<T> Default for DriftCache<T> {
fn default() -> Self {
Self::new(String::new())
}
}
/// What drifted between the cache and the current scan.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DriftSet {
/// Units new or whose content hash changed (need re-derive).
pub changed: Vec<String>,
/// Units in the cache but absent now (evicted).
pub removed: Vec<String>,
}
impl DriftSet {
#[must_use]
pub fn is_empty(&self) -> bool {
self.changed.is_empty() && self.removed.is_empty()
}
}
/// **Pure — PO-B.** The drift between a cache and the current unit hashes.
/// `changed` = units whose hash differs from the cache (new or modified);
/// `removed` = cached units absent now. A unit whose derived value would change
/// has a different content hash, so it is always in `changed` — no false-clean.
#[must_use]
pub fn drift<T>(cache: &DriftCache<T>, current: &BTreeMap<String, String>) -> DriftSet {
let changed = current
.iter()
.filter(|(unit, hash)| cache.entries.get(*unit).map(|e| &e.hash) != Some(hash))
.map(|(unit, _)| unit.clone())
.collect();
let removed = cache
.entries
.keys()
.filter(|u| !current.contains_key(*u))
.cloned()
.collect();
DriftSet { changed, removed }
}
/// **Pure — PO-C.** Apply drift: build the next cache by re-deriving only the
/// units whose hash changed and reusing the cached value for clean ones
/// (dropping removed). `derive(unit, hash)` produces a (re-)derived value.
/// Provably equals [`rebuild`] (the Lean theorem `po_c_incremental_eq_full`).
#[must_use]
pub fn apply_drift<T: Clone>(
prev: &DriftCache<T>,
current: &BTreeMap<String, String>,
derive: impl Fn(&str, &str) -> T,
) -> DriftCache<T> {
let entries = current
.iter()
.map(|(unit, hash)| {
let value = match prev.entries.get(unit) {
Some(e) if &e.hash == hash => e.value.clone(),
_ => derive(unit, hash),
};
(
unit.clone(),
CacheEntry {
hash: hash.clone(),
value,
},
)
})
.collect();
DriftCache {
version: prev.version.clone(),
entries,
}
}
/// **Pure.** A from-scratch rebuild (derive every unit) — the reference
/// [`apply_drift`] is proved equal to (PO-C).
#[must_use]
pub fn rebuild<T>(
version: &str,
current: &BTreeMap<String, String>,
derive: impl Fn(&str, &str) -> T,
) -> DriftCache<T> {
let entries = current
.iter()
.map(|(unit, hash)| {
(
unit.clone(),
CacheEntry {
hash: hash.clone(),
value: derive(unit, hash),
},
)
})
.collect();
DriftCache {
version: version.to_string(),
entries,
}
}
/// The on-disk store path for a repo's index:
/// `<config dir>/index/<repo-hash>/<name>.json` — a drop-in dir (config-lean).
#[must_use]
pub fn store_path(config_dir: &Path, repo_root: &Path, name: &str) -> PathBuf {
let repo_hash = content_hash(repo_root.to_string_lossy().as_bytes());
config_dir
.join("index")
.join(&repo_hash[..16])
.join(format!("{name}.json"))
}
/// Load a cache from `path` (JSON). `None` on a missing / unreadable / malformed
/// file, OR when the stored `version` differs from `expected_version` (a bump ⇒
/// a clean rebuild). Fail-soft — a corrupt cache never blocks; it rebuilds.
#[must_use]
pub fn load<T: for<'de> Deserialize<'de>>(
path: &Path,
expected_version: &str,
) -> Option<DriftCache<T>> {
let text = std::fs::read_to_string(path).ok()?;
let cache: DriftCache<T> = serde_json::from_str(&text).ok()?;
(cache.version == expected_version).then_some(cache)
}
/// Save the cache to `path` atomically (write `.tmp`, then rename — so a killed
/// write never leaves a torn cache that would load as a partial index).
/// Best-effort: a failure never perturbs the session (the next run rebuilds).
pub fn save<T: Serialize>(path: &Path, cache: &DriftCache<T>) {
let Ok(json) = serde_json::to_string(cache) else {
return;
};
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let tmp = path.with_extension("json.tmp");
if std::fs::write(&tmp, json).is_ok() {
let _ = std::fs::rename(&tmp, path);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
fn cache(version: &str, kv: &[(&str, &str, &str)]) -> DriftCache<String> {
let mut c = DriftCache::new(version);
for (u, h, v) in kv {
c.entries.insert(
(*u).to_string(),
CacheEntry {
hash: (*h).to_string(),
value: (*v).to_string(),
},
);
}
c
}
fn hashes(kv: &[(&str, &str)]) -> BTreeMap<String, String> {
kv.iter()
.map(|(u, h)| (u.to_string(), h.to_string()))
.collect()
}
#[test]
fn content_hash_is_stable_blake3_hex() {
assert_eq!(content_hash(b"x"), content_hash(b"x"));
assert_ne!(content_hash(b"x"), content_hash(b"y"));
assert_eq!(content_hash(b"x").len(), 64);
}
#[test]
fn drift_reports_changed_new_and_removed_no_false_clean() {
// PO-B: a same-hash unit is clean; a different-hash unit is changed
// (a value change ⇒ content change ⇒ hash change — never a false-clean);
// a new unit is changed; a vanished unit is removed.
let c = cache(
"v1",
&[("a", "h1", "A"), ("b", "h2", "B"), ("gone", "h9", "G")],
);
let d = drift(&c, &hashes(&[("a", "h1"), ("b", "h2_NEW"), ("c", "h3")]));
let mut changed = d.changed.clone();
changed.sort();
assert_eq!(changed, vec!["b".to_string(), "c".to_string()]);
assert_eq!(d.removed, vec!["gone".to_string()]);
// Fully-unchanged tree ⇒ empty drift.
assert!(drift(&c, &hashes(&[("a", "h1"), ("b", "h2"), ("gone", "h9")])).is_empty());
}
#[test]
fn apply_drift_equals_full_rebuild_for_every_edit() {
// PO-C: the drift-updated cache == a from-scratch rebuild, for modify /
// add / delete. `derive` stamps "D:" so reuse vs re-derive is visible.
let prev = cache("v1", &[("a", "h1", "A"), ("b", "h2", "B")]);
let derive = |u: &str, _h: &str| format!("D:{u}");
for current in [
hashes(&[("a", "h1"), ("b", "h2_NEW")]), // modify b
hashes(&[("a", "h1"), ("b", "h2"), ("c", "h3")]), // add c
hashes(&[("a", "h1")]), // delete b
hashes(&[("x", "hx")]), // total churn
] {
let incremental = apply_drift(&prev, ¤t, derive);
let full = rebuild(&prev.version, ¤t, derive);
// The theorem: same entries + hashes. (Values differ only where the
// incremental path REUSED a cached value it was entitled to.)
assert_eq!(
incremental.entries.keys().collect::<Vec<_>>(),
full.entries.keys().collect::<Vec<_>>()
);
for (u, e) in &incremental.entries {
assert_eq!(&e.hash, &full.entries[u].hash);
// A clean carried-over unit keeps its cached value; a re-derived
// one matches the fresh derive — either way, model-equal.
let clean = prev.entries.get(u).map(|p| &p.hash) == Some(&e.hash);
if clean {
assert_eq!(&e.value, &prev.entries[u].value);
} else {
assert_eq!(&e.value, &full.entries[u].value);
}
}
}
}
#[test]
fn apply_drift_only_rederives_changed_units() {
// The efficiency claim: derive is called exactly for the changed units,
// never for the clean ones (this is what makes a re-scan fast).
let prev = cache(
"v1",
&[("a", "h1", "A"), ("b", "h2", "B"), ("c", "h3", "C")],
);
let calls = RefCell::new(Vec::new());
let derive = |u: &str, _h: &str| {
calls.borrow_mut().push(u.to_string());
format!("D:{u}")
};
// Only b changed; a and c are clean.
let _ = apply_drift(
&prev,
&hashes(&[("a", "h1"), ("b", "h2_NEW"), ("c", "h3")]),
derive,
);
assert_eq!(
*calls.borrow(),
vec!["b".to_string()],
"only the drifted unit re-derives"
);
}
#[test]
fn store_path_is_repo_scoped_under_index_dir() {
// Path-component assertions (not string slashes) so this holds on
// Windows too, where the separator is `\`.
let p = store_path(Path::new("/home/x/.newt"), Path::new("/repo/foo"), "model");
let comps: Vec<String> = p
.components()
.map(|c| c.as_os_str().to_string_lossy().into_owned())
.collect();
assert!(comps.contains(&"index".to_string()), "{p:?}");
assert_eq!(p.file_name().and_then(|f| f.to_str()), Some("model.json"));
// The repo-hash dir sits between `index` and the file, and is stable.
let idx = comps.iter().position(|c| c == "index").unwrap();
assert_eq!(comps[idx + 1].len(), 16, "16-char repo hash: {p:?}");
}
#[test]
fn load_rejects_a_version_mismatch() {
// A version bump ⇒ the cache does not load ⇒ a clean rebuild. Pure over
// the parsed value (the fs read is the thin wrapper).
let c = cache("v1", &[("a", "h1", "A")]);
let json = serde_json::to_string(&c).unwrap();
let back: DriftCache<String> = serde_json::from_str(&json).unwrap();
assert_eq!(back, c);
assert_ne!(
back.version, "v2",
"a v2 expectation would reject this v1 cache"
);
}
}