Skip to main content

shiplog_ports/
lib.rs

1#![warn(missing_docs)]
2//! Port trait definitions for the shiplog pipeline.
3//!
4//! Defines the four core abstractions: [`Ingestor`] (data collection),
5//! [`WorkstreamClusterer`] (event grouping), [`Renderer`] (output generation),
6//! and [`Redactor`] (privacy-aware projection). Adapters depend on ports;
7//! ports never depend on adapters.
8
9use anyhow::Result;
10use shiplog_schema::coverage::CoverageManifest;
11use shiplog_schema::event::EventEnvelope;
12use shiplog_schema::freshness::SourceFreshness;
13use shiplog_schema::workstream::WorkstreamsFile;
14
15/// Output of an ingestion run.
16///
17/// The tool treats these as immutable receipts. `freshness` carries
18/// per-source attribution for cache hits vs fresh fetches; adapters
19/// that have no notion of freshness (or do not yet emit it) may leave
20/// the vector empty.
21///
22/// # Examples
23///
24/// ```
25/// use shiplog_ports::IngestOutput;
26/// use shiplog_schema::coverage::{CoverageManifest, Completeness, TimeWindow};
27/// use chrono::{NaiveDate, Utc};
28/// use shiplog_ids::RunId;
29///
30/// let output = IngestOutput {
31///     events: vec![],
32///     coverage: CoverageManifest {
33///         run_id: RunId::now("test"),
34///         generated_at: Utc::now(),
35///         user: "octocat".into(),
36///         window: TimeWindow {
37///             since: NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(),
38///             until: NaiveDate::from_ymd_opt(2025, 2, 1).unwrap(),
39///         },
40///         mode: "merged".into(),
41///         sources: vec!["github".into()],
42///         slices: vec![],
43///         warnings: vec![],
44///         completeness: Completeness::Complete,
45///     },
46///     freshness: vec![],
47/// };
48/// assert!(output.events.is_empty());
49/// ```
50#[derive(Clone, Debug, PartialEq)]
51pub struct IngestOutput {
52    /// The collected event envelopes.
53    pub events: Vec<EventEnvelope>,
54    /// Coverage manifest describing what was queried and fetched.
55    pub coverage: CoverageManifest,
56    /// Per-source freshness receipts produced by the adapter. Adapters
57    /// that don't yet emit freshness leave this empty; callers must
58    /// tolerate `freshness.is_empty()` and fall back to other signals
59    /// (source decisions, coverage) for those sources.
60    pub freshness: Vec<SourceFreshness>,
61}
62
63/// Basic ingestion trait.
64///
65/// Adapters live in `shiplog-ingest-*` crates.
66///
67/// # Examples
68///
69/// ```rust,no_run
70/// use shiplog_ports::{Ingestor, IngestOutput};
71/// use anyhow::Result;
72///
73/// struct MyIngestor;
74///
75/// impl Ingestor for MyIngestor {
76///     fn ingest(&self) -> Result<IngestOutput> {
77///         todo!("fetch events from your source")
78///     }
79/// }
80/// ```
81pub trait Ingestor {
82    /// Fetch events from the data source and return them with coverage metadata.
83    fn ingest(&self) -> Result<IngestOutput>;
84}
85
86/// Workstream clustering.
87///
88/// This is intentionally a port so the default clustering can be swapped without rewriting the app.
89///
90/// # Examples
91///
92/// ```rust,no_run
93/// use shiplog_ports::WorkstreamClusterer;
94/// use shiplog_schema::event::EventEnvelope;
95/// use shiplog_schema::workstream::WorkstreamsFile;
96/// use anyhow::Result;
97///
98/// struct RepoClusterer;
99///
100/// impl WorkstreamClusterer for RepoClusterer {
101///     fn cluster(&self, events: &[EventEnvelope]) -> Result<WorkstreamsFile> {
102///         todo!("group events by repository")
103///     }
104/// }
105/// ```
106pub trait WorkstreamClusterer {
107    /// Group events into workstreams and return the resulting file.
108    fn cluster(&self, events: &[EventEnvelope]) -> Result<WorkstreamsFile>;
109}
110
111/// Rendering.
112///
113/// Renderers should be pure: input in, bytes out.
114///
115/// # Examples
116///
117/// ```rust,no_run
118/// use shiplog_ports::Renderer;
119/// use shiplog_schema::event::EventEnvelope;
120/// use shiplog_schema::workstream::WorkstreamsFile;
121/// use shiplog_schema::coverage::CoverageManifest;
122/// use anyhow::Result;
123///
124/// struct MarkdownRenderer;
125///
126/// impl Renderer for MarkdownRenderer {
127///     fn render_packet_markdown(
128///         &self,
129///         user: &str,
130///         window_label: &str,
131///         events: &[EventEnvelope],
132///         workstreams: &WorkstreamsFile,
133///         coverage: &CoverageManifest,
134///     ) -> Result<String> {
135///         Ok(format!("# Packet for {user}\n"))
136///     }
137/// }
138/// ```
139pub trait Renderer {
140    /// Render a Markdown shipping packet from the given events and metadata.
141    fn render_packet_markdown(
142        &self,
143        user: &str,
144        window_label: &str,
145        events: &[EventEnvelope],
146        workstreams: &WorkstreamsFile,
147        coverage: &CoverageManifest,
148    ) -> Result<String>;
149}
150
151/// Redaction.
152///
153/// Redaction is a rendering mode. Same underlying ledger, different projections.
154///
155/// # Examples
156///
157/// ```rust,no_run
158/// use shiplog_ports::Redactor;
159/// use shiplog_schema::event::EventEnvelope;
160/// use shiplog_schema::workstream::WorkstreamsFile;
161/// use anyhow::Result;
162///
163/// struct NoOpRedactor;
164///
165/// impl Redactor for NoOpRedactor {
166///     fn redact_events(&self, events: &[EventEnvelope], _profile: &str) -> Result<Vec<EventEnvelope>> {
167///         Ok(events.to_vec())
168///     }
169///     fn redact_workstreams(&self, ws: &WorkstreamsFile, _profile: &str) -> Result<WorkstreamsFile> {
170///         Ok(ws.clone())
171///     }
172/// }
173/// ```
174pub trait Redactor {
175    /// Apply a redaction profile to events, returning redacted copies.
176    fn redact_events(&self, events: &[EventEnvelope], profile: &str) -> Result<Vec<EventEnvelope>>;
177    /// Apply a redaction profile to workstreams, returning redacted copies.
178    fn redact_workstreams(
179        &self,
180        workstreams: &WorkstreamsFile,
181        profile: &str,
182    ) -> Result<WorkstreamsFile>;
183}