Skip to main content

zeph_memory/graph/ingest/
document.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Ingest-document types for `zeph knowledge ingest` (spec-067 §2.3).
5
6use crate::graph::ingest::ledger::IngestLedger;
7use crate::graph::types::{GraphOrigin, GraphProvenance};
8
9/// Classifies the origin of documents fed to [`super::IngestDocument`].
10///
11/// Used to select the appropriate LLM extraction prompt and determine the
12/// [`GraphOrigin`] value stamped on extracted entities and edges.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14#[non_exhaustive]
15pub enum IngestSourceKind {
16    /// A versioned repository artifact such as a spec, design doc, or README.
17    ///
18    /// Uses the tech-doc extraction prompt (precise, structured).
19    StaticArtifact,
20
21    /// A transcript produced by a Zeph subagent task session.
22    ///
23    /// Uses the tech-doc extraction prompt (entity-focused, not conversational).
24    SubagentTranscript,
25
26    /// Content sourced from a foreign agent system (A2A / ACP / external API).
27    ///
28    /// Uses the tech-doc extraction prompt.
29    ///
30    /// # Note
31    ///
32    /// Trust classification for external agents is resolved at the binary layer
33    /// (spec-067 §3 D1); `zeph-memory` treats `ExternalAgent` the same as
34    /// `StaticArtifact` for prompt selection.
35    ExternalAgent,
36    // TODO(D1): add per-source trust classification once the trust-level spec is finalised.
37}
38
39impl IngestSourceKind {
40    /// Returns the LLM system prompt appropriate for this source kind.
41    ///
42    /// All current variants use the tech-doc prompt; the conversational prompt
43    /// (used by the live-conversation path) is intentionally NOT selectable here.
44    ///
45    /// # Note
46    ///
47    /// The return type is `&'static str` — prompts must be compile-time constants.
48    /// This is a known MVP limitation (see C7 in critic handoff); runtime-configurable
49    /// prompts will require a breaking change to this signature.
50    #[must_use]
51    pub fn system_prompt(self) -> &'static str {
52        match self {
53            Self::StaticArtifact | Self::SubagentTranscript | Self::ExternalAgent => {
54                super::prompt::TECH_DOC_SYSTEM_PROMPT
55            }
56        }
57    }
58
59    /// Returns the [`GraphOrigin`] value stamped on entities extracted from this kind.
60    #[must_use]
61    pub fn graph_origin(self) -> GraphOrigin {
62        match self {
63            Self::SubagentTranscript => GraphOrigin::Subagent,
64            Self::StaticArtifact => GraphOrigin::Ingest,
65            Self::ExternalAgent => GraphOrigin::ExternalAgent,
66        }
67    }
68}
69
70/// A validated, hash-stamped document ready for graph extraction.
71///
72/// Created exclusively through [`super::IngestSourceAdapter::parse`] implementations,
73/// which ensure:
74/// - `content` is non-empty.
75/// - `source_uri` is non-empty.
76/// - `content_hash` is the canonical BLAKE3 hex digest of `content`.
77/// - `provenance` is fully populated with the origin matching the source kind (see [`IngestSourceKind::graph_origin`]).
78///
79/// # Examples
80///
81/// ```no_run
82/// use zeph_memory::graph::ingest::{IngestDocument, IngestSourceKind};
83///
84/// // Documents are created by adapters. Direct construction is crate-private.
85/// // This example demonstrates what the adapter produces.
86/// # let doc: IngestDocument = unimplemented!("use SubagentJsonl adapter");
87/// let uri = doc.source_uri();
88/// let hash = doc.content_hash();
89/// let content = doc.content();
90/// ```
91#[derive(Debug, Clone)]
92pub struct IngestDocument {
93    content: String,
94    context: Vec<String>,
95    provenance: GraphProvenance,
96    content_hash: String,
97}
98
99impl IngestDocument {
100    /// Constructs a new `IngestDocument`.
101    ///
102    /// This constructor is `pub(super)` — only adapters in the `graph::ingest` module
103    /// may build documents, ensuring the valid-by-construction invariants hold.
104    pub(super) fn new(
105        content: String,
106        prior_messages: Vec<String>,
107        provenance: GraphProvenance,
108    ) -> Self {
109        let content_hash = IngestLedger::content_hash(content.as_bytes());
110        Self {
111            content,
112            context: prior_messages,
113            provenance,
114            content_hash,
115        }
116    }
117
118    /// The raw text content to be extracted.
119    #[must_use]
120    pub fn content(&self) -> &str {
121        &self.content
122    }
123
124    /// Recent context messages that precede this document (used as extraction context).
125    #[must_use]
126    pub fn context(&self) -> &[String] {
127        &self.context
128    }
129
130    /// Provenance metadata (origin, batch ID, source URI).
131    #[must_use]
132    pub fn provenance(&self) -> &GraphProvenance {
133        &self.provenance
134    }
135
136    /// The canonical BLAKE3 hex digest of [`Self::content`].
137    ///
138    /// Computed once at construction time and cached.
139    #[must_use]
140    pub fn content_hash(&self) -> &str {
141        &self.content_hash
142    }
143
144    /// Repository-relative source locator, e.g. `"subagent:<task_id>#42"`.
145    ///
146    /// Convenience accessor for `provenance().source_uri` — guaranteed non-`None`
147    /// for all documents created by adapters.
148    #[must_use]
149    pub fn source_uri(&self) -> &str {
150        self.provenance.source_uri.as_deref().unwrap_or_default()
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn subagent_transcript_maps_to_subagent_origin() {
160        assert_eq!(
161            IngestSourceKind::SubagentTranscript.graph_origin(),
162            GraphOrigin::Subagent
163        );
164    }
165
166    #[test]
167    fn static_artifact_maps_to_ingest_origin() {
168        assert_eq!(
169            IngestSourceKind::StaticArtifact.graph_origin(),
170            GraphOrigin::Ingest
171        );
172    }
173}