Skip to main content

onetaskgraph_plugin_api/
source.rs

1//! The two traits a plugin implements, and the secret lookup it is handed.
2
3use std::cmp::Ordering;
4use std::collections::BTreeMap;
5
6use schemars::{JsonSchema, Schema};
7use secrecy::SecretString;
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11use crate::{
12    Asset, AssetName, AssetWrite, AssetsWritten, Capabilities, Comment, CommentBody, CommentCopy,
13    DependencyEdge, Direction, Document, DocumentQuery, DocumentVersionCoverage, ItemKind,
14    ItemWrite, Label, MetadataKey, MetadataRecord, Metering, NativeId, NewComment, Page,
15    PageRequest, Priority, Project, ProjectQuery, ProjectVersionCoverage, SharedClock, SourceError,
16    SourceName, Status, StatusCategory, Task, TaskDetailRead, TaskQuery, TaskRef, TaskUpdate,
17    TaskUpdateOutcome, VersionCoverage, Visibility, WriteJournal, WriteSupport, WriteTarget,
18    assetless, commentless, documentless, unwritable, unwritable_field, unwritable_metadata,
19};
20
21/// Whether a source is answering right now.
22///
23/// # Placement is an open contract question
24///
25/// This type lives here because [`TaskSource::health`] returns it and the trait
26/// lives here: placing it in `onetaskgraph-core` would make this crate depend on
27/// the engine and invert the one direction the crate split exists to establish.
28/// The approved contract enumerates this crate's contents exhaustively and does
29/// not name `Health`, so the enumeration and the trait as written cannot both
30/// stand. Compiling forces the placement below; the resolution — add it to the
31/// enumeration, or redesign `health` so no such type crosses the boundary —
32/// belongs to the contract's owner, not to this crate. See `AGENTS.md`.
33#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
34// 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 a third time in this type's own doc comment above and in AGENTS.md's "Open contract question — `Health`": `Health`'s shape is approved contract text that `TaskSource::health` returns, so an enum here would change the serialized form and the trait six undispatched nodes implement. That is the contract owner's call, not this crate's.
35pub struct Health {
36    /// Whether the source answered.
37    ///
38    /// A bare `bool` beside an untyped `detail` cannot say that an unreachable source
39    /// must explain itself, or keep "reachable with a warning" apart from "reachable";
40    /// an enum carrying the detail in its unreachable variant would.
41    // llmlint: ignore[invalid_states_unrepresentable] SECOND PERMITTED REASON — this
42    // restates at this field the justification already recorded at
43    // `Capabilities.max_page_size` (capability.rs), `PageRequest.limit` (query.rs), this
44    // type's own doc comment above, and AGENTS.md's "Open contract question — `Health`":
45    // `Health`'s shape is approved contract text that `TaskSource::health` returns, so an
46    // enum here would change the serialized form and the trait six undispatched nodes
47    // implement. That is the contract owner's call, not this crate's.
48    // llmlint: ignore[boundary_inputs_validated] making "unreachable with no reason given" unrepresentable means an enum here, which changes the serialized form and the trait six undispatched nodes implement. Deferred to the contract's owner — AGENTS.md, "Open contract question — `Health`".
49    pub reachable: bool,
50    /// What the source said, when it said anything useful.
51    pub detail: Option<String>,
52}
53
54/// One configured source, as the engine drives it.
55///
56/// Dyn-compatible through `async_trait` because the engine holds
57/// `Vec<Box<dyn TaskSource>>` over heterogeneous plugins.
58///
59/// Three rules bind every implementation, and the engine's compensation is only
60/// correct while all three hold:
61///
62/// 1. **Apply** every predicate you declare [`Support::Native`](crate::Support::Native).
63/// 2. **Ignore** every [`Support`](crate::Support)-typed predicate you declare
64///    `Unsupported` — return the *wider* result set, never a narrower one.
65///    Silently dropping rows for a predicate you did not declare is the one
66///    failure no test above the plugin can catch.
67/// 3. Never return a silently empty dependency read. Rule 2 reaches the
68///    `Support`-typed *predicates* alone; a dependency read is always real, and so is a
69///    document read — [`Capabilities::documents`] says whether this source has documents
70///    at all, and a source that says it has none is never asked for one rather than
71///    answering an empty page.
72#[async_trait::async_trait]
73pub trait TaskSource: Send + Sync {
74    /// The plugin kind that built this source, for display and for plan output.
75    fn kind(&self) -> &'static str;
76
77    /// What this source applies itself. Read once per query by the engine.
78    fn capabilities(&self) -> Capabilities;
79
80    /// How `a`, a [`Task::version`] this source gave one of its tasks, stands to `b`, another
81    /// it gave the same task: `Greater` when `a` is the later, `Less` when it is the earlier,
82    /// `Equal` when nothing the token covers changed between them, and `None` when the two
83    /// cannot be put in order — a value this source cannot read, or two versions its token
84    /// says differ without saying which came first.
85    ///
86    /// A token's format and its order are the one source's that spells it, so the engine asks
87    /// the source that gave both rather than reading inside either.
88    ///
89    /// Defaulted to equality alone: two identical tokens are one version, and any two others
90    /// are unordered, which a reader takes as *changed, read it again* and never as
91    /// unchanged. A source whose token says which of two versions is the later overrides it.
92    fn order_versions(&self, a: &str, b: &str) -> Option<Ordering> {
93        (a == b).then_some(Ordering::Equal)
94    }
95
96    /// Which parts of a task this source's [`Task::version`] vouches for, which is what the
97    /// store's local cache may serve a task's parts from.
98    ///
99    /// Defaulted to no part, which keeps the cache from ever serving what a token was never
100    /// promised to cover: a source overrides it only for the parts its token covers, as
101    /// `docs/plugin-protocol.md` §4.13d states them.
102    fn version_coverage(&self) -> VersionCoverage {
103        VersionCoverage::none(format!(
104            "the {} plugin does not declare that its change token covers a task",
105            self.kind()
106        ))
107    }
108
109    /// Which parts of a task a copy may take this source's unchanged [`Task::version`] as
110    /// vouching for, when it decides to skip a task its destination already holds as written:
111    /// the task's fields, its comments when the copy carries them, and its forward edges. A copy
112    /// skips only a task whose source covers every one of those it writes.
113    ///
114    /// Defaulted to [`version_coverage`](Self::version_coverage). A source overrides it only to
115    /// state, with its measured bound, a claim about a copy's skip that is not one the cache may
116    /// serve from — as `docs/plugin-protocol.md` §4.13d does for the one plugin that does.
117    fn copy_version_coverage(&self) -> VersionCoverage {
118        self.version_coverage()
119    }
120
121    /// Which parts of a project this source's [`Project::version`] vouches for, on the terms
122    /// of [`version_coverage`](Self::version_coverage).
123    ///
124    /// Defaulted to no part, for the same reason.
125    fn project_version_coverage(&self) -> ProjectVersionCoverage {
126        ProjectVersionCoverage::none(format!(
127            "the {} plugin does not declare that its change token covers a project",
128            self.kind()
129        ))
130    }
131
132    /// Which parts of a document this source's [`Document::version`] vouches for, on the terms
133    /// of [`version_coverage`](Self::version_coverage).
134    ///
135    /// Defaulted to no part, for the same reason.
136    fn document_version_coverage(&self) -> DocumentVersionCoverage {
137        DocumentVersionCoverage::none(format!(
138            "the {} plugin does not declare that its change token covers a document",
139            self.kind()
140        ))
141    }
142
143    /// Tell this source the store's local cache now stands in front of it.
144    ///
145    /// Called exactly once, by the cache as it wraps the source, before any read and before
146    /// any coverage is read — so a source whose token needs a part only a wider selection
147    /// carries can widen its reads here, and declare the coverage that widening buys. Never
148    /// carried over the plugin protocol, as [`version_coverage`](Self::version_coverage) is
149    /// not.
150    ///
151    /// Defaulted to doing nothing: a source not overriding it reads exactly as it did.
152    fn attach_cache(&self) {}
153
154    /// The change token the response that answered this source's last
155    /// [`Direction::DependsOn`] read of task `id` in this process spelled, or `None` when it
156    /// spelled none.
157    ///
158    /// What lets the store's cache keep a task's forward edges by the version they were read
159    /// at when nothing else in the command read the task: those edges are served only to a pin
160    /// that version is at or after, and never vouch for the task's own fields. Synchronous, and
161    /// never carried over the plugin protocol, as [`version_coverage`](Self::version_coverage)
162    /// is not.
163    ///
164    /// Defaulted to `None`: a source whose dependency read spells no token keeps its edges
165    /// remote unless a read of the task itself vouched for them.
166    fn forward_edges_version(&self, id: &NativeId) -> Option<String> {
167        let _ = id;
168        None
169    }
170
171    /// What this source holds about task `id` — the item it read last in this process — that
172    /// a later write expecting that version of it could take instead of reading it again, or
173    /// `None` when it holds nothing of the kind.
174    ///
175    /// Opaque to the engine, which keeps it beside the cached task and hands it back through
176    /// [`prime_write`](Self::prime_write). Defaulted to `None`: a source whose writes read
177    /// nothing first has nothing to keep.
178    fn write_context(&self, id: &NativeId) -> Option<serde_json::Value> {
179        let _ = id;
180        None
181    }
182
183    /// What this source holds about the container every task of it shares — a board's field
184    /// and option ids — that a write could take instead of reading it, or `None`.
185    ///
186    /// Defaulted to `None`, on the terms of [`write_context`](Self::write_context).
187    fn board_context(&self) -> Option<serde_json::Value> {
188        None
189    }
190
191    /// Hold `context`, which [`write_context`](Self::write_context) gave for task `id` at a
192    /// version the caller expects it still at, and `board`, which
193    /// [`board_context`](Self::board_context) gave, so the next write of `id` takes them
194    /// instead of reading — answering whether it took them.
195    ///
196    /// A context this source cannot read is not taken, and the write then reads as it always
197    /// does. Defaulted to taking nothing.
198    fn prime_write(
199        &self,
200        id: &NativeId,
201        context: &serde_json::Value,
202        board: Option<&serde_json::Value>,
203    ) -> bool {
204        let _ = (id, context, board);
205        false
206    }
207
208    /// Drop whatever [`prime_write`](Self::prime_write) took for `id`, and the board context
209    /// with it, so the next write of it reads afresh.
210    fn unprime(&self, id: &NativeId) {
211        let _ = id;
212    }
213
214    /// Start recording every request this source sends, for [`end_journal`](Self::end_journal).
215    ///
216    /// The engine opens one journal around one write at a time. Defaulted to recording
217    /// nothing.
218    fn begin_journal(&self) {}
219
220    /// Stop recording, answering with every request sent since
221    /// [`begin_journal`](Self::begin_journal) — or `None` for a source that records none.
222    fn end_journal(&self) -> Option<WriteJournal> {
223        None
224    }
225
226    /// Whether the source is answering right now.
227    ///
228    /// # Errors
229    ///
230    /// Returns a [`SourceError`] when the check itself could not be made.
231    async fn health(&self) -> Result<Health, SourceError>;
232
233    /// Fetch one task by its native id, or `None` when there is no such task.
234    ///
235    /// # Errors
236    ///
237    /// Returns a [`SourceError`] when the source could not answer.
238    async fn get_task(&self, id: &NativeId) -> Result<Option<Task>, SourceError>;
239
240    /// Fetch several tasks by their native ids, each with the first page of its comments
241    /// when `comments` names the page to read — one answer per id, in the order they were
242    /// given.
243    ///
244    /// Each answer is what [`get_task`](Self::get_task) answers for that id, so a task that
245    /// is not there is `Ok(None)` and a read that failed fails that id alone. Its
246    /// [`TaskDetailRead::comments`] is what [`task_comments`](Self::task_comments) answers
247    /// for `comments`. The engine passes `comments` only to a source declaring
248    /// [`Capabilities::comments`](crate::Capabilities::comments) native.
249    ///
250    /// Defaulted to exactly those calls, item by item, so a source that implements nothing
251    /// here answers as it always did. A source that can read many items — or an item and its
252    /// comments — in fewer requests than that overrides it, which is the whole reason it
253    /// exists: a caller holding twenty ids pays for one batch rather than forty reads.
254    async fn get_task_details(
255        &self,
256        ids: &[NativeId],
257        comments: Option<&PageRequest>,
258    ) -> Vec<Result<Option<TaskDetailRead>, SourceError>> {
259        let mut read = Vec::with_capacity(ids.len());
260        for id in ids {
261            read.push(match self.get_task(id).await {
262                Ok(Some(task)) => {
263                    let comments = match comments {
264                        Some(page) => Some(self.task_comments(id, page).await),
265                        None => None,
266                    };
267                    Ok(Some(TaskDetailRead { task, comments }))
268                }
269                Ok(None) => Ok(None),
270                Err(error) => Err(error),
271            });
272        }
273        read
274    }
275
276    /// Fetch one project by its native id, or `None` when there is no such project.
277    ///
278    /// # Errors
279    ///
280    /// Returns a [`SourceError`] when the source could not answer.
281    async fn get_project(&self, id: &NativeId) -> Result<Option<Project>, SourceError>;
282
283    /// One page of the tasks matching `query`.
284    ///
285    /// # Errors
286    ///
287    /// Returns a [`SourceError`] when the source could not answer.
288    async fn query_tasks(
289        &self,
290        query: &TaskQuery,
291        page: &PageRequest,
292    ) -> Result<Page<Task>, SourceError>;
293
294    /// One page of the projects matching `query`.
295    ///
296    /// # Errors
297    ///
298    /// Returns a [`SourceError`] when the source could not answer.
299    async fn query_projects(
300        &self,
301        query: &ProjectQuery,
302        page: &PageRequest,
303    ) -> Result<Page<Project>, SourceError>;
304
305    /// One page of every label this source knows.
306    ///
307    /// # Errors
308    ///
309    /// Returns a [`SourceError`] when the source could not answer.
310    async fn labels(&self, page: &PageRequest) -> Result<Page<Label>, SourceError>;
311
312    /// One page of the task dependency edges at `id`, in `direction`.
313    ///
314    /// # Errors
315    ///
316    /// Returns a [`SourceError`] when the source could not answer.
317    async fn task_dependencies(
318        &self,
319        id: &NativeId,
320        direction: Direction,
321        page: &PageRequest,
322    ) -> Result<Page<DependencyEdge>, SourceError>;
323
324    /// One page of the project dependency edges at `id`, in `direction`.
325    ///
326    /// # Errors
327    ///
328    /// Returns a [`SourceError`] when the source could not answer.
329    async fn project_dependencies(
330        &self,
331        id: &NativeId,
332        direction: Direction,
333        page: &PageRequest,
334    ) -> Result<Page<DependencyEdge>, SourceError>;
335
336    /// Whether this source can be written through at all.
337    ///
338    /// Defaulted to [`WriteSupport::Unsupported`], which is what keeps this a read
339    /// interface for every source that has nothing to write into: one that cannot be
340    /// written needs no edit and keeps working. Read before a write is attempted, so a
341    /// copy naming such a source as its destination is refused before anything is read.
342    fn writes(&self) -> WriteSupport {
343        WriteSupport::Unsupported
344    }
345
346    /// Create or update one task, answering with the native id the destination holds it
347    /// under.
348    ///
349    /// A source declaring [`WriteSupport::Supported`] owes three things here. It refuses,
350    /// naming the field, anything it cannot represent rather than dropping it — including
351    /// a metadata key it cannot carry, which it names. It writes every other field it was
352    /// given. And it never creates when [`ItemWrite::target`] names an item it does not
353    /// hold.
354    ///
355    /// # Errors
356    ///
357    /// Returns [`SourceError::Refused`] when this source has no write side, when a field
358    /// or a metadata key cannot be represented, or when `target` names nothing here; and
359    /// whatever else the source could not do the write for.
360    async fn write_task(&self, write: &ItemWrite<Task>) -> Result<NativeId, SourceError> {
361        let _ = write;
362        Err(unwritable(self.kind()))
363    }
364
365    /// Create or update one project, on exactly the terms of
366    /// [`write_task`](Self::write_task).
367    ///
368    /// # Errors
369    ///
370    /// As [`write_task`](Self::write_task).
371    async fn write_project(&self, write: &ItemWrite<Project>) -> Result<NativeId, SourceError> {
372        let _ = write;
373        Err(unwritable(self.kind()))
374    }
375
376    /// Whether a write of an item of `kind` at `category` — over the item `target` names, or
377    /// creating one when it is `None` — would have a status to write, asked before the write
378    /// and changing nothing.
379    ///
380    /// A caller that has to put an item back when a later write of it fails — a copy, whose
381    /// journal records what an item held before overwriting it — asks this first, so a status
382    /// this source has no name for is refused while nothing has been recorded or written, and
383    /// nothing has to be put back. It answers what the write itself would refuse a status
384    /// with, in the same words, and sends no request a write of that status would not have
385    /// sent anyway: what it reads to answer, it holds for the write that follows.
386    ///
387    /// Defaulted to `Ok(())`: a source that does not answer it in advance still refuses the
388    /// status in its write, exactly as before.
389    ///
390    /// # Errors
391    ///
392    /// Returns [`SourceError::Refused`] naming the kind, the category and what is missing
393    /// when this source has no name for that status; and whatever else kept it from finding
394    /// out.
395    async fn check_status_write(
396        &self,
397        kind: ItemKind,
398        category: StatusCategory,
399        target: Option<&NativeId>,
400    ) -> Result<(), SourceError> {
401        let _ = (kind, category, target);
402        Ok(())
403    }
404
405    /// Set the status of one task this source holds, and change nothing else about it,
406    /// answering with the status as this source now reads it — or `None` when this source
407    /// holds no such task.
408    ///
409    /// The category lands where this source's own mapping sends it, exactly as a
410    /// [`write_task`](Self::write_task) of a task in that category would: a category this
411    /// source has disabled is refused in the words a write of it is refused with. Title,
412    /// content, labels, metadata, dependencies, [`Task::delivers`], [`Task::delivered_by`],
413    /// project and comments are left exactly as they are.
414    ///
415    /// Defaulted to [`unwritable_field`], which is what keeps this an addition rather than a
416    /// break: a source that cannot write a status on its own needs no edit and refuses by
417    /// saying so. A source declaring [`WriteSupport::Unsupported`] is never asked.
418    ///
419    /// [`Task::delivers`]: crate::Task::delivers
420    /// [`Task::delivered_by`]: crate::Task::delivered_by
421    ///
422    /// # Errors
423    ///
424    /// Returns [`SourceError::Refused`] when this source cannot write a status, or cannot
425    /// write this one; and whatever else the source could not do the write for.
426    async fn set_task_status(
427        &self,
428        id: &NativeId,
429        category: StatusCategory,
430    ) -> Result<Option<Status>, SourceError> {
431        let _ = (id, category);
432        Err(unwritable_field(self.kind(), "status"))
433    }
434
435    /// Set the status of one task this source holds, as [`set_task_status`](Self::set_task_status)
436    /// does, and answer with the whole task as this source now reads it — its
437    /// [`Task::delivers`] among what a caller keeping delivered tasks in step needs — or `None`
438    /// when this source holds no such task.
439    ///
440    /// Defaulted to exactly the two calls a caller would otherwise make: [`get_task`] first,
441    /// answering `None` with nothing written when it does, then `set_task_status`, the status
442    /// it answers put on the task read. A source whose status write answers the whole task in
443    /// the same round trip overrides it to save the read.
444    ///
445    /// [`Task::delivers`]: crate::Task::delivers
446    /// [`get_task`]: Self::get_task
447    ///
448    /// # Errors
449    ///
450    /// As [`get_task`](Self::get_task) and [`set_task_status`](Self::set_task_status).
451    async fn set_task_status_reading(
452        &self,
453        id: &NativeId,
454        category: StatusCategory,
455    ) -> Result<Option<Task>, SourceError> {
456        let Some(mut task) = self.get_task(id).await? else {
457            return Ok(None);
458        };
459        let Some(status) = self.set_task_status(id, category).await? else {
460            return Ok(None);
461        };
462        task.status = status;
463        Ok(Some(task))
464    }
465
466    /// Set the priority of one task this source holds, and change nothing else about it,
467    /// answering with the priority as this source now reads it — or `None` when this source
468    /// holds no such task.
469    ///
470    /// [`Priority::None`] clears the priority. Title, content, status, labels, metadata,
471    /// repositories, dependencies and comments are left exactly as they are. Nothing about
472    /// the task's status moves, so the engine re-evaluates no delivered task after it.
473    ///
474    /// Defaulted to [`unwritable_field`], which is what keeps this an addition rather than a
475    /// break. A source declaring [`WriteSupport::Unsupported`], or declaring
476    /// [`Capabilities::priority`] unsupported, is never asked.
477    ///
478    /// # Errors
479    ///
480    /// Returns [`SourceError::Refused`] when this source cannot write a priority, or cannot
481    /// write this one — a board with no option for it, naming the option; and whatever else
482    /// the source could not do the write for.
483    async fn set_task_priority(
484        &self,
485        id: &NativeId,
486        priority: Priority,
487    ) -> Result<Option<Priority>, SourceError> {
488        let _ = (id, priority);
489        Err(unwritable_field(self.kind(), "priority"))
490    }
491
492    /// Replace the content of one task this source holds with `content`, byte for byte, and
493    /// change nothing else about it — or answer `None` when this source holds no such task.
494    ///
495    /// The content is [`Task::content`] exactly as this source reports it: what a later read
496    /// answers there is `content`. Where the source keeps something else inside the same
497    /// backend field — a metadata block in an issue body — that is kept as it was, and so is
498    /// every other member: title, status, priority, labels, metadata, repositories,
499    /// dependencies, project and comments. Nothing about the task's status moves, so the
500    /// engine re-evaluates no delivered task after it.
501    ///
502    /// Defaulted to [`unwritable_field`] on exactly the terms of
503    /// [`set_task_status`](Self::set_task_status). A source declaring
504    /// [`WriteSupport::Unsupported`] is never asked.
505    ///
506    /// # Errors
507    ///
508    /// Returns [`SourceError::Refused`] when this source cannot write a task's content on its
509    /// own, or cannot represent this one; and whatever else the source could not do the write
510    /// for.
511    async fn set_task_content(
512        &self,
513        id: &NativeId,
514        content: &str,
515    ) -> Result<Option<()>, SourceError> {
516        let _ = (id, content);
517        Err(unwritable_field(self.kind(), "content"))
518    }
519
520    /// Replace the [`Task::delivered_by`] of one task this source holds, and change nothing
521    /// else about it — or answer `None` when this source holds no such task.
522    ///
523    /// Every entry is a qualified id, and the list is the whole of it: what the task held
524    /// there before is replaced, not merged. It is the store's to keep in step — the engine
525    /// calls this whenever it writes a task's [`Task::delivers`] — and nothing a person types
526    /// reaches it directly.
527    ///
528    /// Defaulted to [`unwritable_field`] on exactly the terms of
529    /// [`set_task_status`](Self::set_task_status).
530    ///
531    /// [`Task::delivers`]: crate::Task::delivers
532    /// [`Task::delivered_by`]: crate::Task::delivered_by
533    ///
534    /// # Errors
535    ///
536    /// Returns [`SourceError::Refused`] when this source cannot hold the list, and whatever
537    /// else it could not do the write for.
538    async fn set_delivered_by(
539        &self,
540        id: &NativeId,
541        delivered_by: &[TaskRef],
542    ) -> Result<Option<()>, SourceError> {
543        let _ = (id, delivered_by);
544        Err(unwritable_field(self.kind(), "delivered_by"))
545    }
546
547    /// Set one key of the metadata of one task this source holds, and change nothing else
548    /// about it, answering with the task as this source reads it back after the write — or
549    /// `None` when this source holds no such task.
550    ///
551    /// The key is added when the task does not hold it and replaced when it does; every other
552    /// metadata key, and every other field of the task, is left exactly as it was. A `value`
553    /// the task already holds under `key` is a write that changes nothing, and a source owes
554    /// it no write at all. The answer is a read, not an echo: what the engine reports as the
555    /// value is what the returned task holds under `key`.
556    ///
557    /// Nothing about the task's status or its [`Task::delivers`] moves, so the engine
558    /// re-evaluates no delivered task after this write.
559    ///
560    /// Defaulted to [`unwritable_metadata`] on exactly the terms of
561    /// [`set_task_status`](Self::set_task_status): a source that cannot write one key on its
562    /// own needs no edit and refuses by saying so. A source declaring
563    /// [`WriteSupport::Unsupported`] is never asked.
564    ///
565    /// [`Task::delivers`]: crate::Task::delivers
566    ///
567    /// # Errors
568    ///
569    /// Returns [`SourceError::Refused`] when this source cannot write one key of a task's
570    /// metadata on its own, or cannot write this one without changing something else; and
571    /// whatever else the source could not do the write for.
572    async fn set_task_metadata(
573        &self,
574        id: &NativeId,
575        key: &MetadataKey,
576        value: &Value,
577    ) -> Result<Option<Task>, SourceError> {
578        let _ = (id, key, value);
579        Err(unwritable_metadata(self.kind(), MetadataRecord::Task))
580    }
581
582    /// Apply a targeted update to one task this source holds — every field `update` names, and
583    /// nothing else — answering with a [`TaskUpdateOutcome`], or `None` when this source holds
584    /// no such task.
585    ///
586    /// The outcome is everything the engine reports about the call, so it reads nothing of its
587    /// own: the task as this source reads it once the update landed — from the one read it made
588    /// and what it then wrote, where its backend answers a write with what it holds — the
589    /// fields it actually wrote, and the `delivers` the task held before.
590    ///
591    /// A field already holding the requested value is sent no write, and an update in which
592    /// nothing differs sends no write at all. Every field `update` leaves unnamed — labels,
593    /// repositories, project, [`Task::delivered_by`] and comments always among them — is left
594    /// exactly as it is. An update naming one metadata key both to set and to remove is refused
595    /// before anything is written, in the words of [`TaskUpdate::consistent`].
596    ///
597    /// Defaulted to [`TaskUpdate::rewrite`]: read the task, apply the update, and — only when
598    /// anything differs — [`write_task`](Self::write_task) it with its `target` set. That keeps
599    /// this an addition rather than a break, and a source that predates it behaves correctly and
600    /// is merely not minimal; one whose backend can take a field on its own owes an override
601    /// that sends only what differs. A source declaring [`WriteSupport::Unsupported`] is never
602    /// asked.
603    ///
604    /// [`Task::delivered_by`]: crate::Task::delivered_by
605    ///
606    /// # Errors
607    ///
608    /// Returns [`SourceError::Refused`] when the update contradicts itself, when a field it
609    /// names cannot be represented here — a category this source has disabled included — and
610    /// whatever else the source could not do the write for.
611    async fn update_task(
612        &self,
613        id: &NativeId,
614        update: &TaskUpdate,
615    ) -> Result<Option<TaskUpdateOutcome>, SourceError> {
616        update.rewrite(self, id).await
617    }
618
619    /// Set one key of the metadata of one project this source holds, on exactly the terms of
620    /// [`set_task_metadata`](Self::set_task_metadata).
621    ///
622    /// # Errors
623    ///
624    /// As [`set_task_metadata`](Self::set_task_metadata).
625    async fn set_project_metadata(
626        &self,
627        id: &NativeId,
628        key: &MetadataKey,
629        value: &Value,
630    ) -> Result<Option<Project>, SourceError> {
631        let _ = (id, key, value);
632        Err(unwritable_metadata(self.kind(), MetadataRecord::Project))
633    }
634
635    /// Set one key of the metadata of one document this source holds, on exactly the terms of
636    /// [`set_task_metadata`](Self::set_task_metadata).
637    ///
638    /// A source declaring [`Capabilities::documents`] unsupported is never asked, exactly as
639    /// it is never asked for a document read.
640    ///
641    /// # Errors
642    ///
643    /// As [`set_task_metadata`](Self::set_task_metadata).
644    async fn set_document_metadata(
645        &self,
646        id: &NativeId,
647        key: &MetadataKey,
648        value: &Value,
649    ) -> Result<Option<Document>, SourceError> {
650        let _ = (id, key, value);
651        Err(unwritable_metadata(self.kind(), MetadataRecord::Document))
652    }
653
654    /// Remove one task this destination holds, so a copy that could not finish can put
655    /// the destination back the way it found it.
656    ///
657    /// This is not a verb of the product: nothing a user types deletes anything, and a
658    /// copy never deletes an item it did not itself create in the run that is failing.
659    /// It exists because a copy is either complete or it never happened — a half-written
660    /// project has to be run again, and the re-run is the mutation burst that trips a
661    /// hosted destination's rate limiter. Undoing this run's own creates is what removes
662    /// that retry at source.
663    ///
664    /// A source declaring [`WriteSupport::Supported`] owes a real implementation, for the
665    /// reason it owes [`write_task`](Self::write_task) one: the engine will create items
666    /// there, so it has to be able to remove the ones it created. An `id` naming nothing
667    /// is **not** an error — the item is already gone, which is the state this asks for.
668    ///
669    /// # Errors
670    ///
671    /// Returns [`SourceError::Refused`] when this source has no write side, and whatever
672    /// else the source could not remove the item for.
673    async fn delete_task(&self, id: &NativeId) -> Result<(), SourceError> {
674        let _ = id;
675        Err(unwritable(self.kind()))
676    }
677
678    /// Remove one project this destination holds, on exactly the terms of
679    /// [`delete_task`](Self::delete_task).
680    ///
681    /// # Errors
682    ///
683    /// As [`delete_task`](Self::delete_task).
684    async fn delete_project(&self, id: &NativeId) -> Result<(), SourceError> {
685        let _ = id;
686        Err(unwritable(self.kind()))
687    }
688
689    /// Fetch one document by its native id, or `None` when there is no such document.
690    ///
691    /// Defaulted to [`documentless`], which is what keeps documents an addition rather
692    /// than a break: a source with none needs no edit, keeps working, and says so in the
693    /// same words every other document-free source does. A source that has documents
694    /// declares [`Support::Native`](crate::Support::Native) for
695    /// [`Capabilities::documents`] and owes a real implementation here, because that
696    /// declaration is what makes the engine ask.
697    ///
698    /// # Errors
699    ///
700    /// Returns [`SourceError::Refused`] when this source has no documents, and whatever
701    /// else the source could not answer for.
702    async fn get_document(&self, id: &NativeId) -> Result<Option<Document>, SourceError> {
703        let _ = id;
704        Err(documentless(self.kind()))
705    }
706
707    /// One page of the documents matching `query`.
708    ///
709    /// Defaulted on exactly the terms of [`get_document`](Self::get_document). A source
710    /// with no documents refuses rather than answering an empty page: an empty page reads
711    /// as a source that has documents and holds none matching, which is the one wrong
712    /// answer this method can give.
713    ///
714    /// # Errors
715    ///
716    /// As [`get_document`](Self::get_document).
717    async fn query_documents(
718        &self,
719        query: &DocumentQuery,
720        page: &PageRequest,
721    ) -> Result<Page<Document>, SourceError> {
722        let _ = (query, page);
723        Err(documentless(self.kind()))
724    }
725
726    /// Create or update one document, on exactly the terms of
727    /// [`write_task`](Self::write_task).
728    ///
729    /// # Errors
730    ///
731    /// As [`write_task`](Self::write_task).
732    async fn write_document(&self, write: &ItemWrite<Document>) -> Result<NativeId, SourceError> {
733        let _ = write;
734        Err(unwritable(self.kind()))
735    }
736
737    /// Remove one document this destination holds, on exactly the terms of
738    /// [`delete_task`](Self::delete_task).
739    ///
740    /// # Errors
741    ///
742    /// As [`delete_task`](Self::delete_task).
743    async fn delete_document(&self, id: &NativeId) -> Result<(), SourceError> {
744        let _ = id;
745        Err(unwritable(self.kind()))
746    }
747
748    /// One page of the comments on `task`, oldest first, or `None` when this source holds
749    /// no such task.
750    ///
751    /// Defaulted to [`commentless`], which is what keeps comments an addition rather than a
752    /// break: a source with none needs no edit and keeps working. A source whose tasks have
753    /// comments declares [`Support::Native`](crate::Support::Native) for
754    /// [`Capabilities::comments`] and owes a real implementation of all four comment methods,
755    /// because that declaration is what makes the engine ask.
756    ///
757    /// "No such task" is `None` rather than an error, exactly as it is for
758    /// [`get_task`](Self::get_task); a task that exists and has no comments is an empty page.
759    ///
760    /// # Errors
761    ///
762    /// Returns [`SourceError::Refused`] when this source has no comments, and whatever else
763    /// the source could not answer for.
764    async fn task_comments(
765        &self,
766        task: &NativeId,
767        page: &PageRequest,
768    ) -> Result<Option<Page<Comment>>, SourceError> {
769        let _ = (task, page);
770        Err(commentless(self.kind()))
771    }
772
773    /// [`get_task_details`](Self::get_task_details) as a copy reads the tasks it carries: each
774    /// with the first page of its comments `comments` names — and, where the same request can
775    /// carry them, what this source needs to answer [`task_dependencies`](Self::task_dependencies)
776    /// for it without another, since a copy reads every task's edges next.
777    ///
778    /// Defaulted to `get_task_details(ids, Some(comments))`, which is exactly what it answers.
779    /// A source whose detail read is priced by what it selects overrides it with a read that
780    /// also carries the edges, so a copy of a changed task costs its detail read and nothing
781    /// more; one whose reads are free has no reason to.
782    async fn get_task_details_for_copy(
783        &self,
784        ids: &[NativeId],
785        comments: &PageRequest,
786    ) -> Vec<Result<Option<TaskDetailRead>, SourceError>> {
787        self.get_task_details(ids, Some(comments)).await
788    }
789
790    /// Add one comment to `task`, answering with the comment as the source now holds it, or
791    /// `None` when this source holds no such task.
792    ///
793    /// The body is stored byte for byte. A source that records the author itself refuses a
794    /// [`NewComment::author`] rather than dropping it, naming why; a source that cannot
795    /// represent the body refuses it, naming why, rather than escaping it into something
796    /// else.
797    ///
798    /// # Errors
799    ///
800    /// Returns [`SourceError::Refused`] when this source has no comments or cannot be
801    /// written, when it cannot record what it was given, and whatever else it could not do
802    /// the write for.
803    async fn add_comment(
804        &self,
805        task: &NativeId,
806        comment: &NewComment,
807    ) -> Result<Option<Comment>, SourceError> {
808        let _ = (task, comment);
809        Err(commentless(self.kind()))
810    }
811
812    /// Replace the body of the comment `comment` on `task`, answering with the comment as the
813    /// source now holds it, or `None` when this source holds no such task or that task has no
814    /// such comment.
815    ///
816    /// Only the body and the time it last changed move: the id, the author and the time it
817    /// was written are the comment's own.
818    ///
819    /// # Errors
820    ///
821    /// As [`add_comment`](Self::add_comment).
822    async fn edit_comment(
823        &self,
824        task: &NativeId,
825        comment: &NativeId,
826        body: &CommentBody,
827    ) -> Result<Option<Comment>, SourceError> {
828        let _ = (task, comment, body);
829        Err(commentless(self.kind()))
830    }
831
832    /// Remove the comment `comment` from `task`, answering with the id it removed, or `None`
833    /// when this source holds no such task or that task has no such comment.
834    ///
835    /// Unlike [`delete_task`](Self::delete_task), this *is* a verb of the product — a person
836    /// removes a comment they posted — so a comment that is not there is reported as `None`
837    /// for the engine to refuse by name, rather than treated as already gone.
838    ///
839    /// # Errors
840    ///
841    /// As [`add_comment`](Self::add_comment).
842    async fn delete_comment(
843        &self,
844        task: &NativeId,
845        comment: &NativeId,
846    ) -> Result<Option<NativeId>, SourceError> {
847        let _ = (task, comment);
848        Err(commentless(self.kind()))
849    }
850
851    /// Write one comment a copy carries onto `task`: a new one when `existing` is `None`, and
852    /// otherwise the comment `existing`, which a copy wrote before, made to say `copy` — or
853    /// `None` when this source holds no such task or no such comment.
854    ///
855    /// The answer reports [`Comment::origin`] as `copy.origin`, however the source keeps it.
856    ///
857    /// **Defaulted to the hidden marker**, which is right for every source that records a
858    /// comment's author and times itself: the body is written through
859    /// [`add_comment`](Self::add_comment) or [`edit_comment`](Self::edit_comment) with
860    /// [`CommentOrigin::mark`](crate::CommentOrigin::mark) appended, and read back
861    /// [`unmarked`](Comment::unmarked). A source that can store the origin natively — and so
862    /// takes the origin's own id, author and times for the copy — overrides this.
863    ///
864    /// # Errors
865    ///
866    /// As [`add_comment`](Self::add_comment).
867    async fn write_comment_copy(
868        &self,
869        task: &NativeId,
870        existing: Option<&NativeId>,
871        copy: &CommentCopy,
872    ) -> Result<Option<Comment>, SourceError> {
873        let body = CommentBody::new(copy.origin.qualified()?.mark(copy.body.as_str()))?;
874        let written = match existing {
875            Some(comment) => self.edit_comment(task, comment, &body).await?,
876            None => {
877                self.add_comment(task, &NewComment { body, author: None })
878                    .await?
879            }
880        };
881        Ok(written.map(Comment::unmarked))
882    }
883
884    /// Whether this source keeps template answers beside its items at all.
885    ///
886    /// What lets a regenerate tell an item whose answers went missing — a block deleted by
887    /// hand, which the regenerate writes back — from an item of a source that never keeps any,
888    /// where a missing answer is no difference. Defaulted to `false`, as
889    /// [`task_template_answers`](Self::task_template_answers) is defaulted to `None`; a source
890    /// that keeps answers answers `true` here.
891    fn keeps_template_answers(&self) -> bool {
892        false
893    }
894
895    /// The template answers the task `id` was last rendered from, as this source keeps them
896    /// beside the task — or `None` when it keeps none for it, or holds no such task.
897    ///
898    /// Keeping answers is a source's own choice, never an obligation: a source whose items
899    /// are one record in a hosted system has no room beside the item that is not the item, and
900    /// answers written into its content or its metadata would duplicate what the content
901    /// already says. Defaulted to `None`, which is what keeps this an addition rather than a
902    /// break. A source that keeps them owes three things: they are in neither
903    /// [`Task::content`] nor [`Task::metadata`], they are written only by
904    /// [`write_task_rendered`](Self::write_task_rendered) and
905    /// [`set_task_rendering`](Self::set_task_rendering), and they come back here exactly as
906    /// they were written, JSON types intact.
907    ///
908    /// # Errors
909    ///
910    /// Returns a [`SourceError`] when the source could not answer, a record whose answers it
911    /// cannot read included.
912    async fn task_template_answers(
913        &self,
914        id: &NativeId,
915    ) -> Result<Option<BTreeMap<String, Value>>, SourceError> {
916        let _ = id;
917        Ok(None)
918    }
919
920    /// The template answers the document `id` was last rendered from, on exactly the terms of
921    /// [`task_template_answers`](Self::task_template_answers).
922    ///
923    /// # Errors
924    ///
925    /// As [`task_template_answers`](Self::task_template_answers).
926    async fn document_template_answers(
927        &self,
928        id: &NativeId,
929    ) -> Result<Option<BTreeMap<String, Value>>, SourceError> {
930        let _ = id;
931        Ok(None)
932    }
933
934    /// The template answers the project `id` was last rendered from, on exactly the terms of
935    /// [`task_template_answers`](Self::task_template_answers).
936    ///
937    /// # Errors
938    ///
939    /// As [`task_template_answers`](Self::task_template_answers).
940    async fn project_template_answers(
941        &self,
942        id: &NativeId,
943    ) -> Result<Option<BTreeMap<String, Value>>, SourceError> {
944        let _ = id;
945        Ok(None)
946    }
947
948    /// Create or update one task exactly as [`write_task`](Self::write_task) does, keeping
949    /// `answers` — the answers its content was rendered from — beside it in the same write
950    /// where this source keeps answers at all.
951    ///
952    /// The task arrives carrying its provenance under [`MetadataKey::TEMPLATE_KEY`] like any
953    /// other metadata entry. Defaulted to [`write_task`](Self::write_task) alone: a source that
954    /// keeps no answers writes the task and nothing beside it, which is the whole of what it
955    /// owes. A source that keeps them writes the task and the answers together, so a reader
956    /// never finds one without the other.
957    ///
958    /// # Errors
959    ///
960    /// As [`write_task`](Self::write_task).
961    async fn write_task_rendered(
962        &self,
963        write: &ItemWrite<Task>,
964        answers: &BTreeMap<String, Value>,
965    ) -> Result<NativeId, SourceError> {
966        let _ = answers;
967        self.write_task(write).await
968    }
969
970    /// Create or update one document, on exactly the terms of
971    /// [`write_task_rendered`](Self::write_task_rendered).
972    ///
973    /// # Errors
974    ///
975    /// As [`write_document`](Self::write_document).
976    async fn write_document_rendered(
977        &self,
978        write: &ItemWrite<Document>,
979        answers: &BTreeMap<String, Value>,
980    ) -> Result<NativeId, SourceError> {
981        let _ = answers;
982        self.write_document(write).await
983    }
984
985    /// Create or update one project, on exactly the terms of
986    /// [`write_task_rendered`](Self::write_task_rendered).
987    ///
988    /// # Errors
989    ///
990    /// As [`write_project`](Self::write_project).
991    async fn write_project_rendered(
992        &self,
993        write: &ItemWrite<Project>,
994        answers: &BTreeMap<String, Value>,
995    ) -> Result<NativeId, SourceError> {
996        let _ = answers;
997        self.write_project(write).await
998    }
999
1000    /// Replace one task's rendering — its content, byte for byte, its
1001    /// [`MetadataKey::TEMPLATE_KEY`] entry, set to `provenance`, and the answers it keeps
1002    /// beside the task where it keeps any — in one write, and change nothing else about it;
1003    /// or answer `None` when this source holds no such task.
1004    ///
1005    /// The content is [`Task::content`] exactly as a later read reports it, on the terms of
1006    /// [`set_task_content`](Self::set_task_content). Title, status, priority, labels, every
1007    /// other metadata entry, repositories, dependencies, [`Task::delivers`],
1008    /// [`Task::delivered_by`], project and comments are left exactly as they are. The write is
1009    /// one write: a reader sees the task as it was or as it is now, never a new content beside
1010    /// the old provenance or the old answers.
1011    ///
1012    /// Defaulted to [`unwritable_field`] on exactly the terms of
1013    /// [`set_task_status`](Self::set_task_status). A source declaring
1014    /// [`WriteSupport::Unsupported`] is never asked.
1015    ///
1016    /// [`Task::delivers`]: crate::Task::delivers
1017    /// [`Task::delivered_by`]: crate::Task::delivered_by
1018    ///
1019    /// # Errors
1020    ///
1021    /// Returns [`SourceError::Refused`] when this source cannot replace a task's rendering on
1022    /// its own, or cannot represent this one; and whatever else the source could not do the
1023    /// write for.
1024    async fn set_task_rendering(
1025        &self,
1026        id: &NativeId,
1027        content: &str,
1028        provenance: &Value,
1029        answers: &BTreeMap<String, Value>,
1030    ) -> Result<Option<()>, SourceError> {
1031        let _ = (id, content, provenance, answers);
1032        Err(unwritable_field(self.kind(), "rendering"))
1033    }
1034
1035    /// Replace one document's rendering, on exactly the terms of
1036    /// [`set_task_rendering`](Self::set_task_rendering). A source declaring
1037    /// [`Capabilities::documents`] unsupported is never asked.
1038    ///
1039    /// # Errors
1040    ///
1041    /// As [`set_task_rendering`](Self::set_task_rendering).
1042    async fn set_document_rendering(
1043        &self,
1044        id: &NativeId,
1045        content: &str,
1046        provenance: &Value,
1047        answers: &BTreeMap<String, Value>,
1048    ) -> Result<Option<()>, SourceError> {
1049        let _ = (id, content, provenance, answers);
1050        Err(SourceError::Refused {
1051            message: format!(
1052                "the {} plugin cannot write a document's rendering on its own",
1053                self.kind()
1054            ),
1055        })
1056    }
1057
1058    /// Replace one project's rendering, on exactly the terms of
1059    /// [`set_task_rendering`](Self::set_task_rendering): its content, its
1060    /// [`MetadataKey::TEMPLATE_KEY`] entry and the answers kept beside it change, and its
1061    /// title, status, labels, every other metadata entry, repositories and dependencies are
1062    /// left exactly as they are.
1063    ///
1064    /// # Errors
1065    ///
1066    /// As [`set_task_rendering`](Self::set_task_rendering).
1067    async fn set_project_rendering(
1068        &self,
1069        id: &NativeId,
1070        content: &str,
1071        provenance: &Value,
1072        answers: &BTreeMap<String, Value>,
1073    ) -> Result<Option<()>, SourceError> {
1074        let _ = (id, content, provenance, answers);
1075        Err(SourceError::Refused {
1076            message: format!(
1077                "the {} plugin cannot write a project's rendering on its own",
1078                self.kind()
1079            ),
1080        })
1081    }
1082
1083    /// The image assets the task `id` holds, or none when it holds none or this source holds
1084    /// no such task.
1085    ///
1086    /// Defaulted to none, which is what keeps assets an addition rather than a break: a source
1087    /// that keeps no assets needs no edit and reports every record as holding none. A source
1088    /// declaring [`Capabilities::assets`] native answers what it holds for the record, in any
1089    /// order — the engine orders them by the content's first reference.
1090    ///
1091    /// # Errors
1092    ///
1093    /// Returns a [`SourceError`] when the source could not answer.
1094    async fn task_assets(&self, id: &NativeId) -> Result<Vec<Asset>, SourceError> {
1095        let _ = id;
1096        Ok(Vec::new())
1097    }
1098
1099    /// The image assets the document `id` holds, on exactly the terms of
1100    /// [`task_assets`](Self::task_assets).
1101    ///
1102    /// # Errors
1103    ///
1104    /// As [`task_assets`](Self::task_assets).
1105    async fn document_assets(&self, id: &NativeId) -> Result<Vec<Asset>, SourceError> {
1106        let _ = id;
1107        Ok(Vec::new())
1108    }
1109
1110    /// The bytes of the asset `name` the task `id` holds, or `None` when it holds no such asset
1111    /// or this source holds no such task.
1112    ///
1113    /// Defaulted to `None`, on the terms of [`task_assets`](Self::task_assets).
1114    ///
1115    /// # Errors
1116    ///
1117    /// Returns a [`SourceError`] when the source could not answer.
1118    async fn task_asset(
1119        &self,
1120        id: &NativeId,
1121        name: &AssetName,
1122    ) -> Result<Option<Vec<u8>>, SourceError> {
1123        let _ = (id, name);
1124        Ok(None)
1125    }
1126
1127    /// The bytes of the asset `name` the document `id` holds, on exactly the terms of
1128    /// [`task_asset`](Self::task_asset).
1129    ///
1130    /// # Errors
1131    ///
1132    /// As [`task_asset`](Self::task_asset).
1133    async fn document_asset(
1134        &self,
1135        id: &NativeId,
1136        name: &AssetName,
1137    ) -> Result<Option<Vec<u8>>, SourceError> {
1138        let _ = (id, name);
1139        Ok(None)
1140    }
1141
1142    /// Create or update one task carrying image assets, in one write: the task as
1143    /// [`write_task`](Self::write_task) writes it — or, with `answers`, as
1144    /// [`write_task_rendered`](Self::write_task_rendered) does — and `assets` as the record's
1145    /// whole asset set, answering with where it landed and the content as stored.
1146    ///
1147    /// Only a source declaring [`Capabilities::assets`] native is asked, and the engine has
1148    /// already checked that every reference in the content names one of `assets` and every one
1149    /// of `assets` is referenced. A source that keeps the bytes beside the record stores each
1150    /// payload under its name, removes every asset the record held that `assets` does not
1151    /// name, and answers the content unchanged. A source that serves them at a URL uploads each
1152    /// payload that carries bytes, reuses the URL [`AssetWrite::recorded_assets`] records for
1153    /// each that does not, and writes the record through
1154    /// [`serve_asset_references`](crate::serve_asset_references) — so its references point at
1155    /// those URLs, its [`MetadataKey::ASSETS_KEY`] records each `{sha256, url}`, and a
1156    /// rendering's `body_digest` stays true of what landed — answering the content it wrote.
1157    ///
1158    /// Defaulted to [`assetless`], which is what a source with no assets answers.
1159    ///
1160    /// # Errors
1161    ///
1162    /// As [`write_task`](Self::write_task), and [`SourceError::Refused`] from a source that
1163    /// keeps no assets.
1164    async fn write_task_with_assets(
1165        &self,
1166        write: &ItemWrite<Task>,
1167        answers: Option<&BTreeMap<String, Value>>,
1168        assets: &AssetWrite,
1169    ) -> Result<AssetsWritten, SourceError> {
1170        let _ = (write, answers, assets);
1171        Err(assetless(self.kind()))
1172    }
1173
1174    /// Create or update one document carrying image assets, on exactly the terms of
1175    /// [`write_task_with_assets`](Self::write_task_with_assets).
1176    ///
1177    /// # Errors
1178    ///
1179    /// As [`write_task_with_assets`](Self::write_task_with_assets).
1180    async fn write_document_with_assets(
1181        &self,
1182        write: &ItemWrite<Document>,
1183        answers: Option<&BTreeMap<String, Value>>,
1184        assets: &AssetWrite,
1185    ) -> Result<AssetsWritten, SourceError> {
1186        let _ = (write, answers, assets);
1187        Err(assetless(self.kind()))
1188    }
1189
1190    /// Replace one task's rendering as [`set_task_rendering`](Self::set_task_rendering) does,
1191    /// and its asset set as [`write_task_with_assets`](Self::write_task_with_assets) does, in
1192    /// one write; or answer `None` when this source holds no such task.
1193    ///
1194    /// Defaulted to [`assetless`].
1195    ///
1196    /// # Errors
1197    ///
1198    /// As [`set_task_rendering`](Self::set_task_rendering), and [`SourceError::Refused`] from a
1199    /// source that keeps no assets.
1200    async fn set_task_rendering_with_assets(
1201        &self,
1202        id: &NativeId,
1203        content: &str,
1204        provenance: &Value,
1205        answers: &BTreeMap<String, Value>,
1206        assets: &AssetWrite,
1207    ) -> Result<Option<AssetsWritten>, SourceError> {
1208        let _ = (id, content, provenance, answers, assets);
1209        Err(assetless(self.kind()))
1210    }
1211
1212    /// Replace one document's rendering and its asset set, on exactly the terms of
1213    /// [`set_task_rendering_with_assets`](Self::set_task_rendering_with_assets).
1214    ///
1215    /// # Errors
1216    ///
1217    /// As [`set_task_rendering_with_assets`](Self::set_task_rendering_with_assets).
1218    async fn set_document_rendering_with_assets(
1219        &self,
1220        id: &NativeId,
1221        content: &str,
1222        provenance: &Value,
1223        answers: &BTreeMap<String, Value>,
1224        assets: &AssetWrite,
1225    ) -> Result<Option<AssetsWritten>, SourceError> {
1226        let _ = (id, content, provenance, answers, assets);
1227        Err(assetless(self.kind()))
1228    }
1229
1230    /// What this source has sent to its backend since it was built and what that spent, or
1231    /// `None` when it does not meter its own requests.
1232    ///
1233    /// Defaulted to `None`, which is what keeps metering an addition rather than a break: a
1234    /// source that does not count its requests needs no edit, and is reported as not
1235    /// metering rather than as having spent nothing. A source that answers owes a running
1236    /// total — see [`Metering`] — because what one command spent is read as the difference
1237    /// between two readings.
1238    ///
1239    /// # Errors
1240    ///
1241    /// Returns a [`SourceError`] when the reading itself could not be taken. A caller
1242    /// reports such a source as not metering; what a command cost is never a reason for the
1243    /// command to fail.
1244    async fn metering(&self) -> Result<Option<Metering>, SourceError> {
1245        Ok(None)
1246    }
1247
1248    /// End one command: drop everything held that a person can change.
1249    ///
1250    /// A command is one unit of work — one invocation of the binary, or whatever a caller
1251    /// holding one engine for many units of work calls between them. Within a command a
1252    /// source may hold what it read and reuse it; across this call it may not. After it
1253    /// returns, a source answers no read and bases no write on item content, statuses, board
1254    /// contents or search results it held before the call. It may keep only identifiers and
1255    /// vocabulary that stay valid in normal use — a repository's node id, a team's workflow
1256    /// states — and only where a lookup that misses one re-reads rather than refusing.
1257    ///
1258    /// Defaulted to doing nothing, which is right for a source that holds nothing between
1259    /// calls or holds only what the rule above lets it keep. A source that holds more owes
1260    /// an override. Never called by the engine on its own: the binary's one command is its
1261    /// whole process, so only a caller that outlives a command calls it.
1262    ///
1263    /// # Errors
1264    ///
1265    /// Returns a [`SourceError`] when the source could not be sure it dropped what it held —
1266    /// ordinarily a hosted plugin that did not answer. A caller should not go on to the next
1267    /// command on that source's earlier reads.
1268    async fn end_command(&self) -> Result<(), SourceError> {
1269        Ok(())
1270    }
1271
1272    /// Who can read where `target` lands, read live from the backend.
1273    ///
1274    /// This is the destination's reality, which the engine holds a declaration against
1275    /// before it lets a write through: a source a configuration declares private receives an
1276    /// item that may only be written somewhere private only while this answers
1277    /// [`Visibility::Private`]. The engine asks immediately before the write it is about, so
1278    /// a source must not answer from anything it read for an earlier write unless its
1279    /// backend's visibility cannot change in between.
1280    ///
1281    /// Defaulted to [`Visibility::Unknown`], which is the answer of a source that cannot say
1282    /// and is never treated as private. A source on this machine answers `Private` — what it
1283    /// writes stays on the host — and a hosted source reads its backend.
1284    ///
1285    /// # Errors
1286    ///
1287    /// Returns a [`SourceError`] when the backend could not be asked or would not answer —
1288    /// a credential lacking the scope the read needs among them. An unreadable visibility is
1289    /// never guessed: the engine refuses the write.
1290    async fn visibility(&self, target: &WriteTarget<'_>) -> Result<Visibility, SourceError> {
1291        let _ = target;
1292        Ok(Visibility::Unknown)
1293    }
1294}
1295
1296/// The factory that turns one configuration block into a live [`TaskSource`].
1297///
1298/// Having the compile-time registry and the subprocess seam be the same shape is
1299/// the whole reason this is a trait rather than a free function.
1300pub trait SourcePlugin: Send + Sync + 'static {
1301    /// The name a configuration document's `plugin:` field names.
1302    fn kind(&self) -> &'static str;
1303
1304    /// The JSON Schema for this plugin's own `config:` block.
1305    fn config_schema(&self) -> Schema;
1306
1307    /// Build a live source from one configuration block.
1308    ///
1309    /// `name` is the configured source's name, for error messages only — a
1310    /// plugin never learns it for any other purpose.
1311    ///
1312    /// # Errors
1313    ///
1314    /// Returns [`SourceError::Config`] when `config` is not valid for this
1315    /// plugin, or [`SourceError::Auth`] when a named credential is absent.
1316    fn build(
1317        &self,
1318        name: &SourceName,
1319        config: &serde_json::Value,
1320        secrets: &dyn SecretResolver,
1321    ) -> Result<Box<dyn TaskSource>, SourceError>;
1322
1323    /// Build a live source from one configuration block, handing it the clock it paces and
1324    /// backs off on.
1325    ///
1326    /// The binary builds every in-process source through this, with the process's one
1327    /// [`SharedClock`] — [`system_clock`](crate::system_clock) unless a test asked for
1328    /// simulated time. Defaulted to [`build`](Self::build), which ignores the clock: a plugin
1329    /// that never waits needs no edit, and one that does overrides this and routes its waits
1330    /// through `clock`.
1331    ///
1332    /// # Errors
1333    ///
1334    /// As [`build`](Self::build).
1335    fn build_with_clock(
1336        &self,
1337        name: &SourceName,
1338        config: &serde_json::Value,
1339        secrets: &dyn SecretResolver,
1340        clock: SharedClock,
1341    ) -> Result<Box<dyn TaskSource>, SourceError> {
1342        let _ = clock;
1343        self.build(name, config, secrets)
1344    }
1345
1346    /// The fields of this plugin's `config:` block that name a filesystem path, as dotted
1347    /// paths into that block.
1348    ///
1349    /// A relative value at one of these, **supplied by a configuration document**, is
1350    /// resolved against the directory holding that document before [`Self::build`] sees it;
1351    /// supplied through the environment or a flag it keeps resolving against the process
1352    /// working directory, because there is no document to rebase it on. A plugin is handed
1353    /// values and no origins, so this declaration is the only way it can say which of its
1354    /// own fields that rule reaches.
1355    ///
1356    /// Defaulted to none, which is what keeps this an addition rather than a break: a
1357    /// plugin whose block holds no path needs no edit, and a caller asks every plugin
1358    /// rather than keeping a table of which ones answer.
1359    // llmlint: ignore[invalid_states_unrepresentable] The identity of a configuration field
1360    // is a name, and no type can make a wrong one unrepresentable here: every string is a
1361    // syntactically valid dotted path, so a newtype would validate nothing and would only
1362    // move where a name that is not a field of *this* plugin is accepted. What decides that
1363    // is whether the name is a property of the schema `config_schema` publishes — a
1364    // per-plugin fact no shared type can hold — so the gate is per plugin and executable:
1365    // `document_relative_fields_are_fields_this_plugin_declares` in
1366    // `onetaskgraph-local-md/tests/plugin.rs`, which a plugin adding a declaration owes its
1367    // own copy of.
1368    fn document_relative_paths(&self) -> &'static [&'static str] {
1369        &[]
1370    }
1371
1372    /// What names the remote store a source built from `config` reads — a board's owner and
1373    /// number, a Linear endpoint and team — or `None` for a source with no remote.
1374    ///
1375    /// The store's local cache of a remote source lives under a digest of this, so every
1376    /// process configuring the same remote shares one cache and no two remotes share one.
1377    /// Defaulted to `None`, which is what a source keeping its work on this machine — a
1378    /// folder of Markdown — answers: it has nothing to cache, and its cache setting is
1379    /// ignored. A `config` this plugin would refuse answers `None` too.
1380    fn remote_identity(&self, config: &serde_json::Value) -> Option<String> {
1381        let _ = config;
1382        None
1383    }
1384}
1385
1386/// How a plugin reads the credential its configuration names.
1387///
1388/// A configuration document never carries a credential value, only the name of
1389/// the environment variable holding it.
1390pub trait SecretResolver: Send + Sync {
1391    /// The value of `var`, or `None` when nothing defines it.
1392    ///
1393    /// The returned value is never logged and never appears in `Debug` output.
1394    fn get(&self, var: &str) -> Option<SecretString>;
1395}