vissue-core 0.16.0

Plain-text issue tracking over per-project orgmode files: model, store, queries, and org projection
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
//! Typed issue views shared by JSON output and later control clients.

use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::path::PathBuf;

use crate::model::IssueHeading;

/// One parsed heading plus the `issues.org` it came from.
#[derive(Debug, Clone)]
pub struct IssueRec {
    /// Project directory name the heading lives under.
    pub project: String,
    /// Parsed heading, including body and logbook.
    pub heading: IssueHeading,
    /// Absolute path of the project's `issues.org`.
    pub path: PathBuf,
    /// File-level tags and `#+TAGS:` groups from the preamble.
    pub tag_settings: crate::org::TagSettings,
}

/// Filters for [`crate::catalog::CatalogService::issues_rows`].
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ListQuery {
    /// Restrict to this project name (case-insensitive).
    pub project: Option<String>,
    /// Restrict to this TODO keyword.
    pub state: Option<String>,
    /// Keep only TODO or STARTED issues with no open blocker.
    pub ready: bool,
    /// Case-insensitive substring over id, title, tags, and properties.
    pub query: Option<String>,
    /// Cap the result after sorting.
    pub limit: Option<usize>,
    /// Drop this many leading rows after sorting.
    pub offset: Option<usize>,
}

/// One list/ready row: the fields a board or JSON client paints.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct IssueRow {
    /// Issue id, `<project>-<suffix>`.
    pub id: String,
    /// TODO keyword on the heading.
    pub state: String,
    /// Priority cookie as a one-character string.
    pub priority: String,
    /// Heading title, without tags.
    pub title: String,
    /// Project the heading lives in.
    pub project: String,
    /// Ids listed in `:BLOCKED_BY:`.
    pub blocked_by: Vec<String>,
    /// Identity holding the issue, when claimed.
    pub claimed_by: Option<String>,
    /// Org timestamp of the claim.
    pub claimed_at: Option<String>,
    /// `:PARENT:` id, when set.
    #[serde(default)]
    pub parent: Option<String>,
}

/// One issue as a detail card: properties, tags, file range, body, and logbook.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct IssueDetail {
    /// Issue id, `<project>-<suffix>`.
    pub id: String,
    /// Project the heading lives in.
    pub project: String,
    /// Heading title, without tags.
    pub title: String,
    /// TODO keyword on the heading.
    pub state: String,
    /// Priority cookie as a one-character string.
    pub priority: String,
    /// Property drawer, including planning keys held in the map.
    pub properties: BTreeMap<String, String>,
    /// Tags written on the heading itself.
    pub org_tags: Vec<String>,
    /// Combined heading tags and `:VISSUE_TAGS:`.
    pub tags: Vec<String>,
    /// Ids listed in `:BLOCKED_BY:`.
    pub blocked_by: Vec<String>,
    /// Deed accessions listed in `:DEEDS:`.
    ///
    /// Typed beside `blocked_by` rather than left in `properties` for the same
    /// reason: a client that paints what an issue produced should not have to
    /// know how the drawer spells a list.
    #[serde(default)]
    pub deeds: Vec<String>,
    /// `:PARENT:` id, when set.
    pub parent: Option<String>,
    /// Identity holding the issue, when claimed.
    pub claimed_by: Option<String>,
    /// Org timestamp of the claim.
    pub claimed_at: Option<String>,
    /// `path:line_start-line_end` of the heading in its `issues.org`.
    pub file: String,
    /// 1-based first line of the heading in the file.
    pub line_start: usize,
    /// 1-based last line of the heading in the file.
    pub line_end: usize,
    /// Prose under the heading, without the property drawer or logbook.
    ///
    /// Carried here so a caller that fetched the detail has what the issue
    /// asks for, rather than a file path and a line range to go read.
    #[serde(default)]
    pub body: String,
    /// Logbook lines on the heading, newest first.
    #[serde(default)]
    pub logbook: Vec<LogbookLine>,
}

