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}