onetaskgraph_plugin_api/work.rs
1//! The work items every source is normalised into.
2
3use chrono::{DateTime, Utc};
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7use crate::NativeId;
8
9/// One unit of work as a source reports it.
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
11pub struct Task {
12 /// The source's own opaque identifier.
13 pub id: NativeId,
14 /// The one-line summary a user recognises the task by.
15 pub title: String,
16 /// The long-form body, when the source has one.
17 pub content: Option<String>,
18 /// The source's status, normalised and preserved.
19 pub status: Status,
20 /// Inline rather than by id: a source returning a task already knows them.
21 pub labels: Vec<Label>,
22 /// `None` is a first-class case — an orphan task — not an edge case.
23 pub project: Option<NativeId>,
24 /// Where a human can open this task.
25 // llmlint: ignore[invalid_states_unrepresentable] SECOND PERMITTED REASON — this restates at a new site the justification already recorded at `Capabilities.max_page_size` (capability.rs) and `PageRequest.limit` (query.rs), and in AGENTS.md's "The plugin contract": this crate's field types ARE the approved contract, six undispatched nodes compile against `Option<String>` here, and only the contract's owner may narrow one. No code change is available that clears this without editing that frozen surface.
26 // llmlint: ignore[boundary_inputs_validated] parsing this into a URL type would narrow the same frozen surface, and would add a URL dependency to the crate AGENTS.md says to keep still ("Keep the api crate still" — every change here re-tests every plugin). A plugin that returns a string this interface cannot represent is what `SourceError::Malformed` is for. Contract owner's call; recorded in AGENTS.md, "The plugin contract".
27 pub url: Option<String>,
28 /// When the source says the task was created.
29 pub created_at: Option<DateTime<Utc>>,
30 /// When the source says the task last changed.
31 pub updated_at: Option<DateTime<Utc>>,
32}
33
34/// A grouping of tasks, shaped like a [`Task`] without a parent of its own.
35#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
36pub struct Project {
37 /// The source's own opaque identifier.
38 pub id: NativeId,
39 /// The one-line summary a user recognises the project by.
40 pub title: String,
41 /// The long-form body, when the source has one.
42 pub content: Option<String>,
43 /// The source's status, normalised and preserved.
44 pub status: Status,
45 /// Inline rather than by id, for the same reason as on [`Task`].
46 pub labels: Vec<Label>,
47 /// Where a human can open this project.
48 // llmlint: ignore[invalid_states_unrepresentable] SECOND PERMITTED REASON — this restates at a new site the justification already recorded at `Capabilities.max_page_size` (capability.rs) and `PageRequest.limit` (query.rs), and in AGENTS.md's "The plugin contract": this crate's field types ARE the approved contract, six undispatched nodes compile against `Option<String>` here, and only the contract's owner may narrow one. No code change is available that clears this without editing that frozen surface.
49 // llmlint: ignore[boundary_inputs_validated] parsing this into a URL type would narrow the same frozen surface, and would add a URL dependency to the crate AGENTS.md says to keep still ("Keep the api crate still" — every change here re-tests every plugin). A plugin that returns a string this interface cannot represent is what `SourceError::Malformed` is for. Contract owner's call; recorded in AGENTS.md, "The plugin contract".
50 pub url: Option<String>,
51 /// When the source says the project was created.
52 pub created_at: Option<DateTime<Utc>>,
53 /// When the source says the project last changed.
54 pub updated_at: Option<DateTime<Utc>>,
55}
56
57/// A tag a source attaches to work.
58#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
59pub struct Label {
60 /// The source's own opaque identifier.
61 pub id: NativeId,
62 /// What a user filtering across sources actually types.
63 pub name: String,
64 /// The source's own colour for the label, when it has one.
65 pub color: Option<String>,
66}
67
68/// A source's status, kept in both normalised and original form.
69///
70/// `category` is what every filter compares against; `name` is the source's own
71/// wording, preserved so display never flattens "In Review" into "In Progress".
72#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
73pub struct Status {
74 /// The normalised value filters compare against.
75 pub category: StatusCategory,
76 /// The source's own label for this status.
77 pub name: String,
78}
79
80/// The normalised status vocabulary shared across every source.
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
82#[serde(rename_all = "kebab-case")]
83pub enum StatusCategory {
84 /// Known about, not yet queued.
85 Backlog,
86 /// Queued, not yet started.
87 Todo,
88 /// Being worked on.
89 InProgress,
90 /// Finished.
91 Done,
92 /// Abandoned.
93 Cancelled,
94 /// The source reported a status this vocabulary cannot place.
95 Unknown,
96}
97
98/// A dependency between two items **of the same source**.
99///
100/// Cross-source edges are deliberately absent: relating an id in one system to an
101/// id in another needs state, and the engine is forbidden to hold any.
102#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
103pub struct DependencyEdge {
104 /// The item the edge starts at.
105 pub from: NativeId,
106 /// The item the edge points at.
107 pub to: NativeId,
108 /// What the edge means.
109 pub kind: DependencyKind,
110}
111
112/// What a [`DependencyEdge`] means.
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
114#[serde(rename_all = "kebab-case")]
115pub enum DependencyKind {
116 /// `from` must finish before `to` can.
117 Blocks,
118 /// `from` and `to` are linked without an ordering.
119 Related,
120}
121
122/// Which way a dependency query walks the graph.
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
124#[serde(rename_all = "kebab-case")]
125pub enum Direction {
126 /// What this item depends on — the forward edges every source can report.
127 DependsOn,
128 /// What depends on this item — emulated by the engine for a
129 /// [`ForwardOnly`](crate::DependencySupport::ForwardOnly) source.
130 DependedOnBy,
131}