/// One logbook line on a detail card: note, state flip, or raw CLOCK.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct LogbookLine {
    /// Inactive org timestamp on the line, or empty for a raw CLOCK row.
    #[serde(default)]
    pub timestamp: String,
    /// Previous TODO keyword on a state flip.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub from_state: Option<String>,
    /// New TODO keyword on a state flip.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub to_state: Option<String>,
    /// Folded note text, when the line is a note rather than a state flip.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub note: Option<String>,
    /// Opaque drawer line preserved verbatim (a `CLOCK:` entry, say).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub raw: Option<String>,
}

/// One live claim: who holds the issue and for how long.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ClaimRow {
    /// Issue id.
    pub id: String,
    /// Project the heading lives in.
    pub project: String,
    /// TODO keyword on the heading.
    pub state: String,
    /// Priority cookie as a one-character string.
    pub priority: String,
    /// Identity holding the issue.
    pub holder: Option<String>,
    /// Org timestamp of the claim.
    pub claimed_at: Option<String>,
    /// Whole days since the claim; `-1` when the stamp does not parse.
    pub age_days: i64,
    /// Heading title.
    pub title: String,
}

/// A capped, secret-screened slice of a heading's on-disk range.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Excerpt {
    /// Issue id.
    pub id: String,
    /// Path of the `issues.org` the heading lives in.
    pub file: String,
    /// 1-based first line of the heading.
    pub line_start: usize,
    /// 1-based last line of the heading.
    pub line_end: usize,
    /// Excerpt text, or a suppression notice when credential-shaped.
    pub text: String,
    /// Whether `text` is a suppression notice rather than the heading.
    pub suppressed: bool,
}

/// One search match: the heading plus a short snippet of the hit.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SearchHit {
    /// Issue id.
    pub id: String,
    /// Project the heading lives in.
    pub project: String,
    /// TODO keyword on the heading.
    pub state: String,
    /// Priority cookie as a one-character string.
    pub priority: String,
    /// Heading title.
    pub title: String,
    /// First matching line, capped.
    pub snippet: String,
}

/// One dated row: a deadline or scheduled date on an open issue.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AgendaRow {
    /// Calendar date as `YYYY-MM-DD`.
    pub date: String,
    /// `deadline`, `scheduled`, or `appointment` (a plain active stamp).
    pub kind: String,
    /// Days past the date; `0` when it is today or still upcoming.
    pub overdue_days: i64,
    /// Issue id.
    pub id: String,
    /// Project the heading lives in.
    pub project: String,
    /// TODO keyword on the heading.
    pub state: String,
    /// Priority cookie as a one-character string.
    pub priority: String,
    /// Heading title.
    pub title: String,
}

/// A parent/child subtree node, with the issue's own blockers.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TreeNode {
    /// Issue id.
    pub id: String,
    /// TODO keyword on the heading.
    pub state: String,
    /// Heading title.
    pub title: String,
    /// Direct children by `:PARENT:`.
    pub children: Vec<TreeNode>,
    /// Ids listed in `:BLOCKED_BY:`.
    pub blocked_by: Vec<String>,
}

/// One ranked related-issue hit, with the evidence that produced the score.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct RelatedHit {
    /// Issue id.
    pub id: String,
    /// Project the heading lives in.
    pub project: String,
    /// TODO keyword on the heading.
    pub state: String,
    /// Heading title.
    pub title: String,
    /// Combined evidence score; higher is a closer match.
    pub score: f64,
    /// Named reasons (`blocked_by`, `term:foo`, `org_distance:1`, ...).
    pub evidence: Vec<String>,
}

/// The working set for one issue: the plan it sits in, the products of the work
/// it waits on, and what it has produced so far.
///
/// Assembled from declared edges rather than from similarity, so the set is the
/// answer and not a ranked guess at it. Nothing here is scored, and nothing is
/// dropped for being far away: the partial order already said what this issue
/// needs.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Recall {
    /// Issue the working set is for.
    pub id: String,
    /// Project the heading lives in.
    pub project: String,
    /// TODO keyword on the heading.
    pub state: String,
    /// Heading title.
    pub title: String,
    /// Org deadline stamp, when the heading carries one.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub deadline: Option<String>,
    /// Org scheduled stamp, when the heading carries one.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub scheduled: Option<String>,
    /// Parent chain, outermost plan first, without this issue.
    pub plan: Vec<WalkHit>,
    /// What this issue waits on and where it came from, each with its products.
    pub inputs: Vec<RecallInput>,
    /// Deed accessions this issue has already cited.
    pub produced: Vec<String>,
    /// Heading body: the dispatch note the work is done from.
    pub body: String,
}

