vissue_core/views.rs
1//! Typed issue views shared by JSON output and later control clients.
2
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5use std::path::PathBuf;
6
7use crate::model::IssueHeading;
8
9/// One parsed heading plus the `issues.org` it came from.
10#[derive(Debug, Clone)]
11pub struct IssueRec {
12 /// Project directory name the heading lives under.
13 pub project: String,
14 /// Parsed heading, including body and logbook.
15 pub heading: IssueHeading,
16 /// Absolute path of the project's `issues.org`.
17 pub path: PathBuf,
18 /// File-level tags and `#+TAGS:` groups from the preamble.
19 pub tag_settings: crate::org::TagSettings,
20}
21
22/// Filters for [`crate::catalog::CatalogService::issues_rows`].
23#[derive(Debug, Clone, Default, PartialEq, Eq)]
24pub struct ListQuery {
25 /// Restrict to this project name (case-insensitive).
26 pub project: Option<String>,
27 /// Restrict to this TODO keyword.
28 pub state: Option<String>,
29 /// Keep only TODO or STARTED issues with no open blocker.
30 pub ready: bool,
31 /// Case-insensitive substring over id, title, tags, and properties.
32 pub query: Option<String>,
33 /// Cap the result after sorting.
34 pub limit: Option<usize>,
35 /// Drop this many leading rows after sorting.
36 pub offset: Option<usize>,
37}
38
39/// One list/ready row: the fields a board or JSON client paints.
40#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
41pub struct IssueRow {
42 /// Issue id, `<project>-<suffix>`.
43 pub id: String,
44 /// TODO keyword on the heading.
45 pub state: String,
46 /// Priority cookie as a one-character string.
47 pub priority: String,
48 /// Heading title, without tags.
49 pub title: String,
50 /// Project the heading lives in.
51 pub project: String,
52 /// Ids listed in `:BLOCKED_BY:`.
53 pub blocked_by: Vec<String>,
54 /// Identity holding the issue, when claimed.
55 pub claimed_by: Option<String>,
56 /// Org timestamp of the claim.
57 pub claimed_at: Option<String>,
58 /// `:PARENT:` id, when set.
59 #[serde(default)]
60 pub parent: Option<String>,
61}
62
63/// One issue as a detail card: properties, tags, file range, body, and logbook.
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
65pub struct IssueDetail {
66 /// Issue id, `<project>-<suffix>`.
67 pub id: String,
68 /// Project the heading lives in.
69 pub project: String,
70 /// Heading title, without tags.
71 pub title: String,
72 /// TODO keyword on the heading.
73 pub state: String,
74 /// Priority cookie as a one-character string.
75 pub priority: String,
76 /// Property drawer, including planning keys held in the map.
77 pub properties: BTreeMap<String, String>,
78 /// Tags written on the heading itself.
79 pub org_tags: Vec<String>,
80 /// Combined heading tags and `:VISSUE_TAGS:`.
81 pub tags: Vec<String>,
82 /// Ids listed in `:BLOCKED_BY:`.
83 pub blocked_by: Vec<String>,
84 /// Deed accessions listed in `:DEEDS:`.
85 ///
86 /// Typed beside `blocked_by` rather than left in `properties` for the same
87 /// reason: a client that paints what an issue produced should not have to
88 /// know how the drawer spells a list.
89 #[serde(default)]
90 pub deeds: Vec<String>,
91 /// `:PARENT:` id, when set.
92 pub parent: Option<String>,
93 /// Identity holding the issue, when claimed.
94 pub claimed_by: Option<String>,
95 /// Org timestamp of the claim.
96 pub claimed_at: Option<String>,
97 /// `path:line_start-line_end` of the heading in its `issues.org`.
98 pub file: String,
99 /// 1-based first line of the heading in the file.
100 pub line_start: usize,
101 /// 1-based last line of the heading in the file.
102 pub line_end: usize,
103 /// Prose under the heading, without the property drawer or logbook.
104 ///
105 /// Carried here so a caller that fetched the detail has what the issue
106 /// asks for, rather than a file path and a line range to go read.
107 #[serde(default)]
108 pub body: String,
109 /// Logbook lines on the heading, newest first.
110 #[serde(default)]
111 pub logbook: Vec<LogbookLine>,
112}
113
114/// One logbook line on a detail card: note, state flip, or raw CLOCK.
115#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
116pub struct LogbookLine {
117 /// Inactive org timestamp on the line, or empty for a raw CLOCK row.
118 #[serde(default)]
119 pub timestamp: String,
120 /// Previous TODO keyword on a state flip.
121 #[serde(default, skip_serializing_if = "Option::is_none")]
122 pub from_state: Option<String>,
123 /// New TODO keyword on a state flip.
124 #[serde(default, skip_serializing_if = "Option::is_none")]
125 pub to_state: Option<String>,
126 /// Folded note text, when the line is a note rather than a state flip.
127 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub note: Option<String>,
129 /// Opaque drawer line preserved verbatim (a `CLOCK:` entry, say).
130 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub raw: Option<String>,
132}
133
134/// One live claim: who holds the issue and for how long.
135#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
136pub struct ClaimRow {
137 /// Issue id.
138 pub id: String,
139 /// Project the heading lives in.
140 pub project: String,
141 /// TODO keyword on the heading.
142 pub state: String,
143 /// Priority cookie as a one-character string.
144 pub priority: String,
145 /// Identity holding the issue.
146 pub holder: Option<String>,
147 /// Org timestamp of the claim.
148 pub claimed_at: Option<String>,
149 /// Whole days since the claim; `-1` when the stamp does not parse.
150 pub age_days: i64,
151 /// Heading title.
152 pub title: String,
153}
154
155/// A capped, secret-screened slice of a heading's on-disk range.
156#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
157pub struct Excerpt {
158 /// Issue id.
159 pub id: String,
160 /// Path of the `issues.org` the heading lives in.
161 pub file: String,
162 /// 1-based first line of the heading.
163 pub line_start: usize,
164 /// 1-based last line of the heading.
165 pub line_end: usize,
166 /// Excerpt text, or a suppression notice when credential-shaped.
167 pub text: String,
168 /// Whether `text` is a suppression notice rather than the heading.
169 pub suppressed: bool,
170}
171
172/// One search match: the heading plus a short snippet of the hit.
173#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
174pub struct SearchHit {
175 /// Issue id.
176 pub id: String,
177 /// Project the heading lives in.
178 pub project: String,
179 /// TODO keyword on the heading.
180 pub state: String,
181 /// Priority cookie as a one-character string.
182 pub priority: String,
183 /// Heading title.
184 pub title: String,
185 /// First matching line, capped.
186 pub snippet: String,
187}
188
189/// One dated row: a deadline or scheduled date on an open issue.
190#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
191pub struct AgendaRow {
192 /// Calendar date as `YYYY-MM-DD`.
193 pub date: String,
194 /// `deadline` or `scheduled`.
195 pub kind: String,
196 /// Days past the date; `0` when it is today or still upcoming.
197 pub overdue_days: i64,
198 /// Issue id.
199 pub id: String,
200 /// Project the heading lives in.
201 pub project: String,
202 /// TODO keyword on the heading.
203 pub state: String,
204 /// Priority cookie as a one-character string.
205 pub priority: String,
206 /// Heading title.
207 pub title: String,
208}
209
210/// A parent/child subtree node, with the issue's own blockers.
211#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
212pub struct TreeNode {
213 /// Issue id.
214 pub id: String,
215 /// TODO keyword on the heading.
216 pub state: String,
217 /// Heading title.
218 pub title: String,
219 /// Direct children by `:PARENT:`.
220 pub children: Vec<TreeNode>,
221 /// Ids listed in `:BLOCKED_BY:`.
222 pub blocked_by: Vec<String>,
223}
224
225/// One ranked related-issue hit, with the evidence that produced the score.
226#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
227pub struct RelatedHit {
228 /// Issue id.
229 pub id: String,
230 /// Project the heading lives in.
231 pub project: String,
232 /// TODO keyword on the heading.
233 pub state: String,
234 /// Heading title.
235 pub title: String,
236 /// Combined evidence score; higher is a closer match.
237 pub score: f64,
238 /// Named reasons (`blocked_by`, `term:foo`, `org_distance:1`, ...).
239 pub evidence: Vec<String>,
240}
241
242/// The working set for one issue: the plan it sits in, the products of the work
243/// it waits on, and what it has produced so far.
244///
245/// Assembled from declared edges rather than from similarity, so the set is the
246/// answer and not a ranked guess at it. Nothing here is scored, and nothing is
247/// dropped for being far away: the partial order already said what this issue
248/// needs.
249#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
250pub struct Recall {
251 /// Issue the working set is for.
252 pub id: String,
253 /// Project the heading lives in.
254 pub project: String,
255 /// TODO keyword on the heading.
256 pub state: String,
257 /// Heading title.
258 pub title: String,
259 /// Parent chain, outermost plan first, without this issue.
260 pub plan: Vec<WalkHit>,
261 /// What this issue waits on and where it came from, each with its products.
262 pub inputs: Vec<RecallInput>,
263 /// Deed accessions this issue has already cited.
264 pub produced: Vec<String>,
265 /// Heading body: the dispatch note the work is done from.
266 pub body: String,
267}
268
269/// What a plan's children hold, child by child.
270///
271/// A report rather than an average. Weighting children is a judgement the
272/// tracker has no basis for, a child that settled split has no single position
273/// to fold in, and a child nobody voted on is absent rather than neutral, so
274/// there is no honest number to reduce these rows to.
275#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
276pub struct PlanConsensus {
277 /// The plan the children hang under.
278 pub plan: String,
279 /// Heading title of the plan.
280 pub title: String,
281 /// One row per child, in the order `children` walks them.
282 pub children: Vec<ChildConsensus>,
283}
284
285/// One child of a plan, and what its own ballots settled on.
286#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
287pub struct ChildConsensus {
288 /// Issue id.
289 pub id: String,
290 /// TODO keyword on the heading.
291 pub state: String,
292 /// Heading title.
293 pub title: String,
294 /// Ballots cast on this child.
295 pub ballots: usize,
296 /// How the child settled, when anyone voted on it.
297 pub settling: Option<crate::consensus::Settling>,
298 /// The choice the child holds and its share, when one leads.
299 pub holds: Option<(String, f64)>,
300}
301
302impl PlanConsensus {
303 /// Children nobody has voted on.
304 #[must_use]
305 pub fn unvoted(&self) -> Vec<&ChildConsensus> {
306 self.children.iter().filter(|c| c.ballots == 0).collect()
307 }
308
309 /// Children whose own reviewers split into groups that do not listen to
310 /// each other.
311 #[must_use]
312 pub fn split(&self) -> Vec<&ChildConsensus> {
313 self.children
314 .iter()
315 .filter(|c| c.settling == Some(crate::consensus::Settling::Split))
316 .collect()
317 }
318
319 /// Whether a gate over this plan should pass.
320 ///
321 /// False when any child settled split or carries no ballots. Both are rows
322 /// a person has to go read, and neither is something a parent can decide
323 /// on their behalf, which is the whole argument for this being a report.
324 #[must_use]
325 pub fn settled(&self) -> bool {
326 self.split().is_empty() && self.unvoted().is_empty()
327 }
328
329 /// The distinct choices the settled children hold.
330 ///
331 /// One entry means the children that were voted on point the same way.
332 /// More than one means they disagree with each other, which is the case a
333 /// per-child report exists to make visible and an average would hide.
334 #[must_use]
335 pub fn positions(&self) -> Vec<&str> {
336 let mut seen: Vec<&str> = Vec::new();
337 for child in &self.children {
338 if let Some((choice, _)) = &child.holds
339 && !seen.contains(&choice.as_str())
340 {
341 seen.push(choice.as_str());
342 }
343 }
344 seen
345 }
346}
347
348/// One declared input to an issue, and the deeds that input produced.
349#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
350pub struct RecallInput {
351 /// Issue id.
352 pub id: String,
353 /// Project the heading lives in.
354 pub project: String,
355 /// TODO keyword on the heading.
356 pub state: String,
357 /// Heading title.
358 pub title: String,
359 /// Which declared edge made this an input (`blocked-by`, `discovered-from`).
360 pub relation: String,
361 /// Deed accessions cited on that heading.
362 pub deeds: Vec<String>,
363 /// A capped excerpt of that input's heading, when one was asked for.
364 ///
365 /// What the input concluded lives in its body: `append` writes the report
366 /// there, and the deed names the product rather than the reasoning. Off
367 /// unless asked, because the common case wants the accessions and a working
368 /// set that pastes four screens of prose is one nobody reads.
369 ///
370 /// Screened and capped by the same path `body-excerpt` uses, so an input
371 /// whose body looks like credential material is suppressed here too.
372 #[serde(default, skip_serializing_if = "Option::is_none")]
373 pub excerpt: Option<String>,
374 /// The most recent note in that input's logbook, when it has one.
375 ///
376 /// What happened to the input, for the case where it produced no deed. A
377 /// blocker that closed without naming a product would otherwise hand the
378 /// next unit its title and nothing else.
379 pub last_note: Option<String>,
380}
381
382/// One related heading from a walk: children, ancestors, impact, or backlinks.
383#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
384pub struct WalkHit {
385 /// Issue id.
386 pub id: String,
387 /// Project the heading lives in.
388 pub project: String,
389 /// TODO keyword on the heading.
390 pub state: String,
391 /// Heading title.
392 pub title: String,
393 /// How this heading relates to the walk root (`child`, `ancestor`, ...).
394 pub relation: String,
395}