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