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}