Skip to content

Commit 14bb4f7

Browse files
authored
v0.7.0: Warm sync with file watcher, placement learning, fuzzy links (#8)
## Summary - File watcher inside `engraph serve` — real-time detection of vault changes via notify-debouncer-full, automatic re-indexing with 2s debounce - Placement correction learning — detects when user moves a note from suggested_folder to a different folder, updates centroids incrementally - Fuzzy link matching — sliding window Levenshtein matching (0.92 threshold) + first-name matching for People notes (suggestion-only) - `created_by` filtering — track note origin, filter via list MCP tool - Centroid math fix — replaced EMA (0.9/0.1) with true online mean - Indexer refactoring — extracted index_file, remove_file, rename_file for per-file operations 225 tests passing, clippy clean, manually tested against live vault.
1 parent 6250f94 commit 14bb4f7

14 files changed

Lines changed: 2572 additions & 328 deletions

File tree

Cargo.lock

Lines changed: 238 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ strsim = "0.11"
2929
ignore = "0.4"
3030
rmcp = { version = "1.2", features = ["transport-io"] }
3131
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
32+
notify = "7.0"
33+
notify-debouncer-full = "0.4"
3234

3335
[dev-dependencies]
3436
tempfile = "3"

src/context.rs

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -201,9 +201,10 @@ pub fn context_list(
201201
params: &ContextParams,
202202
folder: Option<&str>,
203203
tags: &[String],
204+
created_by: Option<&str>,
204205
limit: usize,
205206
) -> Result<Vec<NoteListItem>> {
206-
let files = params.store.list_files(folder, tags, limit)?;
207+
let files = params.store.list_files(folder, tags, created_by, limit)?;
207208
let file_ids: Vec<i64> = files.iter().map(|f| f.id).collect();
208209
let edge_counts = params
209210
.store
@@ -364,7 +365,7 @@ pub fn context_project(params: &ContextParams, name: &str) -> Result<ProjectCont
364365

365366
// Files in same folder
366367
if let Some(folder) = &project_folder {
367-
let folder_files = params.store.list_files(Some(folder), &[], 50)?;
368+
let folder_files = params.store.list_files(Some(folder), &[], None, 50)?;
368369
for f in folder_files {
369370
if Some(f.id) != project_id && child_ids.insert(f.id) {
370371
child_records.push(f);
@@ -664,9 +665,11 @@ mod tests {
664665
let d1 = generate_docid("note.md");
665666
let d2 = generate_docid("other.md");
666667
store
667-
.insert_file("note.md", "h1", 100, &["rust".into()], &d1)
668+
.insert_file("note.md", "h1", 100, &["rust".into()], &d1, None)
669+
.unwrap();
670+
store
671+
.insert_file("other.md", "h2", 100, &[], &d2, None)
668672
.unwrap();
669-
store.insert_file("other.md", "h2", 100, &[], &d2).unwrap();
670673

671674
let f1 = store.get_file("note.md").unwrap().unwrap().id;
672675
let f2 = store.get_file("other.md").unwrap().unwrap().id;
@@ -712,7 +715,7 @@ mod tests {
712715
fn test_read_file_not_on_disk() {
713716
let (_tmp, store, root) = setup_vault();
714717
store
715-
.insert_file("ghost.md", "h3", 100, &[], "ggg333")
718+
.insert_file("ghost.md", "h3", 100, &[], "ggg333", None)
716719
.unwrap();
717720
let params = ContextParams {
718721
store: &store,
@@ -743,7 +746,7 @@ mod tests {
743746
vault_path: &root,
744747
profile: None,
745748
};
746-
let items = context_list(&params, None, &[], 20).unwrap();
749+
let items = context_list(&params, None, &[], None, 20).unwrap();
747750
assert_eq!(items.len(), 2);
748751
}
749752

@@ -755,7 +758,7 @@ mod tests {
755758
vault_path: &root,
756759
profile: None,
757760
};
758-
let items = context_list(&params, None, &["rust".into()], 20).unwrap();
761+
let items = context_list(&params, None, &["rust".into()], None, 20).unwrap();
759762
assert_eq!(items.len(), 1);
760763
assert_eq!(items[0].path, "note.md");
761764
}
@@ -803,10 +806,17 @@ mod tests {
803806

804807
let store = Store::open_memory().unwrap();
805808
let f1 = store
806-
.insert_file("People/John.md", "h1", 100, &["person".into()], "aaa111")
809+
.insert_file(
810+
"People/John.md",
811+
"h1",
812+
100,
813+
&["person".into()],
814+
"aaa111",
815+
None,
816+
)
807817
.unwrap();
808818
let f2 = store
809-
.insert_file("daily.md", "h2", 100, &[], "bbb222")
819+
.insert_file("daily.md", "h2", 100, &[], "bbb222", None)
810820
.unwrap();
811821
store.insert_edge(f2, f1, "mention").unwrap();
812822
store
@@ -865,10 +875,11 @@ mod tests {
865875
100,
866876
&["project".into()],
867877
"aaa111",
878+
None,
868879
)
869880
.unwrap();
870881
let f2 = store
871-
.insert_file("01-Projects/child.md", "h2", 100, &[], "bbb222")
882+
.insert_file("01-Projects/child.md", "h2", 100, &[], "bbb222", None)
872883
.unwrap();
873884
store.insert_edge(f2, f1, "wikilink").unwrap();
874885
store.insert_edge(f1, f2, "wikilink").unwrap();
@@ -915,7 +926,7 @@ mod tests {
915926

916927
let store = Store::open_memory().unwrap();
917928
store
918-
.insert_file("result.md", "h1", 100, &["topic".into()], "aaa111")
929+
.insert_file("result.md", "h1", 100, &["topic".into()], "aaa111", None)
919930
.unwrap();
920931

921932
let params = ContextParams {
@@ -948,7 +959,7 @@ mod tests {
948959

949960
let store = Store::open_memory().unwrap();
950961
store
951-
.insert_file("long.md", "h1", 100, &[], "aaa111")
962+
.insert_file("long.md", "h1", 100, &[], "aaa111", None)
952963
.unwrap();
953964

954965
let params = ContextParams {
@@ -981,10 +992,10 @@ mod tests {
981992

982993
let store = Store::open_memory().unwrap();
983994
let f1 = store
984-
.insert_file("main.md", "h1", 100, &[], "aaa111")
995+
.insert_file("main.md", "h1", 100, &[], "aaa111", None)
985996
.unwrap();
986997
let f2 = store
987-
.insert_file("related.md", "h2", 100, &[], "bbb222")
998+
.insert_file("related.md", "h2", 100, &[], "bbb222", None)
988999
.unwrap();
9891000
store.insert_edge(f1, f2, "wikilink").unwrap();
9901001

src/fts.rs

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ mod tests {
2727
100,
2828
&[],
2929
&generate_docid("notes/ticket.md"),
30+
None,
3031
)
3132
.unwrap();
3233

@@ -54,6 +55,7 @@ mod tests {
5455
100,
5556
&[],
5657
&generate_docid("notes/note.md"),
58+
None,
5759
)
5860
.unwrap();
5961

@@ -70,13 +72,34 @@ mod tests {
7072
let store = setup_store();
7173

7274
let file_id1 = store
73-
.insert_file("notes/a.md", "h1", 100, &[], &generate_docid("notes/a.md"))
75+
.insert_file(
76+
"notes/a.md",
77+
"h1",
78+
100,
79+
&[],
80+
&generate_docid("notes/a.md"),
81+
None,
82+
)
7483
.unwrap();
7584
let file_id2 = store
76-
.insert_file("notes/b.md", "h2", 100, &[], &generate_docid("notes/b.md"))
85+
.insert_file(
86+
"notes/b.md",
87+
"h2",
88+
100,
89+
&[],
90+
&generate_docid("notes/b.md"),
91+
None,
92+
)
7793
.unwrap();
7894
let file_id3 = store
79-
.insert_file("notes/c.md", "h3", 100, &[], &generate_docid("notes/c.md"))
95+
.insert_file(
96+
"notes/c.md",
97+
"h3",
98+
100,
99+
&[],
100+
&generate_docid("notes/c.md"),
101+
None,
102+
)
80103
.unwrap();
81104

82105
// Chunk with "delivery" appearing multiple times should rank higher.
@@ -114,6 +137,7 @@ mod tests {
114137
100,
115138
&[],
116139
&generate_docid("notes/del.md"),
140+
None,
117141
)
118142
.unwrap();
119143

src/graph.rs

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ mod tests {
217217
100,
218218
&["rust".into()],
219219
&generate_docid("seed.md"),
220+
None,
220221
)
221222
.unwrap();
222223
let f2 = store
@@ -226,6 +227,7 @@ mod tests {
226227
100,
227228
&["rust".into()],
228229
&generate_docid("linked.md"),
230+
None,
229231
)
230232
.unwrap();
231233
let _f3 = store
@@ -235,6 +237,7 @@ mod tests {
235237
100,
236238
&[],
237239
&generate_docid("unlinked.md"),
240+
None,
238241
)
239242
.unwrap();
240243

@@ -265,10 +268,10 @@ mod tests {
265268
fn test_graph_expand_skips_seeds() {
266269
let store = Store::open_memory().unwrap();
267270
let f1 = store
268-
.insert_file("a.md", "h1", 100, &[], &generate_docid("a.md"))
271+
.insert_file("a.md", "h1", 100, &[], &generate_docid("a.md"), None)
269272
.unwrap();
270273
let f2 = store
271-
.insert_file("b.md", "h2", 100, &[], &generate_docid("b.md"))
274+
.insert_file("b.md", "h2", 100, &[], &generate_docid("b.md"), None)
272275
.unwrap();
273276

274277
store.insert_edge(f1, f2, "wikilink").unwrap();
@@ -302,13 +305,20 @@ mod tests {
302305
fn test_graph_expand_multi_parent_takes_highest() {
303306
let store = Store::open_memory().unwrap();
304307
let f1 = store
305-
.insert_file("a.md", "h1", 100, &[], &generate_docid("a.md"))
308+
.insert_file("a.md", "h1", 100, &[], &generate_docid("a.md"), None)
306309
.unwrap();
307310
let f2 = store
308-
.insert_file("b.md", "h2", 100, &[], &generate_docid("b.md"))
311+
.insert_file("b.md", "h2", 100, &[], &generate_docid("b.md"), None)
309312
.unwrap();
310313
let f3 = store
311-
.insert_file("shared.md", "h3", 100, &[], &generate_docid("shared.md"))
314+
.insert_file(
315+
"shared.md",
316+
"h3",
317+
100,
318+
&[],
319+
&generate_docid("shared.md"),
320+
None,
321+
)
312322
.unwrap();
313323

314324
store.insert_edge(f1, f3, "wikilink").unwrap();
@@ -349,7 +359,9 @@ mod tests {
349359
#[test]
350360
fn test_graph_expand_empty_graph() {
351361
let store = Store::open_memory().unwrap();
352-
let f1 = store.insert_file("a.md", "h1", 100, &[], "aaa111").unwrap();
362+
let f1 = store
363+
.insert_file("a.md", "h1", 100, &[], "aaa111", None)
364+
.unwrap();
353365

354366
let seeds = vec![RankedResult {
355367
file_path: "a.md".into(),
@@ -374,6 +386,7 @@ mod tests {
374386
100,
375387
&["rust".into(), "cli".into()],
376388
&generate_docid("seed.md"),
389+
None,
377390
)
378391
.unwrap();
379392
let f2 = store
@@ -383,6 +396,7 @@ mod tests {
383396
100,
384397
&["rust".into()],
385398
&generate_docid("linked.md"),
399+
None,
386400
)
387401
.unwrap();
388402

0 commit comments

Comments
 (0)