Skip to main content

ai_memory/
models.rs

1// Copyright 2026 AlphaOne LLC
2// SPDX-License-Identifier: Apache-2.0
3
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7/// Memory tier — mirrors human memory systems.
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
9#[serde(rename_all = "snake_case")]
10pub enum Tier {
11    Short,
12    Mid,
13    Long,
14}
15
16impl Tier {
17    pub fn as_str(&self) -> &'static str {
18        match self {
19            Self::Short => "short",
20            Self::Mid => "mid",
21            Self::Long => "long",
22        }
23    }
24
25    pub fn from_str(s: &str) -> Option<Self> {
26        match s {
27            "short" => Some(Self::Short),
28            "mid" => Some(Self::Mid),
29            "long" => Some(Self::Long),
30            _ => None,
31        }
32    }
33
34    /// Numeric rank for tier comparison: Short=0, Mid=1, Long=2.
35    #[cfg(test)]
36    pub fn rank(&self) -> u8 {
37        match self {
38            Self::Short => 0,
39            Self::Mid => 1,
40            Self::Long => 2,
41        }
42    }
43
44    pub fn default_ttl_secs(&self) -> Option<i64> {
45        match self {
46            Self::Short => Some(6 * 3600),
47            Self::Mid => Some(7 * 24 * 3600),
48            Self::Long => None,
49        }
50    }
51}
52
53impl std::fmt::Display for Tier {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        f.write_str(self.as_str())
56    }
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct Memory {
61    pub id: String,
62    pub tier: Tier,
63    pub namespace: String,
64    pub title: String,
65    pub content: String,
66    pub tags: Vec<String>,
67    pub priority: i32,
68    /// 0.0-1.0 — how certain is this memory
69    pub confidence: f64,
70    /// Who/what created this: "user", "claude", "hook", "api", "import"
71    pub source: String,
72    pub access_count: i64,
73    pub created_at: String,
74    pub updated_at: String,
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub last_accessed_at: Option<String>,
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub expires_at: Option<String>,
79    #[serde(default = "default_metadata")]
80    pub metadata: Value,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct MemoryLink {
85    pub source_id: String,
86    pub target_id: String,
87    pub relation: String, // "related_to", "supersedes", "contradicts", "derived_from"
88    pub created_at: String,
89}
90
91#[derive(Debug, Deserialize)]
92pub struct CreateMemory {
93    #[serde(default = "default_tier")]
94    pub tier: Tier,
95    #[serde(default = "default_namespace")]
96    pub namespace: String,
97    pub title: String,
98    pub content: String,
99    #[serde(default)]
100    pub tags: Vec<String>,
101    #[serde(default = "default_priority")]
102    pub priority: i32,
103    #[serde(default = "default_confidence")]
104    pub confidence: f64,
105    #[serde(default = "default_source")]
106    pub source: String,
107    #[serde(default)]
108    pub expires_at: Option<String>,
109    #[serde(default)]
110    pub ttl_secs: Option<i64>,
111    #[serde(default = "default_metadata")]
112    pub metadata: Value,
113    /// Optional agent identifier. When unset, the server resolves a default
114    /// via `crate::identity` (NHI-hardened precedence chain).
115    #[serde(default)]
116    pub agent_id: Option<String>,
117    /// Optional visibility scope (Task 1.5). One of `VALID_SCOPES`. When
118    /// unset, treated as `private` by the query layer.
119    #[serde(default)]
120    pub scope: Option<String>,
121}
122
123fn default_tier() -> Tier {
124    Tier::Mid
125}
126fn default_namespace() -> String {
127    "global".to_string()
128}
129fn default_priority() -> i32 {
130    5
131}
132fn default_confidence() -> f64 {
133    1.0
134}
135fn default_source() -> String {
136    "api".to_string()
137}
138pub fn default_metadata() -> Value {
139    Value::Object(serde_json::Map::new())
140}
141
142#[derive(Debug, Deserialize)]
143pub struct UpdateMemory {
144    pub title: Option<String>,
145    pub content: Option<String>,
146    pub tier: Option<Tier>,
147    pub namespace: Option<String>,
148    pub tags: Option<Vec<String>>,
149    pub priority: Option<i32>,
150    pub confidence: Option<f64>,
151    pub expires_at: Option<String>,
152    pub metadata: Option<Value>,
153}
154
155#[derive(Debug, Deserialize)]
156pub struct SearchQuery {
157    pub q: String,
158    #[serde(default)]
159    pub namespace: Option<String>,
160    #[serde(default)]
161    pub tier: Option<Tier>,
162    #[serde(default = "default_limit")]
163    pub limit: Option<usize>,
164    #[serde(default)]
165    pub min_priority: Option<i32>,
166    #[serde(default)]
167    pub since: Option<String>,
168    #[serde(default)]
169    pub until: Option<String>,
170    #[serde(default)]
171    pub tags: Option<String>, // comma-separated
172    /// Filter by `metadata.agent_id` (exact match).
173    #[serde(default)]
174    pub agent_id: Option<String>,
175    /// Task 1.5 visibility: the querying agent's namespace position.
176    /// When set, results are filtered per `metadata.scope` rules.
177    #[serde(default)]
178    pub as_agent: Option<String>,
179}
180
181#[allow(clippy::unnecessary_wraps)]
182fn default_limit() -> Option<usize> {
183    Some(20)
184}
185
186#[derive(Debug, Deserialize)]
187pub struct ListQuery {
188    #[serde(default)]
189    pub namespace: Option<String>,
190    #[serde(default)]
191    pub tier: Option<Tier>,
192    #[serde(default = "default_limit")]
193    pub limit: Option<usize>,
194    #[serde(default)]
195    pub offset: Option<usize>,
196    #[serde(default)]
197    pub min_priority: Option<i32>,
198    #[serde(default)]
199    pub since: Option<String>,
200    #[serde(default)]
201    pub until: Option<String>,
202    #[serde(default)]
203    pub tags: Option<String>,
204    /// Filter by `metadata.agent_id` (exact match).
205    #[serde(default)]
206    pub agent_id: Option<String>,
207}
208
209#[derive(Debug, Deserialize)]
210pub struct RecallQuery {
211    pub context: Option<String>,
212    #[serde(default)]
213    pub namespace: Option<String>,
214    #[serde(default = "default_recall_limit")]
215    pub limit: Option<usize>,
216    #[serde(default)]
217    pub tags: Option<String>,
218    #[serde(default)]
219    pub since: Option<String>,
220    #[serde(default)]
221    pub until: Option<String>,
222    /// Task 1.5 visibility filtering.
223    #[serde(default)]
224    pub as_agent: Option<String>,
225    /// Task 1.11 — context-budget-aware recall. When set, return the
226    /// top-scored memories whose cumulative estimated tokens fit within
227    /// this budget.
228    #[serde(default)]
229    pub budget_tokens: Option<usize>,
230}
231
232#[allow(clippy::unnecessary_wraps)]
233fn default_recall_limit() -> Option<usize> {
234    Some(10)
235}
236
237#[derive(Debug, Deserialize)]
238pub struct RecallBody {
239    pub context: String,
240    #[serde(default)]
241    pub namespace: Option<String>,
242    #[serde(default = "default_recall_limit")]
243    pub limit: Option<usize>,
244    #[serde(default)]
245    pub tags: Option<String>,
246    #[serde(default)]
247    pub since: Option<String>,
248    #[serde(default)]
249    pub until: Option<String>,
250    /// Task 1.5 visibility filtering.
251    #[serde(default)]
252    pub as_agent: Option<String>,
253    /// Task 1.11 — context-budget-aware recall.
254    #[serde(default)]
255    pub budget_tokens: Option<usize>,
256}
257
258#[derive(Debug, Deserialize)]
259pub struct LinkBody {
260    pub source_id: String,
261    pub target_id: String,
262    #[serde(default = "default_relation")]
263    pub relation: String,
264}
265
266fn default_relation() -> String {
267    "related_to".to_string()
268}
269
270#[derive(Debug, Deserialize)]
271pub struct ForgetQuery {
272    #[serde(default)]
273    pub namespace: Option<String>,
274    #[serde(default)]
275    pub pattern: Option<String>, // FTS pattern
276    #[serde(default)]
277    pub tier: Option<Tier>,
278}
279
280#[derive(Debug, Serialize)]
281pub struct Stats {
282    pub total: usize,
283    pub by_tier: Vec<TierCount>,
284    pub by_namespace: Vec<NamespaceCount>,
285    pub expiring_soon: usize,
286    pub links_count: usize,
287    pub db_size_bytes: u64,
288}
289
290#[derive(Debug, Serialize)]
291pub struct TierCount {
292    pub tier: String,
293    pub count: usize,
294}
295
296#[derive(Debug, Serialize)]
297pub struct NamespaceCount {
298    pub namespace: String,
299    pub count: usize,
300}
301
302/// One node of the hierarchical namespace tree returned by
303/// `memory_get_taxonomy` (Pillar 1 / Stream A).
304///
305/// `count` is the number of memories at *exactly* this namespace;
306/// `subtree_count` is the count of memories at this node plus every
307/// descendant the depth limit allowed us to expand. Children are sorted
308/// alphabetically by `name` so callers get a stable rendering order.
309#[derive(Debug, Clone, Serialize)]
310pub struct TaxonomyNode {
311    /// Full namespace path of this node. Empty string for the synthetic
312    /// root when no `namespace_prefix` is supplied.
313    pub namespace: String,
314    /// Last `/`-delimited segment of `namespace` (display label). Empty
315    /// for the synthetic root.
316    pub name: String,
317    /// Memories whose namespace equals this node's `namespace`.
318    pub count: usize,
319    /// Memories at this node plus all descendants visible within the
320    /// requested `depth`. Memories beneath the depth cutoff still
321    /// contribute to the `subtree_count` of the boundary ancestor.
322    pub subtree_count: usize,
323    /// Direct child nodes, sorted alphabetically by `name`.
324    pub children: Vec<TaxonomyNode>,
325}
326
327/// Result envelope returned by `db::get_taxonomy`.
328///
329/// `total_count` is the global memory count for the prefix (independent
330/// of `depth`/`limit` truncation) so callers can render an honest
331/// "X memories in N namespaces" header even when the tree was
332/// truncated. `truncated` is set when the `limit` parameter forced us
333/// to drop input rows when assembling the tree.
334#[derive(Debug, Clone, Serialize)]
335pub struct Taxonomy {
336    pub tree: TaxonomyNode,
337    pub total_count: usize,
338    pub truncated: bool,
339}
340
341/// One nearest-neighbor result from a `memory_check_duplicate` lookup
342/// (Pillar 2 / Stream D). `similarity` is the cosine similarity in
343/// `[-1.0, 1.0]`, rounded to three decimals at the response layer.
344#[derive(Debug, Clone, Serialize)]
345pub struct DuplicateMatch {
346    pub id: String,
347    pub title: String,
348    pub namespace: String,
349    pub similarity: f32,
350}
351
352/// Result envelope returned by `db::check_duplicate`.
353///
354/// `is_duplicate` is `nearest.similarity >= threshold`. `nearest` is
355/// `None` only when the candidate pool is empty (no embedded, live
356/// memories matched the namespace filter). When `is_duplicate` is true,
357/// `nearest.id` doubles as the suggested merge target — we surface it
358/// under that name in the JSON response so the contract stays explicit.
359#[derive(Debug, Clone, Serialize)]
360pub struct DuplicateCheck {
361    pub is_duplicate: bool,
362    pub threshold: f32,
363    pub nearest: Option<DuplicateMatch>,
364    pub candidates_scanned: usize,
365}
366
367/// Namespace reserved for agent registrations (Task 1.3).
368pub const AGENTS_NAMESPACE: &str = "_agents";
369
370/// Tag stamped on entity-typed memories so `(title, namespace)` can be
371/// shared across regular memories and entities without ambiguity (Pillar
372/// 2 / Stream B).
373pub const ENTITY_TAG: &str = "entity";
374
375/// Marker written to `metadata.kind` on entity-typed memories. The
376/// db layer keys entity lookups off this field so the alias resolver
377/// never returns a regular memory that happens to share a title with an
378/// entity registered later.
379pub const ENTITY_KIND: &str = "entity";
380
381/// Resolved entity record returned by `db::entity_get_by_alias` and
382/// embedded in the `db::entity_register` response (Pillar 2 / Stream B).
383/// `aliases` is the full alias set for the entity, ordered by
384/// `created_at ASC, alias ASC` for stable display.
385#[derive(Debug, Clone, Serialize)]
386pub struct EntityRecord {
387    pub entity_id: String,
388    pub canonical_name: String,
389    pub namespace: String,
390    pub aliases: Vec<String>,
391}
392
393/// Outcome of `db::entity_register`. `created` is `true` when a new
394/// entity memory was inserted, `false` when an existing entity was
395/// reused (idempotent re-registration that just merged new aliases into
396/// the existing record).
397#[derive(Debug, Clone, Serialize)]
398pub struct EntityRegistration {
399    pub entity_id: String,
400    pub canonical_name: String,
401    pub namespace: String,
402    pub aliases: Vec<String>,
403    pub created: bool,
404}
405
406/// Single row returned by `db::kg_timeline` (Pillar 2 / Stream C).
407///
408/// Captures one outbound assertion from a source memory: the
409/// `target_id` and its `relation`, the temporal-validity window
410/// (`valid_from` / `valid_until`), the agent that observed it
411/// (`observed_by`), and the target's display fields (`title`,
412/// `target_namespace`) for caller convenience. `valid_from` is the
413/// authoritative ordering key — events with NULL `valid_from` are
414/// excluded from the timeline by the query.
415#[derive(Debug, Clone, Serialize)]
416pub struct KgTimelineEvent {
417    pub target_id: String,
418    pub relation: String,
419    pub valid_from: String,
420    pub valid_until: Option<String>,
421    pub observed_by: Option<String>,
422    pub title: String,
423    pub target_namespace: String,
424}
425
426/// One node returned by `db::kg_query` (Pillar 2 / Stream C —
427/// `memory_kg_query`). Each node represents a memory reachable from the
428/// query's source through one outbound link, carrying the link's
429/// temporal-validity columns plus the target memory's display fields and
430/// the traversal path. `depth` is the actual number of hops from the
431/// source (1..=`KG_QUERY_MAX_SUPPORTED_DEPTH`); `path` is the
432/// `src->mid->target` chain as discovered by the recursive CTE.
433#[derive(Debug, Clone, Serialize)]
434pub struct KgQueryNode {
435    pub target_id: String,
436    pub relation: String,
437    pub valid_from: Option<String>,
438    pub valid_until: Option<String>,
439    pub observed_by: Option<String>,
440    pub title: String,
441    pub target_namespace: String,
442    pub depth: usize,
443    pub path: String,
444}
445
446// ---------------------------------------------------------------------------
447// Task 1.9 — Governance Enforcement
448// ---------------------------------------------------------------------------
449
450/// The outcome of a governance check. Callers MAY execute on `Allow`,
451/// MUST reject on `Deny`, and SHOULD queue + return the `pending_id` on
452/// `Pending`.
453#[derive(Debug, Clone, PartialEq, Eq)]
454pub enum GovernanceDecision {
455    /// Allowed; proceed with the action.
456    Allow,
457    /// Denied; surface the reason to the caller.
458    Deny(String),
459    /// Queued for approval; the caller receives the new `pending_id`.
460    Pending(String),
461}
462
463/// Actions that governance gates. Used as the `action_type` column value in
464/// `pending_actions` and as the discriminator for enforcement calls.
465#[derive(Debug, Clone, Copy, PartialEq, Eq)]
466pub enum GovernedAction {
467    Store,
468    Delete,
469    Promote,
470}
471
472impl GovernedAction {
473    #[must_use]
474    pub fn as_str(self) -> &'static str {
475        match self {
476            Self::Store => "store",
477            Self::Delete => "delete",
478            Self::Promote => "promote",
479        }
480    }
481}
482
483/// A single approval vote recorded on a consensus-gated pending action (Task 1.10).
484#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
485pub struct Approval {
486    pub agent_id: String,
487    pub approved_at: String,
488}
489
490/// Row returned by `db::list_pending_actions`.
491#[derive(Debug, Clone, Serialize, Deserialize)]
492pub struct PendingAction {
493    pub id: String,
494    pub action_type: String,
495    pub memory_id: Option<String>,
496    pub namespace: String,
497    pub payload: Value,
498    pub requested_by: String,
499    pub requested_at: String,
500    pub status: String,
501    #[serde(skip_serializing_if = "Option::is_none")]
502    pub decided_by: Option<String>,
503    #[serde(skip_serializing_if = "Option::is_none")]
504    pub decided_at: Option<String>,
505    /// Task 1.10: consensus vote log. Empty for Human/Agent paths.
506    #[serde(default)]
507    pub approvals: Vec<Approval>,
508}
509
510/// v0.6.2 (S34): a pending-action decision (approve / reject) the originating
511/// node wants propagated to peers so callers on any peer see consistent state
512/// (approve/reject on node-2 → decision must reach node-1 etc.).
513///
514/// Shipped as an additive `sync_push.pending_decisions` field. Peers apply
515/// via `db::decide_pending_action`; already-decided rows are a no-op.
516#[derive(Debug, Clone, Serialize, Deserialize)]
517pub struct PendingDecision {
518    pub id: String,
519    pub approved: bool,
520    pub decider: String,
521}
522
523/// v0.6.2 (S35): a namespace-standard metadata row the originating node wants
524/// propagated to peers. `set_namespace_standard` writes to `namespace_meta`
525/// locally; without federation, a peer sees the standard memory (fanned out
526/// via `broadcast_store_quorum`) but not the `(namespace, standard_id,
527/// parent_namespace)` tuple, so inheritance-chain walks on the peer fall
528/// back to `auto_detect_parent` and can miss an explicit parent link.
529///
530/// Shipped as an additive `sync_push.namespace_meta` field. Peers apply
531/// via `db::set_namespace_standard(conn, namespace, standard_id,
532/// parent_namespace.as_deref())`.
533#[derive(Debug, Clone, Serialize, Deserialize)]
534pub struct NamespaceMetaEntry {
535    pub namespace: String,
536    pub standard_id: String,
537    #[serde(default, skip_serializing_if = "Option::is_none")]
538    pub parent_namespace: Option<String>,
539    #[serde(default)]
540    pub updated_at: String,
541}
542
543// ---------------------------------------------------------------------------
544// Task 1.8 — Governance Metadata
545// ---------------------------------------------------------------------------
546
547/// Who is permitted to perform a governed action.
548///
549/// Stored inside a namespace standard's `metadata.governance` and consulted
550/// by Task 1.9 (enforcement) + Task 1.10 (approver types). Task 1.8 only
551/// defines the shape + validation — no runtime enforcement yet.
552#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
553#[serde(rename_all = "snake_case")]
554pub enum GovernanceLevel {
555    /// Any caller may perform the action (no gate).
556    Any,
557    /// Caller must be a registered agent (see Task 1.3 `_agents` namespace).
558    Registered,
559    /// Only the memory's original `metadata.agent_id` owner may perform the action.
560    Owner,
561    /// Action requires explicit approval by an `ApproverType` (handled in 1.9 + 1.10).
562    Approve,
563}
564
565impl GovernanceLevel {
566    /// Human-readable tag used by logs and error messages.
567    /// Consumed by Task 1.9 enforcement path.
568    #[allow(dead_code)]
569    #[must_use]
570    pub fn as_str(&self) -> &'static str {
571        match self {
572            Self::Any => "any",
573            Self::Registered => "registered",
574            Self::Owner => "owner",
575            Self::Approve => "approve",
576        }
577    }
578}
579
580/// Who approves actions gated by [`GovernanceLevel::Approve`].
581///
582/// Serialized representation (externally-tagged, `snake_case`):
583///
584/// - [`Self::Human`] → `"human"`
585/// - [`Self::Agent`] → `{"agent": "alice"}`
586/// - [`Self::Consensus`] → `{"consensus": 3}`
587#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
588#[serde(rename_all = "snake_case")]
589pub enum ApproverType {
590    /// Human approval required (interactive or out-of-band).
591    Human,
592    /// Specific registered agent must approve, identified by `agent_id`.
593    Agent(String),
594    /// Consensus of N approvers (any mix of human/agent registrations).
595    Consensus(u32),
596}
597
598impl ApproverType {
599    /// Discriminator tag for logs / telemetry.
600    /// Consumed by Task 1.10 approver-types path.
601    #[allow(dead_code)]
602    #[must_use]
603    pub fn kind(&self) -> &'static str {
604        match self {
605            Self::Human => "human",
606            Self::Agent(_) => "agent",
607            Self::Consensus(_) => "consensus",
608        }
609    }
610}
611
612/// Governance policy attached to a namespace's standard memory
613/// (stored in `metadata.governance`).
614///
615/// Default policy when a standard has no `metadata.governance`:
616/// `{ write: Any, promote: Any, delete: Owner, approver: Human }`.
617///
618/// v0.6.2 (S34 defensive): `promote`, `delete`, and `approver` carry
619/// `#[serde(default)]` so partial-policy payloads (a common shape for
620/// operator CLIs / test harnesses that only care about `write`) round-trip
621/// instead of 400-ing out on missing fields. `write` remains required —
622/// it's the core knob a policy is attempting to set.
623#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
624pub struct GovernancePolicy {
625    pub write: GovernanceLevel,
626    #[serde(default = "default_promote_level")]
627    pub promote: GovernanceLevel,
628    #[serde(default = "default_delete_level")]
629    pub delete: GovernanceLevel,
630    #[serde(default = "default_approver")]
631    pub approver: ApproverType,
632}
633
634fn default_promote_level() -> GovernanceLevel {
635    GovernanceLevel::Any
636}
637
638fn default_delete_level() -> GovernanceLevel {
639    GovernanceLevel::Owner
640}
641
642fn default_approver() -> ApproverType {
643    ApproverType::Human
644}
645
646impl Default for GovernancePolicy {
647    fn default() -> Self {
648        Self {
649            write: GovernanceLevel::Any,
650            promote: default_promote_level(),
651            delete: default_delete_level(),
652            approver: default_approver(),
653        }
654    }
655}
656
657impl GovernancePolicy {
658    /// Parse a policy out of a `metadata.governance` JSON value. Returns
659    /// `None` when the field is missing/null. Parse errors propagate so
660    /// callers can surface them to the user instead of silently defaulting.
661    pub fn from_metadata(metadata: &Value) -> Option<Result<Self, serde_json::Error>> {
662        let gov = metadata.get("governance")?;
663        if gov.is_null() {
664            return None;
665        }
666        Some(serde_json::from_value(gov.clone()))
667    }
668}
669
670/// Closed set of visibility scopes stamped into `metadata.scope` (Task 1.5).
671/// Controls which agents can see a memory via hierarchical namespace matching.
672/// Memories without a `scope` field are treated as `private` by the query layer.
673pub const VALID_SCOPES: &[&str] = &["private", "team", "unit", "org", "collective"];
674
675/// Closed set of agent types. Extend carefully — values are persisted.
676pub const VALID_AGENT_TYPES: &[&str] = &[
677    "ai:claude-opus-4.6",
678    "ai:claude-opus-4.7",
679    "ai:codex-5.4",
680    "ai:grok-4.2",
681    "human",
682    "system",
683];
684
685#[derive(Debug, Deserialize)]
686pub struct RegisterAgentBody {
687    pub agent_id: String,
688    pub agent_type: String,
689    #[serde(default)]
690    pub capabilities: Option<Vec<String>>,
691}
692
693#[derive(Debug, Serialize)]
694pub struct AgentRegistration {
695    pub agent_id: String,
696    pub agent_type: String,
697    pub capabilities: Vec<String>,
698    pub registered_at: String,
699    pub last_seen_at: String,
700}
701
702/// Phase 3 foundation (issue #224): vector clock tracking the latest
703/// `updated_at` this peer has seen from each known remote peer.
704///
705/// Entries are populated lazily — both on HTTP `/sync/push` (receiver
706/// records the sender's latest `updated_at`) and on HTTP `/sync/since`
707/// (sender advances `last_pulled_at`). Full CRDT-lite merge rules using
708/// the clock are **not** in the v0.6.0 GA foundation; they land in a
709/// follow-up PR under issue #224 Task 3a.1. The foundation ships the
710/// wire format so adding the merge semantics later does not force a
711/// schema migration.
712#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
713pub struct VectorClock {
714    /// Map of peer `agent_id` -> latest RFC3339 `updated_at` seen from
715    /// that peer. A peer absent from the map is equivalent to
716    /// "never-seen-anything." Encoded as a JSON object on the wire.
717    #[serde(default)]
718    pub entries: std::collections::BTreeMap<String, String>,
719}
720
721impl VectorClock {
722    /// Advance this clock to include `peer_id`'s latest seen timestamp.
723    /// Monotonic — an older timestamp never overwrites a newer one.
724    #[allow(dead_code)] // Consumed by Task 3a.1 CRDT-lite merge (issue #224).
725    pub fn observe(&mut self, peer_id: &str, at: &str) {
726        self.entries
727            .entry(peer_id.to_string())
728            .and_modify(|existing| {
729                if at > existing.as_str() {
730                    *existing = at.to_string();
731                }
732            })
733            .or_insert_with(|| at.to_string());
734    }
735
736    /// Look up the latest timestamp this clock has from `peer_id`.
737    #[must_use]
738    #[allow(dead_code)] // Consumed by Task 3a.1 CRDT-lite merge (issue #224).
739    pub fn latest_from(&self, peer_id: &str) -> Option<&str> {
740        self.entries.get(peer_id).map(String::as_str)
741    }
742}
743
744/// Phase 3 foundation: one row of the `sync_state` table serialised for
745/// diagnostic / API responses.
746#[allow(dead_code)] // Consumed by Task 3b.2 sync diagnostics API (issue #224).
747#[derive(Debug, Clone, Serialize, Deserialize)]
748pub struct SyncStateEntry {
749    pub agent_id: String,
750    pub peer_id: String,
751    pub last_seen_at: String,
752    pub last_pulled_at: String,
753}
754
755pub const MAX_CONTENT_SIZE: usize = 65_536;
756
757/// Maximum number of path segments in a hierarchical namespace (Task 1.4).
758/// `alphaone/engineering/platform/team/squad/pod/role/agent` = 8 levels.
759pub const MAX_NAMESPACE_DEPTH: usize = 8;
760
761/// Number of `/`-delimited segments in a namespace path.
762///
763/// Flat namespaces (`"global"`, `"ai-memory"`) return `1`. An empty string
764/// returns `0`.
765///
766/// # Examples
767/// ```
768/// # use ai_memory::models::namespace_depth;
769/// assert_eq!(namespace_depth("global"), 1);
770/// assert_eq!(namespace_depth("alphaone/engineering"), 2);
771/// assert_eq!(namespace_depth("alphaone/engineering/platform"), 3);
772/// ```
773#[must_use]
774pub fn namespace_depth(ns: &str) -> usize {
775    if ns.is_empty() {
776        return 0;
777    }
778    ns.split('/').filter(|s| !s.is_empty()).count()
779}
780
781/// Parent of a hierarchical namespace, or `None` for flat / empty inputs.
782///
783/// Part of the Task 1.4 hierarchical-namespace API. Consumed by Tasks 1.5
784/// (visibility rules), 1.6 (N-level inheritance), 1.7 (vertical promotion),
785/// and 1.12 (hierarchy-aware recall).
786#[allow(dead_code)]
787///
788/// Parent of `"a/b/c"` is `"a/b"`. Parent of `"flat"` is `None` (a flat
789/// namespace has no parent). Parent of `""` is `None`.
790///
791/// # Examples
792/// ```
793/// # use ai_memory::models::namespace_parent;
794/// assert_eq!(namespace_parent("alphaone/engineering/platform"), Some("alphaone/engineering".to_string()));
795/// assert_eq!(namespace_parent("alphaone"), None);
796/// assert_eq!(namespace_parent(""), None);
797/// ```
798#[must_use]
799pub fn namespace_parent(ns: &str) -> Option<String> {
800    ns.rsplit_once('/').map(|(parent, _)| parent.to_string())
801}
802
803/// Ancestors of a namespace, ordered most-specific-first (including the
804/// namespace itself as the first element).
805///
806/// Part of the Task 1.4 hierarchical-namespace API. Consumed by Tasks 1.6
807/// (N-level rule inheritance) and 1.12 (hierarchy-aware recall scoring).
808#[allow(dead_code)]
809///
810/// For `"a/b/c"` returns `["a/b/c", "a/b", "a"]`. For a flat namespace
811/// returns a single-element vec containing the namespace. For an empty
812/// input returns an empty vec.
813///
814/// # Examples
815/// ```
816/// # use ai_memory::models::namespace_ancestors;
817/// assert_eq!(
818///     namespace_ancestors("alphaone/engineering/platform"),
819///     vec!["alphaone/engineering/platform", "alphaone/engineering", "alphaone"]
820/// );
821/// assert_eq!(namespace_ancestors("global"), vec!["global"]);
822/// assert!(namespace_ancestors("").is_empty());
823/// ```
824#[must_use]
825pub fn namespace_ancestors(ns: &str) -> Vec<String> {
826    if ns.is_empty() {
827        return Vec::new();
828    }
829    let mut out = Vec::with_capacity(namespace_depth(ns));
830    let mut current = ns.to_string();
831    loop {
832        out.push(current.clone());
833        match namespace_parent(&current) {
834            Some(p) if !p.is_empty() => current = p,
835            _ => break,
836        }
837    }
838    out
839}
840pub const PROMOTION_THRESHOLD: i64 = 5;
841/// How much to extend TTL on access (1 hour for short, 1 day for mid)
842pub const SHORT_TTL_EXTEND_SECS: i64 = 3600;
843pub const MID_TTL_EXTEND_SECS: i64 = 86400;
844
845#[cfg(test)]
846mod tests {
847    use super::*;
848
849    #[test]
850    fn tier_from_str_valid() {
851        assert_eq!(Tier::from_str("short"), Some(Tier::Short));
852        assert_eq!(Tier::from_str("mid"), Some(Tier::Mid));
853        assert_eq!(Tier::from_str("long"), Some(Tier::Long));
854    }
855
856    #[test]
857    fn tier_from_str_invalid() {
858        assert_eq!(Tier::from_str("invalid"), None);
859        assert_eq!(Tier::from_str(""), None);
860        assert_eq!(Tier::from_str("SHORT"), None); // case-sensitive
861    }
862
863    #[test]
864    fn tier_as_str_roundtrip() {
865        for tier in [Tier::Short, Tier::Mid, Tier::Long] {
866            let s = tier.as_str();
867            assert_eq!(Tier::from_str(s), Some(tier));
868        }
869    }
870
871    #[test]
872    fn tier_default_ttl() {
873        assert_eq!(Tier::Short.default_ttl_secs(), Some(6 * 3600));
874        assert_eq!(Tier::Mid.default_ttl_secs(), Some(7 * 24 * 3600));
875        assert_eq!(Tier::Long.default_ttl_secs(), None);
876    }
877
878    #[test]
879    fn tier_display() {
880        assert_eq!(format!("{}", Tier::Short), "short");
881        assert_eq!(format!("{}", Tier::Mid), "mid");
882        assert_eq!(format!("{}", Tier::Long), "long");
883    }
884
885    #[test]
886    fn constants_valid() {
887        const _: () = assert!(MAX_CONTENT_SIZE > 0);
888        const _: () = assert!(PROMOTION_THRESHOLD > 0);
889        assert_eq!(SHORT_TTL_EXTEND_SECS, 3600);
890        assert_eq!(MID_TTL_EXTEND_SECS, 86400);
891    }
892
893    #[test]
894    fn tier_rank_ordering() {
895        assert!(Tier::Short.rank() < Tier::Mid.rank());
896        assert!(Tier::Mid.rank() < Tier::Long.rank());
897        assert_eq!(Tier::Short.rank(), 0);
898        assert_eq!(Tier::Mid.rank(), 1);
899        assert_eq!(Tier::Long.rank(), 2);
900    }
901
902    // Task 1.4 — hierarchical namespace helpers --------------------------------
903
904    #[test]
905    fn depth_flat_namespace() {
906        assert_eq!(namespace_depth("global"), 1);
907        assert_eq!(namespace_depth("ai-memory"), 1);
908        assert_eq!(namespace_depth("under_score"), 1);
909    }
910
911    #[test]
912    fn depth_hierarchical() {
913        assert_eq!(namespace_depth("a/b"), 2);
914        assert_eq!(namespace_depth("alphaone/engineering"), 2);
915        assert_eq!(namespace_depth("alphaone/engineering/platform"), 3);
916        assert_eq!(
917            namespace_depth("a/b/c/d/e/f/g/h"),
918            8,
919            "max depth of 8 counts each segment"
920        );
921    }
922
923    #[test]
924    fn depth_empty_is_zero() {
925        assert_eq!(namespace_depth(""), 0);
926    }
927
928    #[test]
929    fn parent_hierarchical() {
930        assert_eq!(
931            namespace_parent("alphaone/engineering/platform"),
932            Some("alphaone/engineering".to_string())
933        );
934        assert_eq!(
935            namespace_parent("alphaone/engineering"),
936            Some("alphaone".to_string())
937        );
938    }
939
940    #[test]
941    fn parent_flat_is_none() {
942        assert_eq!(namespace_parent("global"), None);
943        assert_eq!(namespace_parent("ai-memory"), None);
944        assert_eq!(namespace_parent(""), None);
945    }
946
947    #[test]
948    fn ancestors_three_levels() {
949        let a = namespace_ancestors("alphaone/engineering/platform");
950        assert_eq!(
951            a,
952            vec![
953                "alphaone/engineering/platform".to_string(),
954                "alphaone/engineering".to_string(),
955                "alphaone".to_string(),
956            ],
957            "ancestors ordered most-specific-first"
958        );
959    }
960
961    #[test]
962    fn ancestors_flat_namespace() {
963        assert_eq!(namespace_ancestors("global"), vec!["global".to_string()]);
964        assert_eq!(
965            namespace_ancestors("ai-memory"),
966            vec!["ai-memory".to_string()]
967        );
968    }
969
970    #[test]
971    fn ancestors_empty_input() {
972        assert!(namespace_ancestors("").is_empty());
973    }
974
975    #[test]
976    fn ancestors_single_level() {
977        assert_eq!(namespace_ancestors("a"), vec!["a".to_string()]);
978    }
979
980    #[test]
981    fn ancestors_max_depth() {
982        let a = namespace_ancestors("a/b/c/d/e/f/g/h");
983        assert_eq!(a.len(), 8);
984        assert_eq!(a[0], "a/b/c/d/e/f/g/h");
985        assert_eq!(a[7], "a");
986    }
987
988    // Task 1.8 — governance types ---------------------------------------
989
990    #[test]
991    fn governance_default_policy() {
992        let p = GovernancePolicy::default();
993        assert_eq!(p.write, GovernanceLevel::Any);
994        assert_eq!(p.promote, GovernanceLevel::Any);
995        assert_eq!(p.delete, GovernanceLevel::Owner);
996        assert_eq!(p.approver, ApproverType::Human);
997    }
998
999    #[test]
1000    fn governance_level_serde_snake_case() {
1001        // Serialize each level as a lowercase JSON string
1002        for (level, expected) in [
1003            (GovernanceLevel::Any, "any"),
1004            (GovernanceLevel::Registered, "registered"),
1005            (GovernanceLevel::Owner, "owner"),
1006            (GovernanceLevel::Approve, "approve"),
1007        ] {
1008            let json = serde_json::to_string(&level).unwrap();
1009            assert_eq!(json, format!("\"{expected}\""));
1010            // Roundtrip
1011            let back: GovernanceLevel = serde_json::from_str(&json).unwrap();
1012            assert_eq!(back, level);
1013        }
1014    }
1015
1016    #[test]
1017    fn approver_type_serde_shapes() {
1018        // Human → unit variant serializes as bare string
1019        let json = serde_json::to_string(&ApproverType::Human).unwrap();
1020        assert_eq!(json, "\"human\"");
1021
1022        // Agent(s) → externally tagged
1023        let a = ApproverType::Agent("alice".to_string());
1024        let json = serde_json::to_string(&a).unwrap();
1025        assert_eq!(json, r#"{"agent":"alice"}"#);
1026        let back: ApproverType = serde_json::from_str(&json).unwrap();
1027        assert_eq!(back, a);
1028
1029        // Consensus(n) → externally tagged, numeric payload
1030        let c = ApproverType::Consensus(3);
1031        let json = serde_json::to_string(&c).unwrap();
1032        assert_eq!(json, r#"{"consensus":3}"#);
1033        let back: ApproverType = serde_json::from_str(&json).unwrap();
1034        assert_eq!(back, c);
1035    }
1036
1037    #[test]
1038    fn governance_policy_full_roundtrip() {
1039        let p = GovernancePolicy {
1040            write: GovernanceLevel::Registered,
1041            promote: GovernanceLevel::Approve,
1042            delete: GovernanceLevel::Owner,
1043            approver: ApproverType::Agent("maintainer".to_string()),
1044        };
1045        let json = serde_json::to_string(&p).unwrap();
1046        let back: GovernancePolicy = serde_json::from_str(&json).unwrap();
1047        assert_eq!(back, p);
1048    }
1049
1050    #[test]
1051    fn governance_from_metadata_missing() {
1052        let meta = serde_json::json!({"agent_id": "alice"});
1053        assert!(GovernancePolicy::from_metadata(&meta).is_none());
1054    }
1055
1056    #[test]
1057    fn governance_from_metadata_null() {
1058        let meta = serde_json::json!({"governance": null});
1059        assert!(GovernancePolicy::from_metadata(&meta).is_none());
1060    }
1061
1062    #[test]
1063    fn governance_from_metadata_default_shape() {
1064        let default = GovernancePolicy::default();
1065        let meta = serde_json::json!({"governance": serde_json::to_value(&default).unwrap()});
1066        let parsed = GovernancePolicy::from_metadata(&meta)
1067            .expect("present")
1068            .expect("valid");
1069        assert_eq!(parsed, default);
1070    }
1071
1072    #[test]
1073    fn governance_from_metadata_invalid_returns_err() {
1074        let meta = serde_json::json!({
1075            "governance": {"write": "bogus", "promote": "any", "delete": "any", "approver": "human"}
1076        });
1077        let result = GovernancePolicy::from_metadata(&meta).expect("present");
1078        assert!(result.is_err(), "unknown enum value must fail deserialize");
1079    }
1080
1081    // v0.6.2 (S34 defense): partial policy payloads fall back to the
1082    // `Default for GovernancePolicy` values for any field the caller omitted.
1083    // `write` remains required — it's the core knob the policy expresses.
1084
1085    #[test]
1086    fn governance_partial_policy_write_only_uses_defaults() {
1087        let json = serde_json::json!({"write": "owner"});
1088        let parsed: GovernancePolicy = serde_json::from_value(json).expect("write-only parses");
1089        assert_eq!(parsed.write, GovernanceLevel::Owner);
1090        assert_eq!(parsed.promote, GovernanceLevel::Any);
1091        assert_eq!(parsed.delete, GovernanceLevel::Owner);
1092        assert_eq!(parsed.approver, ApproverType::Human);
1093    }
1094
1095    #[test]
1096    fn governance_partial_policy_write_and_promote() {
1097        let json = serde_json::json!({"write": "any", "promote": "registered"});
1098        let parsed: GovernancePolicy = serde_json::from_value(json).expect("parses");
1099        assert_eq!(parsed.promote, GovernanceLevel::Registered);
1100        // Absent fields still take defaults.
1101        assert_eq!(parsed.delete, GovernanceLevel::Owner);
1102        assert_eq!(parsed.approver, ApproverType::Human);
1103    }
1104
1105    #[test]
1106    fn governance_missing_write_still_errors() {
1107        // `write` is the core policy knob — must remain required to avoid
1108        // silently accepting an empty object as "any writes allowed".
1109        let json = serde_json::json!({"promote": "owner"});
1110        let err = serde_json::from_value::<GovernancePolicy>(json);
1111        assert!(err.is_err(), "missing write must fail deserialize");
1112    }
1113
1114    #[test]
1115    fn governance_level_as_str_tags() {
1116        assert_eq!(GovernanceLevel::Any.as_str(), "any");
1117        assert_eq!(GovernanceLevel::Registered.as_str(), "registered");
1118        assert_eq!(GovernanceLevel::Owner.as_str(), "owner");
1119        assert_eq!(GovernanceLevel::Approve.as_str(), "approve");
1120    }
1121
1122    #[test]
1123    fn approver_type_kind_tags() {
1124        assert_eq!(ApproverType::Human.kind(), "human");
1125        assert_eq!(ApproverType::Agent("a".into()).kind(), "agent");
1126        assert_eq!(ApproverType::Consensus(3).kind(), "consensus");
1127    }
1128
1129    // -----------------------------------------------------------------
1130    // W12-H — additional small-module pinning
1131    // -----------------------------------------------------------------
1132
1133    #[test]
1134    fn default_metadata_is_empty_object() {
1135        let v = default_metadata();
1136        assert!(v.is_object());
1137        assert!(v.as_object().unwrap().is_empty());
1138    }
1139
1140    #[test]
1141    fn governed_action_as_str_pinned() {
1142        assert_eq!(GovernedAction::Store.as_str(), "store");
1143        assert_eq!(GovernedAction::Delete.as_str(), "delete");
1144        assert_eq!(GovernedAction::Promote.as_str(), "promote");
1145    }
1146
1147    #[test]
1148    fn governance_decision_equality() {
1149        assert_eq!(GovernanceDecision::Allow, GovernanceDecision::Allow);
1150        assert_ne!(
1151            GovernanceDecision::Deny("a".into()),
1152            GovernanceDecision::Deny("b".into()),
1153        );
1154        assert_eq!(
1155            GovernanceDecision::Pending("p1".into()),
1156            GovernanceDecision::Pending("p1".into())
1157        );
1158    }
1159
1160    #[test]
1161    fn vector_clock_observe_monotonic() {
1162        let mut vc = VectorClock::default();
1163        vc.observe("peer-a", "2026-04-01T00:00:00+00:00");
1164        vc.observe("peer-a", "2026-05-01T00:00:00+00:00");
1165        // Older never overwrites newer.
1166        vc.observe("peer-a", "2026-03-01T00:00:00+00:00");
1167        assert_eq!(vc.latest_from("peer-a"), Some("2026-05-01T00:00:00+00:00"));
1168    }
1169
1170    #[test]
1171    fn vector_clock_latest_from_unknown_is_none() {
1172        let vc = VectorClock::default();
1173        assert!(vc.latest_from("never-seen").is_none());
1174    }
1175
1176    #[test]
1177    fn vector_clock_serde_roundtrip() {
1178        let mut vc = VectorClock::default();
1179        vc.observe("p1", "2026-04-01T00:00:00+00:00");
1180        vc.observe("p2", "2026-04-02T00:00:00+00:00");
1181        let json = serde_json::to_string(&vc).unwrap();
1182        let back: VectorClock = serde_json::from_str(&json).unwrap();
1183        assert_eq!(back.entries.len(), 2);
1184        assert_eq!(back, vc);
1185    }
1186
1187    #[test]
1188    fn namespace_parent_with_trailing_slash() {
1189        // "a/" splits to parent="a" and tail="". The function returns the
1190        // parent regardless of whether the final segment is empty.
1191        assert_eq!(namespace_parent("a/"), Some("a".to_string()));
1192    }
1193
1194    #[test]
1195    fn namespace_depth_skips_empty_segments() {
1196        // Multiple slashes do not inflate the depth count.
1197        assert_eq!(namespace_depth("a//b"), 2);
1198        assert_eq!(namespace_depth("/a"), 1);
1199        assert_eq!(namespace_depth("a/"), 1);
1200    }
1201
1202    #[test]
1203    fn namespace_ancestors_two_levels() {
1204        // Two-level namespace produces self + parent.
1205        assert_eq!(
1206            namespace_ancestors("a/b"),
1207            vec!["a/b".to_string(), "a".to_string()]
1208        );
1209    }
1210
1211    #[test]
1212    fn memory_serde_roundtrip_minimal() {
1213        let m = Memory {
1214            id: "abc".into(),
1215            tier: Tier::Mid,
1216            namespace: "global".into(),
1217            title: "t".into(),
1218            content: "c".into(),
1219            tags: vec!["x".into()],
1220            priority: 5,
1221            confidence: 0.9,
1222            source: "api".into(),
1223            access_count: 0,
1224            created_at: "2026-04-01T00:00:00+00:00".into(),
1225            updated_at: "2026-04-01T00:00:00+00:00".into(),
1226            last_accessed_at: None,
1227            expires_at: None,
1228            metadata: default_metadata(),
1229        };
1230        let json = serde_json::to_string(&m).unwrap();
1231        let back: Memory = serde_json::from_str(&json).unwrap();
1232        assert_eq!(back.id, m.id);
1233        assert_eq!(back.tier, Tier::Mid);
1234    }
1235
1236    #[test]
1237    fn approver_type_kind_for_each_variant() {
1238        // Hits all three discriminant arms. Mirrors the existing test but
1239        // ensures we cover a Consensus(0) which is the lower edge.
1240        assert_eq!(ApproverType::Human.kind(), "human");
1241        assert_eq!(ApproverType::Agent(String::new()).kind(), "agent");
1242        assert_eq!(ApproverType::Consensus(0).kind(), "consensus");
1243    }
1244
1245    #[test]
1246    fn governance_partial_policy_with_approver() {
1247        // Partial policy with `approver` set and other fields defaulted.
1248        let json = serde_json::json!({
1249            "write": "owner",
1250            "approver": {"agent": "alice"}
1251        });
1252        let parsed: GovernancePolicy = serde_json::from_value(json).expect("parses");
1253        assert_eq!(parsed.write, GovernanceLevel::Owner);
1254        assert_eq!(parsed.approver, ApproverType::Agent("alice".to_string()));
1255        assert_eq!(parsed.promote, GovernanceLevel::Any);
1256        assert_eq!(parsed.delete, GovernanceLevel::Owner);
1257    }
1258}