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}