@@ -79,6 +79,17 @@ pub struct PlacementCorrection {
7979 pub corrected_at : String ,
8080}
8181
82+ /// A fact about the user's identity, inferred or stated (v1.6).
83+ #[ derive( Debug , Clone , serde:: Serialize ) ]
84+ pub struct IdentityFact {
85+ pub id : i64 ,
86+ pub tier : i64 ,
87+ pub key : String ,
88+ pub value : String ,
89+ pub source : Option < String > ,
90+ pub updated_at : String ,
91+ }
92+
8293/// Summary statistics for the store.
8394#[ derive( Debug ) ]
8495pub struct StoreStats {
@@ -357,6 +368,19 @@ impl Store {
357368 CREATE INDEX IF NOT EXISTS idx_migration_id ON migration_log(migration_id);" ,
358369 ) ?;
359370
371+ // Identity facts table (v1.6)
372+ self . conn . execute_batch (
373+ "CREATE TABLE IF NOT EXISTS identity_facts (
374+ id INTEGER PRIMARY KEY,
375+ tier INTEGER NOT NULL,
376+ key TEXT NOT NULL,
377+ value TEXT NOT NULL,
378+ source TEXT,
379+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
380+ UNIQUE(tier, key, value)
381+ );" ,
382+ ) ?;
383+
360384 Ok ( ( ) )
361385 }
362386
@@ -1614,6 +1638,50 @@ impl Store {
16141638 Ok ( ( ) )
16151639 }
16161640
1641+ // ── Identity Facts ───────────────────────────────────────────
1642+
1643+ pub fn upsert_identity_fact (
1644+ & self ,
1645+ tier : i64 ,
1646+ key : & str ,
1647+ value : & str ,
1648+ source : Option < & str > ,
1649+ ) -> Result < ( ) > {
1650+ self . conn . execute (
1651+ "INSERT INTO identity_facts (tier, key, value, source, updated_at)
1652+ VALUES (?1, ?2, ?3, ?4, datetime('now'))
1653+ ON CONFLICT(tier, key, value) DO UPDATE SET
1654+ source = excluded.source,
1655+ updated_at = datetime('now')" ,
1656+ rusqlite:: params![ tier, key, value, source] ,
1657+ ) ?;
1658+ Ok ( ( ) )
1659+ }
1660+
1661+ pub fn get_identity_facts ( & self , tier : i64 ) -> Result < Vec < IdentityFact > > {
1662+ let mut stmt = self . conn . prepare (
1663+ "SELECT id, tier, key, value, source, updated_at
1664+ FROM identity_facts WHERE tier = ?1 ORDER BY key, value" ,
1665+ ) ?;
1666+ let rows = stmt. query_map ( rusqlite:: params![ tier] , |row| {
1667+ Ok ( IdentityFact {
1668+ id : row. get ( 0 ) ?,
1669+ tier : row. get ( 1 ) ?,
1670+ key : row. get ( 2 ) ?,
1671+ value : row. get ( 3 ) ?,
1672+ source : row. get ( 4 ) ?,
1673+ updated_at : row. get ( 5 ) ?,
1674+ } )
1675+ } ) ?;
1676+ Ok ( rows. collect :: < std:: result:: Result < Vec < _ > , _ > > ( ) ?)
1677+ }
1678+
1679+ pub fn clear_identity_facts ( & self , tier : i64 ) -> Result < ( ) > {
1680+ self . conn
1681+ . execute ( "DELETE FROM identity_facts WHERE tier = ?1" , rusqlite:: params![ tier] ) ?;
1682+ Ok ( ( ) )
1683+ }
1684+
16171685 // ── Helpers ─────────────────────────────────────────────────
16181686
16191687 pub fn next_vector_id ( & self ) -> Result < u64 > {
@@ -3525,4 +3593,41 @@ mod tests {
35253593 assert ! ( record. is_some( ) ) ;
35263594 assert_eq ! ( record. unwrap( ) . content_hash, "hash1" ) ;
35273595 }
3596+
3597+ #[ test]
3598+ fn test_insert_and_get_identity_facts ( ) {
3599+ let store = Store :: open_memory ( ) . unwrap ( ) ;
3600+ store. upsert_identity_fact ( 0 , "name" , "Test User" , None ) . unwrap ( ) ;
3601+ store. upsert_identity_fact ( 1 , "active_project" , "Project A" , Some ( "01-Projects/a.md" ) ) . unwrap ( ) ;
3602+ store. upsert_identity_fact ( 1 , "active_project" , "Project B" , Some ( "01-Projects/b.md" ) ) . unwrap ( ) ;
3603+
3604+ let l0 = store. get_identity_facts ( 0 ) . unwrap ( ) ;
3605+ assert_eq ! ( l0. len( ) , 1 ) ;
3606+ assert_eq ! ( l0[ 0 ] . key, "name" ) ;
3607+ assert_eq ! ( l0[ 0 ] . value, "Test User" ) ;
3608+
3609+ let l1 = store. get_identity_facts ( 1 ) . unwrap ( ) ;
3610+ assert_eq ! ( l1. len( ) , 2 ) ;
3611+ }
3612+
3613+ #[ test]
3614+ fn test_upsert_identity_fact_replaces ( ) {
3615+ let store = Store :: open_memory ( ) . unwrap ( ) ;
3616+ store. upsert_identity_fact ( 0 , "name" , "Old Name" , None ) . unwrap ( ) ;
3617+ store. upsert_identity_fact ( 0 , "name" , "New Name" , None ) . unwrap ( ) ;
3618+
3619+ let facts = store. get_identity_facts ( 0 ) . unwrap ( ) ;
3620+ assert_eq ! ( facts. len( ) , 2 ) ; // Different values = different rows
3621+ }
3622+
3623+ #[ test]
3624+ fn test_clear_identity_facts_by_tier ( ) {
3625+ let store = Store :: open_memory ( ) . unwrap ( ) ;
3626+ store. upsert_identity_fact ( 0 , "name" , "User" , None ) . unwrap ( ) ;
3627+ store. upsert_identity_fact ( 1 , "active_project" , "P1" , None ) . unwrap ( ) ;
3628+ store. clear_identity_facts ( 1 ) . unwrap ( ) ;
3629+
3630+ assert_eq ! ( store. get_identity_facts( 0 ) . unwrap( ) . len( ) , 1 ) ;
3631+ assert_eq ! ( store. get_identity_facts( 1 ) . unwrap( ) . len( ) , 0 ) ;
3632+ }
35283633}
0 commit comments