@@ -56,6 +56,23 @@ pub struct FolderInfo {
5656 pub note_count : usize ,
5757}
5858
59+ #[ derive( Debug , Serialize ) ]
60+ pub struct PersonContext {
61+ pub name : String ,
62+ pub note : Option < NoteContent > ,
63+ pub mentioned_in : Vec < MentionInfo > ,
64+ pub linked_from : Vec < String > ,
65+ pub linked_to : Vec < String > ,
66+ pub total_chars : usize ,
67+ }
68+
69+ #[ derive( Debug , Serialize ) ]
70+ pub struct MentionInfo {
71+ pub path : String ,
72+ pub docid : Option < String > ,
73+ pub snippet : String ,
74+ }
75+
5976// ---------------------------------------------------------------------------
6077// Helpers
6178// ---------------------------------------------------------------------------
@@ -233,6 +250,100 @@ pub fn vault_map(params: &ContextParams) -> Result<VaultMap> {
233250 } )
234251}
235252
253+ /// Build a person context bundle: note content, mentions, wikilink connections.
254+ pub fn context_who ( params : & ContextParams , name : & str ) -> Result < PersonContext > {
255+ let name_md = format ! ( "{}.md" , name) ;
256+ let name_lower = name_md. to_lowercase ( ) ;
257+ let all_files = params. store . get_all_files ( ) ?;
258+ let person_file = all_files. iter ( ) . find ( |f| {
259+ let basename = f. path . rsplit ( '/' ) . next ( ) . unwrap_or ( & f. path ) . to_lowercase ( ) ;
260+ basename == name_lower
261+ } ) ;
262+
263+ let ( note, person_id) = if let Some ( pf) = person_file {
264+ let n = context_read ( params, & pf. path ) ?;
265+ ( Some ( n) , Some ( pf. id ) )
266+ } else {
267+ ( None , None )
268+ } ;
269+
270+ let mut mentioned_in = Vec :: new ( ) ;
271+ let mut linked_from = Vec :: new ( ) ;
272+ let mut linked_to = Vec :: new ( ) ;
273+
274+ if let Some ( pid) = person_id {
275+ // Mention edges
276+ let mentions = params. store . get_incoming ( pid, Some ( "mention" ) ) ?;
277+ for ( fid, _) in & mentions {
278+ if let Some ( path) = params. store . get_file_path_by_id ( * fid) . ok ( ) . flatten ( ) {
279+ let docid = params
280+ . store
281+ . get_file_by_id ( * fid)
282+ . ok ( )
283+ . flatten ( )
284+ . and_then ( |f| f. docid ) ;
285+ let snippet = get_mention_snippet ( params, * fid, name) ;
286+ mentioned_in. push ( MentionInfo {
287+ path,
288+ docid,
289+ snippet,
290+ } ) ;
291+ }
292+ }
293+ // Wikilink edges
294+ let incoming_wl = params. store . get_incoming ( pid, Some ( "wikilink" ) ) ?;
295+ for ( fid, _) in & incoming_wl {
296+ if let Some ( path) = params. store . get_file_path_by_id ( * fid) . ok ( ) . flatten ( ) {
297+ linked_from. push ( path) ;
298+ }
299+ }
300+ let outgoing_wl = params. store . get_outgoing ( pid, Some ( "wikilink" ) ) ?;
301+ for ( fid, _) in & outgoing_wl {
302+ if let Some ( path) = params. store . get_file_path_by_id ( * fid) . ok ( ) . flatten ( ) {
303+ linked_to. push ( path) ;
304+ }
305+ }
306+ }
307+
308+ let total_chars = note. as_ref ( ) . map ( |n| n. char_count ) . unwrap_or ( 0 )
309+ + mentioned_in. iter ( ) . map ( |m| m. snippet . len ( ) ) . sum :: < usize > ( ) ;
310+
311+ Ok ( PersonContext {
312+ name : name. to_string ( ) ,
313+ note,
314+ mentioned_in,
315+ linked_from,
316+ linked_to,
317+ total_chars,
318+ } )
319+ }
320+
321+ /// Get a snippet from a file mentioning a name. Try FTS first, fall back to disk read.
322+ fn get_mention_snippet ( params : & ContextParams , file_id : i64 , name : & str ) -> String {
323+ if let Ok ( results) = params. store . fts_search ( name, 5 )
324+ && let Some ( r) = results. iter ( ) . find ( |r| r. file_id == file_id)
325+ {
326+ return r. snippet . clone ( ) ;
327+ }
328+ if let Some ( path) = params. store . get_file_path_by_id ( file_id) . ok ( ) . flatten ( ) {
329+ let full_path = params. vault_path . join ( & path) ;
330+ if let Ok ( content) = std:: fs:: read_to_string ( & full_path) {
331+ let name_lower = name. to_lowercase ( ) ;
332+ for line in content. lines ( ) {
333+ if line. to_lowercase ( ) . contains ( & name_lower) {
334+ let truncated: String = line. chars ( ) . take ( 200 ) . collect ( ) ;
335+ return if line. len ( ) > 200 {
336+ format ! ( "{}..." , truncated)
337+ } else {
338+ truncated
339+ } ;
340+ }
341+ }
342+ }
343+ }
344+ String :: new ( )
345+ }
346+
236347// ---------------------------------------------------------------------------
237348// Tests
238349// ---------------------------------------------------------------------------
@@ -383,4 +494,56 @@ mod tests {
383494 assert ! ( fm. is_empty( ) ) ;
384495 assert ! ( body. contains( "# Just content" ) ) ;
385496 }
497+
498+ #[ test]
499+ fn test_who_finds_person ( ) {
500+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
501+ let root = tmp. path ( ) . to_path_buf ( ) ;
502+ std:: fs:: create_dir_all ( root. join ( "People" ) ) . unwrap ( ) ;
503+ std:: fs:: write (
504+ root. join ( "People/John.md" ) ,
505+ "---\n aliases:\n - JN\n ---\n # John\n Developer." ,
506+ )
507+ . unwrap ( ) ;
508+ std:: fs:: write ( root. join ( "daily.md" ) , "# Daily\n Talked to John about Rust." ) . unwrap ( ) ;
509+
510+ let store = Store :: open_memory ( ) . unwrap ( ) ;
511+ let f1 = store
512+ . insert_file ( "People/John.md" , "h1" , 100 , & [ "person" . into ( ) ] , "aaa111" )
513+ . unwrap ( ) ;
514+ let f2 = store
515+ . insert_file ( "daily.md" , "h2" , 100 , & [ ] , "bbb222" )
516+ . unwrap ( ) ;
517+ store. insert_edge ( f2, f1, "mention" ) . unwrap ( ) ;
518+ store
519+ . insert_chunk ( f2, "# Daily" , "Talked to John about Rust." , 10 , 20 )
520+ . unwrap ( ) ;
521+ store
522+ . insert_fts_chunk ( f2, 0 , "Talked to John about Rust." )
523+ . unwrap ( ) ;
524+
525+ let params = ContextParams {
526+ store : & store,
527+ vault_path : & root,
528+ profile : None ,
529+ } ;
530+ let person = context_who ( & params, "John" ) . unwrap ( ) ;
531+ assert ! ( person. note. is_some( ) ) ;
532+ assert_eq ! ( person. name, "John" ) ;
533+ assert_eq ! ( person. mentioned_in. len( ) , 1 ) ;
534+ assert ! ( person. mentioned_in[ 0 ] . path. contains( "daily" ) ) ;
535+ }
536+
537+ #[ test]
538+ fn test_who_not_found ( ) {
539+ let ( _tmp, store, root) = setup_vault ( ) ;
540+ let params = ContextParams {
541+ store : & store,
542+ vault_path : & root,
543+ profile : None ,
544+ } ;
545+ let person = context_who ( & params, "NonExistent" ) . unwrap ( ) ;
546+ assert ! ( person. note. is_none( ) ) ;
547+ assert ! ( person. mentioned_in. is_empty( ) ) ;
548+ }
386549}
0 commit comments