Skip to main content

onetaskgraph_plugin_api/
capability.rs

1//! What a source declares it can do natively, so the engine can compensate for
2//! the rest instead of reducing every source to the weakest one's floor.
3
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7/// One source's declared abilities.
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
9pub struct Capabilities {
10    /// Whether the source has projects at all.
11    pub projects: Support,
12    /// Whether the source can select tasks belonging to no project.
13    pub orphan_tasks: Support,
14    /// Whether the source filters by label itself.
15    pub filter_by_label: Support,
16    /// Whether the source filters by status itself.
17    pub filter_by_status: Support,
18    /// Whether the source searches titles itself.
19    pub search_title: Support,
20    /// Whether the source searches bodies itself.
21    pub search_content: Support,
22    /// How far the source can walk task dependencies.
23    pub task_dependencies: DependencySupport,
24    /// How far the source can walk project dependencies.
25    pub project_dependencies: DependencySupport,
26    /// The largest page the source will serve. At least 1 — a source that serves no rows
27    /// cannot be paged, and every implementation rejects zero where its config is read.
28    // llmlint: ignore[invalid_states_unrepresentable] this field's wire shape is frozen by the plugin contract every source is written against; only the contract's owner may change it, and tightening it is post-build follow-up.
29    // llmlint: ignore[boundary_inputs_validated] the boundary that reads a user's configuration does reject zero — `CapabilityConfig::max_page_size` (onetaskgraph-in-memory/src/config.rs) is a `NonZeroU32` and names the setting when it refuses. What stays a plain `u32` is this frozen contract field, which only the contract's owner may narrow — AGENTS.md, "The plugin contract".
30    pub max_page_size: u32,
31}
32
33/// Whether a source applies one predicate itself.
34///
35/// Keeps an `Unsupported` variant because in-memory compensation for a filter or
36/// a search is sound: the engine over-fetches and narrows. Do not conflate this
37/// with [`DependencySupport`], which has no such variant.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
39#[serde(rename_all = "kebab-case")]
40pub enum Support {
41    /// The source applies this predicate itself.
42    Native,
43    /// The source ignores this predicate; the engine narrows the wider result.
44    Unsupported,
45}
46
47impl Support {
48    /// Whether the source applies the predicate itself.
49    #[must_use]
50    pub fn is_native(self) -> bool {
51        matches!(self, Self::Native)
52    }
53}
54
55/// How far a source can walk its own dependency edges.
56///
57/// There is deliberately **no** unsupported variant: dependency traversal is a
58/// guaranteed capability of this product, not one a source may opt out of. A
59/// source that cannot report an item's forward edges cannot implement
60/// [`TaskSource`](crate::TaskSource). The weakest declaration is
61/// [`ForwardOnly`](Self::ForwardOnly), which the engine answers in reverse by a
62/// bounded scan and reports as emulated.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
64#[serde(rename_all = "kebab-case")]
65pub enum DependencySupport {
66    /// The source answers both directions itself.
67    BothDirections,
68    /// The source answers forward edges; the engine emulates the reverse.
69    ForwardOnly,
70}
71
72impl DependencySupport {
73    /// Whether the source answers the reverse direction itself.
74    #[must_use]
75    pub fn answers_reverse(self) -> bool {
76        matches!(self, Self::BothDirections)
77    }
78}