Skip to main content

aicx_parser/
timeline.rs

1//! Shared timeline and segmentation data types.
2//!
3//! Vibecrafted with AI Agents by Vetcoders (c)2026 Vetcoders
4
5use chrono::{DateTime, Utc};
6#[cfg(feature = "json-schema")]
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9use std::fmt;
10use std::path::PathBuf;
11
12/// Canonical kind for a session segment in the store.
13///
14/// Kind determines the subdirectory under `<project>/<date>/` and is part
15/// of the canonical store path. Classification is conservative: when in
16/// doubt, segments fall through to `Other`.
17#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
18#[serde(rename_all = "lowercase")]
19pub enum Kind {
20    Conversations,
21    Plans,
22    Reports,
23    #[default]
24    Other,
25}
26
27impl Kind {
28    /// Directory name used in the canonical store layout.
29    pub fn dir_name(self) -> &'static str {
30        match self {
31            Self::Conversations => "conversations",
32            Self::Plans => "plans",
33            Self::Reports => "reports",
34            Self::Other => "other",
35        }
36    }
37
38    /// Parse from a string (case-insensitive, accepts both singular and plural).
39    pub fn parse(s: &str) -> Option<Self> {
40        match s.to_ascii_lowercase().as_str() {
41            "conversations" | "conversation" => Some(Self::Conversations),
42            "plans" | "plan" => Some(Self::Plans),
43            "reports" | "report" => Some(Self::Reports),
44            "other" => Some(Self::Other),
45            _ => None,
46        }
47    }
48}
49
50impl fmt::Display for Kind {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        f.write_str(self.dir_name())
53    }
54}
55
56/// Canonical stream/frame classification for a timeline entry or stored chunk.
57///
58/// This axis is intentionally orthogonal to `role`: source formats drift in how
59/// they spell assistant reasoning or tool payloads, but downstream retrieval
60/// needs one stable vocabulary for "which channel is this?".
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
62#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
63#[serde(rename_all = "snake_case")]
64pub enum FrameKind {
65    UserMsg,
66    AgentReply,
67    InternalThought,
68    ToolCall,
69    SystemNote,
70}
71
72impl FrameKind {
73    pub fn as_str(self) -> &'static str {
74        match self {
75            Self::UserMsg => "user_msg",
76            Self::AgentReply => "agent_reply",
77            Self::InternalThought => "internal_thought",
78            Self::ToolCall => "tool_call",
79            Self::SystemNote => "system_note",
80        }
81    }
82
83    pub fn parse(value: &str) -> Option<Self> {
84        match value.trim().to_ascii_lowercase().as_str() {
85            "user_msg" | "user" => Some(Self::UserMsg),
86            "agent_reply" | "assistant" | "reply" => Some(Self::AgentReply),
87            "internal_thought" | "thought" | "thinking" | "reasoning" => {
88                Some(Self::InternalThought)
89            }
90            "tool_call" | "tool" | "tool_result" | "function_call" => Some(Self::ToolCall),
91            "system_note" | "system" | "note" | "notification" | "error" => Some(Self::SystemNote),
92            _ => None,
93        }
94    }
95}
96
97impl fmt::Display for FrameKind {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        f.write_str(self.as_str())
100    }
101}
102
103/// Neutral metadata for conversation JSON consumers.
104///
105/// This is descriptive only: it does not decide indexing policy, filtering, or
106/// chunking behavior.
107#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
108#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
109#[serde(rename_all = "snake_case")]
110pub enum MessageKind {
111    #[default]
112    Conversation,
113    WorkflowPrompt,
114    ContinuationSummary,
115    CollapseStub,
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
119#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
120#[serde(rename_all = "snake_case")]
121pub enum CollapseStubKind {
122    SkillRef,
123    DedupRef,
124}
125
126/// Unified timeline entry from any AI agent source.
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct TimelineEntry {
129    pub timestamp: DateTime<Utc>,
130    pub agent: String,
131    pub session_id: String,
132    pub role: String,
133    pub message: String,
134    #[serde(default, skip_serializing_if = "Option::is_none")]
135    pub frame_kind: Option<FrameKind>,
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub branch: Option<String>,
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub cwd: Option<String>,
140    #[serde(default, skip_serializing_if = "Option::is_none")]
141    pub timestamp_source: Option<String>,
142    #[serde(default, skip_serializing_if = "Option::is_none")]
143    pub source_path: Option<String>,
144    #[serde(default, skip_serializing_if = "Option::is_none")]
145    pub source_sha256: Option<String>,
146    #[serde(default, skip_serializing_if = "Option::is_none")]
147    pub source_line_span: Option<(u64, u64)>,
148}
149
150/// Denoised conversation message — the canonical projection of a TimelineEntry
151/// containing only user/assistant messages with repo-centric identity.
152///
153/// This is the primary unit for "recover the conversation" workflows.
154/// Tool calls, tool results, reasoning/thoughts, system noise, and artifact
155/// payloads are excluded. Artifact paths may appear as references only.
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct ConversationMessage {
158    pub timestamp: DateTime<Utc>,
159    pub agent: String,
160    pub session_id: String,
161    /// Only "user" or "assistant" — reasoning and system roles are excluded.
162    pub role: String,
163    /// Raw, untrimmed, untruncated message body.
164    pub message: String,
165    /// Canonical project/repo identity (derived from cwd + project filter).
166    pub repo_project: String,
167    /// Secondary provenance: source working directory path.
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub source_path: Option<String>,
170    /// Git branch at time of message (when available).
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub branch: Option<String>,
173    /// Neutral descriptive metadata for JSON consumers.
174    #[serde(default)]
175    pub message_kind: MessageKind,
176    /// Present only when `message_kind` is `collapse_stub`.
177    #[serde(default, skip_serializing_if = "Option::is_none")]
178    pub collapse_stub_kind: Option<CollapseStubKind>,
179}
180
181/// Configuration for extraction.
182#[derive(Debug, Clone)]
183pub struct ExtractionConfig {
184    pub project_filter: Vec<String>,
185    pub cutoff: DateTime<Utc>,
186    pub include_assistant: bool,
187    pub watermark: Option<DateTime<Utc>>,
188}
189
190/// Info about an available source directory/file.
191#[derive(Debug, Clone, Serialize)]
192pub struct SourceInfo {
193    pub agent: String,
194    pub path: PathBuf,
195    pub sessions: usize,
196    pub size_bytes: u64,
197    pub protected_by_git: bool,
198    pub protection_backend: String,
199    pub protection_root: Option<PathBuf>,
200    pub git_remote_count: usize,
201    pub git_remotes: Vec<String>,
202    pub protection_warning: Option<String>,
203}
204
205/// Explicit trust tier for a repo identity signal.
206///
207/// Not all evidence for "which repo is this?" is equal. A git remote URL
208/// is canonical truth; a directory layout is a strong hint; a hex hash is
209/// opaque noise. This enum makes the distinction machine-readable so the
210/// store can decide whether to assert identity or route to fallback.
211#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
212pub enum SourceTier {
213    /// Git remote URL or explicit GitHub/GitLab link in message text.
214    /// The strongest signal — the repo literally named itself.
215    Primary,
216    /// Local git repo discovered on disk (via `.git/` traversal + known layout),
217    /// or a projectHash resolved through a trustworthy local mapping file.
218    Secondary,
219    /// Known directory layout (e.g. `~/hosted/<org>/<repo>`) without a `.git/`
220    /// directory or remote confirmation. Plausible but not proven.
221    Fallback,
222    /// Hex hash, opaque identifier, or source that is explicitly not a
223    /// conversation (e.g. `.pb` protobuf, step-output). Must never assert
224    /// repo identity on its own.
225    Opaque,
226}
227
228impl SourceTier {
229    /// Whether this tier is strong enough to assert repo identity for
230    /// canonical store placement (under `store/<org>/<repo>/`).
231    pub fn is_assertable(self) -> bool {
232        matches!(self, Self::Primary | Self::Secondary)
233    }
234}
235
236#[derive(Debug, Clone, PartialEq, Eq, Hash)]
237pub struct RepoIdentity {
238    pub organization: String,
239    pub repository: String,
240}
241
242impl RepoIdentity {
243    pub fn slug(&self) -> String {
244        format!("{}/{}", self.organization, self.repository)
245    }
246}
247
248#[derive(Debug, Clone)]
249pub struct SemanticSegment {
250    pub repo: Option<RepoIdentity>,
251    /// The trust tier of the strongest signal that produced `repo`.
252    /// `None` when `repo` is `None`.
253    pub source_tier: Option<SourceTier>,
254    pub kind: Kind,
255    pub agent: String,
256    pub session_id: String,
257    pub entries: Vec<TimelineEntry>,
258}
259
260impl SemanticSegment {
261    pub fn project_label(&self) -> String {
262        self.repo
263            .as_ref()
264            .map(RepoIdentity::slug)
265            .unwrap_or_else(|| "non-repository-contexts".to_string())
266    }
267
268    /// Whether the repo identity is strong enough for canonical store placement.
269    /// Returns `false` for `None` repo or Fallback/Opaque tiers.
270    pub fn has_assertable_identity(&self) -> bool {
271        self.source_tier.is_some_and(SourceTier::is_assertable)
272    }
273}