/// What a plan's children hold, child by child.
///
/// A report rather than an average. Weighting children is a judgement the
/// tracker has no basis for, a child that settled split has no single position
/// to fold in, and a child nobody voted on is absent rather than neutral, so
/// there is no honest number to reduce these rows to.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PlanConsensus {
    /// The plan the children hang under.
    pub plan: String,
    /// Heading title of the plan.
    pub title: String,
    /// One row per child, in the order `children` walks them.
    pub children: Vec<ChildConsensus>,
}

/// One child of a plan, and what its own ballots settled on.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ChildConsensus {
    /// Issue id.
    pub id: String,
    /// TODO keyword on the heading.
    pub state: String,
    /// Heading title.
    pub title: String,
    /// Ballots cast on this child.
    pub ballots: usize,
    /// How the child settled, when anyone voted on it.
    pub settling: Option<crate::consensus::Settling>,
    /// The choice the child holds and its share, when one leads.
    pub holds: Option<(String, f64)>,
}

impl PlanConsensus {
    /// Children nobody has voted on.
    #[must_use]
    pub fn unvoted(&self) -> Vec<&ChildConsensus> {
        self.children.iter().filter(|c| c.ballots == 0).collect()
    }

    /// Children whose own reviewers split into groups that do not listen to
    /// each other.
    #[must_use]
    pub fn split(&self) -> Vec<&ChildConsensus> {
        self.children
            .iter()
            .filter(|c| c.settling == Some(crate::consensus::Settling::Split))
            .collect()
    }

    /// Whether a gate over this plan should pass.
    ///
    /// False when any child settled split or carries no ballots. Both are rows
    /// a person has to go read, and neither is something a parent can decide
    /// on their behalf, which is the whole argument for this being a report.
    #[must_use]
    pub fn settled(&self) -> bool {
        self.split().is_empty() && self.unvoted().is_empty()
    }

    /// The distinct choices the settled children hold.
    ///
    /// One entry means the children that were voted on point the same way.
    /// More than one means they disagree with each other, which is the case a
    /// per-child report exists to make visible and an average would hide.
    #[must_use]
    pub fn positions(&self) -> Vec<&str> {
        let mut seen: Vec<&str> = Vec::new();
        for child in &self.children {
            if let Some((choice, _)) = &child.holds
                && !seen.contains(&choice.as_str())
            {
                seen.push(choice.as_str());
            }
        }
        seen
    }
}

/// One declared input to an issue, and the deeds that input produced.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RecallInput {
    /// Issue id.
    pub id: String,
    /// Project the heading lives in.
    pub project: String,
    /// TODO keyword on the heading.
    pub state: String,
    /// Heading title.
    pub title: String,
    /// Which declared edge made this an input (`blocked-by`, `discovered-from`).
    pub relation: String,
    /// Deed accessions cited on that heading.
    pub deeds: Vec<String>,
    /// A capped excerpt of that input's heading, when one was asked for.
    ///
    /// What the input concluded lives in its body: `append` writes the report
    /// there, and the deed names the product rather than the reasoning. Off
    /// unless asked, because the common case wants the accessions and a working
    /// set that pastes four screens of prose is one nobody reads.
    ///
    /// Screened and capped by the same path `body-excerpt` uses, so an input
    /// whose body looks like credential material is suppressed here too.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub excerpt: Option<String>,
    /// The most recent note in that input's logbook, when it has one.
    ///
    /// What happened to the input, for the case where it produced no deed. A
    /// blocker that closed without naming a product would otherwise hand the
    /// next unit its title and nothing else.
    pub last_note: Option<String>,
}

/// One related heading from a walk: children, ancestors, impact, or backlinks.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WalkHit {
    /// Issue id.
    pub id: String,
    /// Project the heading lives in.
    pub project: String,
    /// TODO keyword on the heading.
    pub state: String,
    /// Heading title.
    pub title: String,
    /// How this heading relates to the walk root (`child`, `ancestor`, ...).
    pub relation: String,
}