lean-ctx 3.9.19

Context Runtime for AI Agents with CCP. 79 MCP tools, 10 read modes, 95+ compression patterns, cross-session memory (CCP), persistent AI knowledge with temporal facts + contradiction detection, multi-agent context sharing, LITM-aware positioning, AAAK compact format, adaptive compression with Thompson Sampling bandits. Supports 24+ AI tools. Reduces LLM token consumption by up to 99%.
//! `KnowledgeSnapshot` — the single in-memory view of a project's durable
//! knowledge that every *outbound* rendering shares.
//!
//! Before this existed, the Context Package builder read `knowledge.json` +
//! `relations.json` inline and shaped its own `KnowledgeLayer`. Adding a second
//! portable format (Open Knowledge Format, see [`super::okf`]) would have meant a
//! *second* extractor reading the same stores with subtly different rules — the
//! exact fragmentation the "one model, many renderings" positioning warns
//! against. Instead both the ctxpkg `KnowledgeLayer` and the OKF Markdown bundle
//! are rendered from this one snapshot, so they can never drift on *what counts
//! as the project's knowledge*.
//!
//! The snapshot carries the full fact history (ctxpkg preserves superseded facts
//! for fidelity); [`KnowledgeSnapshot::current_facts`] is the human-facing view
//! portable exports use.

use crate::core::knowledge_relations::{KnowledgeEdge, KnowledgeRelationGraph};
use lean_ctx_protocol::KnowledgeObjectV1;
use serde::{Deserialize, Serialize};

use super::types::{ConsolidatedInsight, KnowledgeFact, ProjectKnowledge, ProjectPattern};

/// Receipt-safe reference to a selected portable knowledge object.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct KnowledgeRef {
    pub object_id: String,
    pub evidence_digest: String,
    pub policy_ref: String,
}

/// A consistent read of a project's knowledge (facts + patterns + insights) and
/// its relation graph, taken at one point in time. The single source of truth
/// for outbound renderings (ctxpkg, OKF).
#[derive(Debug, Clone)]
pub struct KnowledgeSnapshot {
    pub project_root: String,
    pub project_hash: String,
    /// All facts, including superseded ones (ctxpkg keeps the history).
    pub facts: Vec<KnowledgeFact>,
    pub patterns: Vec<ProjectPattern>,
    /// Consolidated insights (the project's `history`).
    pub insights: Vec<ConsolidatedInsight>,
    /// Typed relations between facts (`relations.json`).
    pub relations: Vec<KnowledgeEdge>,
    /// Task identity for portable retrieval snapshots.
    pub task_id: Option<String>,
    /// Retrieval-policy version used to select portable objects.
    pub policy_version: Option<String>,
    /// Stable references to portable objects selected for the task.
    pub knowledge_refs: Vec<KnowledgeRef>,
    /// Deterministic digest of the task, policy, and selected references.
    pub snapshot_hash: Option<String>,
}

impl KnowledgeSnapshot {
    /// Loads a project's knowledge and relation graph from disk into one
    /// snapshot. Missing stores yield empty collections rather than an error, so
    /// callers can treat "no knowledge yet" via [`KnowledgeSnapshot::is_empty`].
    pub fn collect(project_root: &str) -> Self {
        let knowledge = ProjectKnowledge::load_or_create(project_root);
        let relations = KnowledgeRelationGraph::load(&knowledge.project_hash)
            .map(|g| g.edges)
            .unwrap_or_default();
        Self::from_project(&knowledge, relations)
    }

    /// Builds a snapshot from an already-loaded `ProjectKnowledge` plus its
    /// relation edges. Keeps the collection logic in one place for callers that
    /// already hold the knowledge (e.g. inside a lock).
    pub fn from_project(knowledge: &ProjectKnowledge, relations: Vec<KnowledgeEdge>) -> Self {
        Self {
            project_root: knowledge.project_root.clone(),
            project_hash: knowledge.project_hash.clone(),
            facts: knowledge.facts.clone(),
            patterns: knowledge.patterns.clone(),
            insights: knowledge.history.clone(),
            relations,
            task_id: None,
            policy_version: None,
            knowledge_refs: Vec::new(),
            snapshot_hash: None,
        }
    }

    /// Build a deterministic task-local snapshot from portable knowledge objects.
    pub fn from_items(task_id: &str, policy_version: &str, items: &[KnowledgeObjectV1]) -> Self {
        let knowledge_refs = items
            .iter()
            .map(|item| KnowledgeRef {
                object_id: item.object_id().to_owned(),
                evidence_digest: item.evidence_digest.clone(),
                policy_ref: item.policy_ref.clone(),
            })
            .collect::<Vec<_>>();
        let mut hasher = blake3::Hasher::new();
        for part in std::iter::once(task_id)
            .chain(std::iter::once(policy_version))
            .chain(knowledge_refs.iter().flat_map(|reference| {
                [
                    reference.object_id.as_str(),
                    reference.evidence_digest.as_str(),
                    reference.policy_ref.as_str(),
                ]
            }))
        {
            hasher.update(&(part.len() as u64).to_le_bytes());
            hasher.update(part.as_bytes());
        }
        let snapshot_hash = hasher.finalize().to_hex().to_string();
        Self {
            project_root: String::new(),
            project_hash: snapshot_hash.clone(),
            facts: Vec::new(),
            patterns: Vec::new(),
            insights: Vec::new(),
            relations: Vec::new(),
            task_id: Some(task_id.to_owned()),
            policy_version: Some(policy_version.to_owned()),
            knowledge_refs,
            snapshot_hash: Some(snapshot_hash),
        }
    }

    /// True when there is nothing worth exporting (no facts, patterns, or
    /// insights). Relations alone never make a bundle — they are edges between
    /// facts that, without endpoints, carry no standalone meaning.
    pub fn is_empty(&self) -> bool {
        self.facts.is_empty()
            && self.patterns.is_empty()
            && self.insights.is_empty()
            && self.knowledge_refs.is_empty()
    }

    /// The current (non-superseded, temporally valid) facts — the human-facing
    /// view portable exports render. Superseded history stays in [`Self::facts`]
    /// for ctxpkg fidelity but would only confuse a hand-edited OKF bundle.
    pub fn current_facts(&self) -> Vec<&KnowledgeFact> {
        self.facts.iter().filter(|f| f.is_current()).collect()
    }
}