Skip to main content

onetaskgraph_core/engine/
copy.rs

1//! The copy verb: one item out of one source and into another, by the rules that make a
2//! second copy an update rather than a duplicate.
3//!
4//! Correspondence lives on the item and never in a table. A copied item carries
5//! [`GlobalId::ORIGIN_KEY`], whose value is the qualified id it was copied from, and the
6//! two match rules below read exactly that — so nothing here is written down outside the
7//! plugin that owns the item, and the invariant this engine is built around is untouched.
8//!
9//! 1. **Follow the origin.** An item already carrying an origin whose source half is the
10//!    destination names the destination item *directly*, and the copy updates it. This is
11//!    the half that makes an edit's copy-back an update: the local file came from the
12//!    remote item and knows which one.
13//! 2. **Search by origin.** Otherwise the destination is scanned, one page at a time, for
14//!    an item whose origin is the id being copied. Found, the copy updates it; not found,
15//!    the copy creates one carrying that origin.
16//!
17//! Which rule found the item decides what the copy records there. A copy that got its
18//! target from rule 1 is a copy-back — the destination is the *original*, and the item
19//! being copied is the one that came from it — so the destination keeps the origin it
20//! already holds, holding none included. Every other copy records the id it was copied
21//! from. See [`recorded`] for what stamping a copy-back's own id there costs.
22//!
23//! A destination write is at the user's explicit request, names its destination, goes
24//! through that source's own write interface into that source's own store, and is never
25//! read back to answer a query. That is what makes it a write and not a cache.
26
27use std::collections::{BTreeMap, BTreeSet};
28
29use onetaskgraph_plugin_api::{
30    Cursor, DependencyEdge, DependencyEndpoint, DependencyKind, Direction, Document, DocumentQuery,
31    ItemKind, ItemWrite, Location, Metering, NativeId, Page, PageRequest, Project, ProjectQuery,
32    Repository, SourceError, SourceName, StatusCategory, Task, TaskQuery, TaskRef,
33};
34use schemars::JsonSchema;
35use serde::{Deserialize, Serialize};
36use serde_json::Value;
37
38use crate::GlobalId;
39use crate::resolve::ResolvedSource;
40
41use super::delivery::{Delivered, targets};
42use super::fetch::{fits, unrepeated};
43use super::local::ProjectSelector;
44use super::{
45    DocumentFilters, DocumentRequest, Engine, EngineError, Filters, LeftBehind, Paging, Qualified,
46    TaskRequest,
47};
48
49/// A request to copy work into one configured destination.
50#[derive(Debug, Clone)]
51pub struct CopyRequest {
52    /// The qualified items to copy, in the order they were named.
53    pub items: CopyItems,
54    /// What those ids name, and what comes with them.
55    pub scope: CopyScope,
56    /// The configured source to copy into — a source name, never a qualified id.
57    pub destination: SourceName,
58    /// How to re-establish a correspondence the two origin rules cannot find.
59    pub match_by: Option<MatchBy>,
60    /// Whether an origin naming nothing at the destination falls through to the search
61    /// rule instead of refusing.
62    pub recreate: bool,
63    /// Whether to perform every read and no write.
64    pub dry_run: bool,
65}
66
67/// The items one copy names: at least one, because a copy naming none is not a copy.
68///
69/// A newtype rather than a bare `Vec`, for the reason [`Repository`] is one: the empty
70/// list is not a copy of nothing, it is a caller mistake, and a type that can hold it
71/// leaves every reader to decide what it means — a report with no entries, an error, a
72/// silent success. None of those is better than not being able to say it.
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct CopyItems(Vec<GlobalId>);
75
76impl CopyItems {
77    /// The items a caller named, or `None` when they named none.
78    #[must_use]
79    pub fn new(items: Vec<GlobalId>) -> Option<Self> {
80        (!items.is_empty()).then_some(Self(items))
81    }
82
83    /// The items, in the order they were named.
84    #[must_use]
85    pub fn as_slice(&self) -> &[GlobalId] {
86        &self.0
87    }
88}
89
90/// What the ids a copy names are, and what travels with them.
91///
92/// One value rather than a kind beside a flag, because three of the four combinations
93/// those two would make are real and the fourth — tasks, with the tasks of each also
94/// copied — means nothing. The member list is a variant for the same reason: it narrows a
95/// project copy that carries its tasks, and it has nothing to say to the other three.
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub enum CopyScope {
98    /// The ids name tasks, and only those tasks are copied.
99    Tasks,
100    /// The ids name projects.
101    Projects {
102        /// Whether the tasks in each project are copied too.
103        tasks: bool,
104    },
105    /// The ids name projects, and of the tasks in them exactly these are copied.
106    ///
107    /// A member the list does not name is not read at the destination, not compared, not
108    /// written and not reported, and no walk for what the copy left behind runs. A named
109    /// member with no counterpart there is still created: the list narrows which members
110    /// are read and written, never which outcomes are possible.
111    ///
112    /// An edge from a copied item to a member the list does not name resolves to the
113    /// destination id that member's own [`GlobalId::ORIGIN_KEY`] records at the source,
114    /// without reading the destination for it. When that member records none, the copy is
115    /// refused before anything is written.
116    Members(CopyItems),
117    /// The ids name documents, and only those documents are copied.
118    ///
119    /// Nothing travels with a document: it takes part in no dependency graph, and it holds
120    /// nothing of its own the way a project holds tasks.
121    Documents,
122}
123
124/// The caller-named escape for a correspondence neither origin rule can find.
125///
126/// A person editing Markdown who deletes or corrupts the origin key leaves an item rule 1
127/// cannot use and rule 2 cannot find, and the next copy would create a second item. This
128/// is how that is re-established without hand-editing ids.
129#[derive(Debug, Clone, PartialEq, Eq)]
130pub enum MatchBy {
131    /// Match the item whose title is the same.
132    Title,
133    /// Match the item whose value at this metadata key is the same.
134    Metadata(String),
135}
136
137impl MatchBy {
138    /// The spelling a caller types, `title` or any metadata key.
139    #[must_use]
140    pub fn parse(key: &str) -> Self {
141        if key == "title" {
142            Self::Title
143        } else {
144            Self::Metadata(key.to_owned())
145        }
146    }
147}
148
149/// What a copy did, one entry per item.
150///
151/// The same per-item outcomes reach every consumer: the machine-readable output renders
152/// this, the rendered output renders this, and a Rust caller is handed it.
153#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
154pub struct CopyReport {
155    /// One entry per item the copy considered, in the order it considered them.
156    pub items: Vec<CopyOutcome>,
157    // Three flat scalars rather than one nested object, and a `!skip_serializing_if` beside
158    // every skip: both are load-bearing for the generated SDKs rather than matters of
159    // taste, and AGENTS.md's note on what a copied document's references are pointed at is
160    // where that reasoning lives.
161    /// Reference occurrences the copy rewrote to the destination's own location for the
162    /// record they name.
163    ///
164    /// A silent bound is indistinguishable from a bug, so the copy says what it did to the
165    /// references the documents it carried hold. This and the two below are totals over the
166    /// whole invocation rather than figures per document, and all three default to zero, so
167    /// a consumer written against the output before they existed is unaffected.
168    ///
169    /// **What these figures do not claim.** The referent set is a document's own project,
170    /// so a reference to a record in a *different* project is never recognised at all and
171    /// cannot appear in [`Self::references_unresolved`] either. These are the references
172    /// the copy recognised; they are not a census of every reference a document holds.
173    /// Noticing an out-of-scope reference would need exactly the unbounded destination walk
174    /// this design refuses.
175    #[serde(default, skip_serializing_if = "nothing_to_report")]
176    #[schemars(!skip_serializing_if)]
177    pub references_rewritten: u64,
178    /// Reference occurrences the copy recognised and left byte-for-byte as they were,
179    /// because the correspondence could not be established.
180    #[serde(default, skip_serializing_if = "nothing_to_report")]
181    #[schemars(!skip_serializing_if)]
182    pub references_unresolved: u64,
183    /// How many of [`Self::references_unresolved`] were left alone because the
184    /// correspondence was **ambiguous** rather than merely absent. A sub-count, never
185    /// larger than it.
186    ///
187    /// Split out because the two mean different things to a reader. A reference with no
188    /// counterpart is ordinary and expected under the bound above — the design working. An
189    /// ambiguous one says the destination holds duplicate records for one work item, or the
190    /// source reports one location for two records, and re-running the copy will never
191    /// clear it.
192    #[serde(default, skip_serializing_if = "nothing_to_report")]
193    #[schemars(!skip_serializing_if)]
194    // llmlint: ignore[invalid_states_unrepresentable] JSON Schema cannot express an
195    // inequality between two numbers, so a private constructor here would hold this in one
196    // consumer of three while both SDKs' generated models went on admitting it. What holds
197    // it is `substitute`: `Resolution` has no variant that counts an occurrence ambiguous
198    // without counting it unresolved.
199    pub references_ambiguous: u64,
200    /// `delivers` entries the copy rewrote to the destination's own id for a member of the
201    /// copied set, over the whole invocation. Every other entry arrives qualified and is not
202    /// counted. Left out when zero, as the three figures above are.
203    #[serde(default, skip_serializing_if = "nothing_to_report")]
204    #[schemars(!skip_serializing_if)]
205    pub delivers_rewritten: u64,
206    /// One entry per delivered task the copy kept in step with a task it landed, after the
207    /// whole copy was complete — see `task status set`, which reports the same entries. Left
208    /// out when there were none.
209    ///
210    /// A failed entry does not undo the copy: the tasks it landed stay landed, and the
211    /// command exits `4`.
212    #[serde(default, skip_serializing_if = "Vec::is_empty")]
213    #[schemars(!skip_serializing_if)]
214    pub delivered: Vec<Delivered>,
215    /// What this copy spent, summed over the sources in the command that meter their own
216    /// requests — and absent, never zero, when none of them does.
217    ///
218    /// Omitted from the wire when absent, like the three figures above, so a consumer
219    /// written against the output before it existed reads the same document.
220    #[serde(default, skip_serializing_if = "Option::is_none")]
221    pub spent: Option<Spent>,
222}
223
224/// Whether one of [`CopyReport`]'s reference figures has anything to say.
225///
226/// A copy that recognised no reference reports that by leaving the figure out rather than
227/// by writing a nought, so the machine output of a task or project copy is exactly what it
228/// was before these figures existed. The human rendering says it in words either way,
229/// because a reader there needs to be told the copy looked.
230fn nothing_to_report(figure: &u64) -> bool {
231    *figure == 0
232}
233
234/// What one command spent, summed over the sources in it that meter their own requests.
235///
236/// **Source-owned.** Every figure is what a source said it sent and spent while the command
237/// ran, read through [`TaskSource::metering`](onetaskgraph_plugin_api::TaskSource::metering)
238/// before the command and again after it. The engine adds the differences up by name and
239/// interprets none of them, which is why the budget and unit names are open vocabulary.
240#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
241pub struct Spent {
242    /// How many HTTP requests those sources sent for this command.
243    pub requests: u64,
244    /// What those requests spent, one entry per budget and unit, ordered by budget name.
245    pub budgets: Vec<BudgetSpent>,
246}
247
248/// What one command spent against one budget.
249#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
250pub struct BudgetSpent {
251    /// The budget, as the source names it — `graphql`, `rest`.
252    // llmlint: ignore[invalid_states_unrepresentable] The contract this report lands states `budget` and `unit` as strings in an open vocabulary the engine interprets none of, and both SDKs are generated from this type's schema; the one value such a vocabulary can refuse, the empty name, never reaches here, because `difference` below treats a source reporting one as not metering.
253    pub budget: String,
254    /// What it is metered in — `points`, `requests`.
255    // llmlint: ignore[invalid_states_unrepresentable] As `budget` above.
256    pub unit: String,
257    /// How much was spent against it, in that unit.
258    pub amount: u64,
259    /// Whether any part of `amount` was modelled by a source rather than reported by its
260    /// backend or counted, which makes `amount` a lower bound on what the backend charged
261    /// rather than a measurement of it.
262    pub lower_bound: bool,
263}
264
265/// One document's reference figures, before they are folded into the invocation's.
266#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
267struct Counted {
268    /// Occurrences rewritten.
269    rewritten: u64,
270    /// Occurrences recognised and left alone.
271    unresolved: u64,
272    /// How many of those were ambiguous.
273    ambiguous: u64,
274}
275
276impl Counted {
277    /// Fold one document's figures into the invocation's.
278    fn add(&mut self, other: Self) {
279        self.rewritten += other.rewritten;
280        self.unresolved += other.unresolved;
281        self.ambiguous += other.ambiguous;
282    }
283}
284
285/// What happened to one item.
286///
287/// `action` and `destination` are one value rather than two fields side by side: an
288/// updated item without a destination id, or an orphan without one, are states this type
289/// must not be able to say — the id *is* what those outcomes are about. The one outcome
290/// that legitimately has none is a dry run that would create, because nothing was
291/// created and there is no id to report.
292#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
293pub struct CopyOutcome {
294    /// The qualified id the item was read from.
295    pub source: GlobalId,
296    /// What happened to it, and where.
297    #[serde(flatten)]
298    pub action: CopyAction,
299}
300
301impl CopyOutcome {
302    /// The qualified id this outcome landed on, when it landed on one.
303    #[must_use]
304    pub fn destination(&self) -> Option<&GlobalId> {
305        self.action.destination()
306    }
307}
308
309/// The four things a copy can do to one item, and the id each of them is about.
310#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
311#[serde(tag = "action", rename_all = "kebab-case")]
312pub enum CopyAction {
313    // llmlint: ignore[names_match_behavior] `created` is Contract D's serialized action
314    // for both a completed create and a dry run that would create; the optional destination
315    // distinguishes those cases, and renaming this public variant would break Rust callers.
316    /// The destination held no counterpart, so one was created.
317    Created {
318        /// The id it was created under, or `null` for a dry run that would have created
319        /// one — there is no id, because nothing was.
320        destination: Option<GlobalId>,
321    },
322    /// The destination held a counterpart and it now reads as the source does.
323    Updated {
324        /// The item that was updated.
325        destination: GlobalId,
326    },
327    /// The destination held a counterpart that already read that way; nothing was written.
328    Unchanged {
329        /// The item that already said it.
330        destination: GlobalId,
331    },
332    /// The destination holds a counterpart the source no longer does. A copy never
333    /// deletes, so it was left exactly as it is.
334    Orphaned {
335        /// The item that was left alone.
336        destination: GlobalId,
337    },
338}
339
340impl CopyAction {
341    /// The qualified id this action is about, when there is one.
342    #[must_use]
343    pub fn destination(&self) -> Option<&GlobalId> {
344        match self {
345            Self::Created { destination } => destination.as_ref(),
346            Self::Updated { destination }
347            | Self::Unchanged { destination }
348            | Self::Orphaned { destination } => Some(destination),
349        }
350    }
351
352    /// The word this action serializes as, taken from its own `Serialize`.
353    ///
354    /// Read back off the wire form rather than written out again in a `match`, for the
355    /// reason `render::wire` gives: a second spelling of `unchanged` would be a second
356    /// place for it to drift from the one a caller reads.
357    #[must_use]
358    pub fn name(&self) -> String {
359        serde_json::to_value(self).expect("a contract enum serialises")["action"]
360            .as_str()
361            .expect("an internally tagged enum carries its tag")
362            .to_owned()
363    }
364}
365
366/// What one item points at, resolved as far as the copy has got: its forward edges and the
367/// tasks it delivers.
368struct Pointing<'a> {
369    /// Its forward edges, `None` where the far end is a member not landed yet.
370    edges: &'a [Option<DependencyEdge>],
371    /// The tasks it delivers that can already be named at the destination.
372    delivers: &'a [TaskRef],
373}
374
375/// Where one item is going at the destination.
376enum Target {
377    /// Update the destination item with this id, reached by the rule named.
378    Update {
379        /// The destination item this copy updates.
380        id: NativeId,
381        /// Which rule found it.
382        found: Found,
383    },
384    /// Create one.
385    Create,
386}
387
388/// Which of the rules above found the destination item a copy is updating.
389///
390/// The two are the same instruction — update that item — and a different answer about the
391/// origin, which is why the distinction is carried this far rather than dropped where it
392/// is made. See [`recorded`].
393#[derive(Clone, Copy, PartialEq, Eq)]
394enum Found {
395    /// Rule 1: the item being copied already named it, so this copy is a copy-back.
396    Origin,
397    /// Rule 2 or the caller's matching escape: the destination was searched for it.
398    Search,
399}
400
401/// What a scan of the destination is looking for.
402enum Wanted {
403    /// An item recording this qualified id as its origin.
404    Origin(String),
405    /// An item whose title is this.
406    Title(String),
407    /// An item holding this value at this metadata key.
408    Metadata(String, Value),
409}
410
411impl Wanted {
412    /// Whether one destination item is the one being looked for.
413    fn found(&self, title: &str, metadata: &BTreeMap<String, Value>) -> bool {
414        match self {
415            Self::Origin(id) => {
416                metadata.get(GlobalId::ORIGIN_KEY) == Some(&Value::String(id.clone()))
417            }
418            Self::Title(wanted) => title == wanted,
419            Self::Metadata(key, value) => metadata.get(key) == Some(value),
420        }
421    }
422}
423
424/// What the destination held before this copy touched one item.
425///
426/// Read once, in [`Engine::land`], and used three times over: to decide whether the write
427/// would change anything, to repair the item's edges once the rest of the copy has landed,
428/// and — if the copy cannot finish — to put the item back exactly as it was.
429#[derive(Clone)]
430struct Prior {
431    /// The item as the destination held it.
432    item: Item,
433    /// Its forward edges there.
434    edges: Vec<DependencyEdge>,
435}
436
437/// One item that landed with an edge whose far end was not written yet.
438///
439/// Held until every item of the whole copy has landed, because the far end may be in
440/// another project of the same command: a copy of two projects at once is one copied set,
441/// not two, and an edge across them is remapped rather than written as a foreign id.
442struct Deferred {
443    /// The item, as it was read and resolved.
444    item: Planned,
445    /// The destination project it was filed under.
446    filed: Option<NativeId>,
447    /// Where it landed.
448    destination: NativeId,
449    /// What the destination held there before, when it held anything.
450    prior: Option<Prior>,
451}
452
453/// What one item's undo has to do to put the destination back.
454enum Undo {
455    /// The copy created it, so undoing means removing it.
456    Created {
457        /// Which write interface removes it.
458        kind: Level,
459        /// The destination id it was created under.
460        id: NativeId,
461    },
462    /// The copy overwrote something, so undoing means writing that something back.
463    ///
464    /// No `kind` beside the id, unlike the variant above: what was there says which of the
465    /// two write interfaces takes it back, and a second spelling of that could disagree
466    /// with it.
467    Updated {
468        /// The destination id that was overwritten.
469        id: NativeId,
470        /// What was there before.
471        prior: Prior,
472    },
473}
474
475impl Undo {
476    /// The destination id this entry is about.
477    fn id(&self) -> &NativeId {
478        match self {
479            Self::Created { id, .. } | Self::Updated { id, .. } => id,
480        }
481    }
482
483    /// Which of the destination's three write interfaces this entry belongs to.
484    ///
485    /// An id alone does not identify a destination item: nothing stops a destination
486    /// numbering its tasks and its projects in one namespace, and a local-Markdown store
487    /// filing `alpha.md` under both is the ordinary case rather than the contrived one.
488    /// So this pairs with `id` wherever one entry has to be told from another.
489    fn kind(&self) -> Level {
490        match self {
491            Self::Created { kind, .. } => *kind,
492            // Read off what was there, for the reason the variant carries no `kind` of
493            // its own: two spellings of one fact can disagree, and this one cannot.
494            Self::Updated { prior, .. } => prior.item.level(),
495        }
496    }
497}
498
499/// Everything one copy has written, in the order it wrote it, so a copy that cannot finish
500/// can undo its own writes.
501///
502/// This is not state the engine keeps: it lives for the length of one `copy` call and is
503/// dropped with it, so the invariant that nothing of a user's work is written down outside
504/// the plugin that owns it is untouched.
505#[derive(Default)]
506struct Journal {
507    /// One entry per destination item this copy first touched, in that order.
508    entries: Vec<Undo>,
509}
510
511impl Journal {
512    /// Record what has to happen to put one destination item back.
513    ///
514    /// The *first* entry for an id is the one that matters and later ones are dropped: an
515    /// item written twice — once as it lands, once when its edges are repaired — was only
516    /// ever one thing before this copy started, and that is what undoing it restores.
517    fn record(&mut self, entry: Undo) {
518        if self
519            .entries
520            .iter()
521            .any(|held| held.kind() == entry.kind() && held.id() == entry.id())
522        {
523            return;
524        }
525        self.entries.push(entry);
526    }
527}
528
529/// What one copy invocation carries from the item it lands to the next.
530///
531/// Like the [`Journal`] beside it, this lives for the length of one `copy` call and is
532/// dropped with it: nothing here is state the engine keeps, and nothing in it is read back
533/// to answer a later query.
534#[derive(Default)]
535struct Running {
536    /// Every item an edge of this command resolves to a destination id rather than writing as
537    /// it was read: the ids named, what travels with them, and — for a member copy — each
538    /// member the copy does not carry whose own recorded origin already names its
539    /// destination item. That last kind is resolvable without being copied.
540    resolvable: Vec<GlobalId>,
541    /// The destination item each of those corresponds to, as soon as it is known — whether
542    /// this command found it, landed it, or read it off a member's recorded origin.
543    ///
544    /// Keyed by the qualified id's own rendering, which is what a recorded origin holds
545    /// anyway — making `GlobalId` orderable for a local map would put an ordering on a
546    /// contract type for a reason no caller of it has.
547    counterparts: BTreeMap<String, NativeId>,
548    /// The items held back for [`Engine::repair`], across the whole request.
549    deferred: Vec<Deferred>,
550    /// The reference figures, one total for the whole invocation rather than one per
551    /// document.
552    references: Counted,
553    /// The destination project each source project corresponds to, once this command has
554    /// looked for it — so a second task filed under the same project does not walk the
555    /// destination for it again.
556    filings: BTreeMap<String, Option<NativeId>>,
557    /// `delivers` entries naming a member of the copied set, over the whole invocation.
558    delivers_rewritten: u64,
559    /// Every task this copy landed, for the relation rule once the whole copy is complete.
560    landed: Vec<LandedTask>,
561}
562
563/// One task a copy landed, and what the relation rule reads of it once the copy is complete.
564struct LandedTask {
565    /// Where it landed.
566    destination: GlobalId,
567    /// The source it was read from, which a bare `delivers` entry names a task of.
568    origin: SourceName,
569    /// Its `delivers`, as its source reported them.
570    delivers: Vec<TaskRef>,
571    /// The `delivers` the destination held there before this copy, qualified.
572    before: Vec<GlobalId>,
573    /// Its status category, as the copy wrote it.
574    category: StatusCategory,
575}
576
577/// A task a copy landed, with the tasks it delivers now and delivered before, qualified.
578struct Deliverer {
579    destination: GlobalId,
580    category: StatusCategory,
581    now: Vec<GlobalId>,
582    before: Vec<GlobalId>,
583}
584
585/// One item, read and resolved, on its way into the destination.
586struct Planned {
587    /// Where it came from.
588    source: GlobalId,
589    /// The item as its source reported it.
590    item: Item,
591    /// Its forward edges, as its source reported them.
592    edges: Vec<DependencyEdge>,
593    /// Where it is going.
594    target: Target,
595    /// What the destination held at that target, read once where the target was found.
596    ///
597    /// The read that decided an item exists is the read of what it holds, so the two are
598    /// one round trip rather than two against a hosted destination.
599    held: Option<Prior>,
600}
601
602/// A task, a project or a document, so the copy path is written once.
603#[derive(Clone)]
604enum Item {
605    /// A task.
606    Task(Box<Task>),
607    /// A project.
608    Project(Box<Project>),
609    /// A document.
610    Document(Box<Document>),
611}
612
613impl Item {
614    fn id(&self) -> &NativeId {
615        match self {
616            Self::Task(task) => &task.id,
617            Self::Project(project) => &project.id,
618            Self::Document(document) => &document.id,
619        }
620    }
621
622    fn level(&self) -> Level {
623        match self {
624            Self::Task(_) => Level::Task,
625            Self::Project(_) => Level::Project,
626            Self::Document(_) => Level::Document,
627        }
628    }
629}
630
631/// Which of a destination's three read-and-write interfaces one item belongs to.
632///
633/// Deliberately not [`ItemKind`]: that enum names what a *dependency endpoint* points at,
634/// and the contract gives it no document variant because nothing may point at a document.
635/// This one names which pair of methods reads and writes an item, which is a different
636/// question with a third answer.
637///
638/// Ordered so it can key a map of what a destination holds, per interface: an id alone
639/// does not identify a destination item, for the reason [`Undo::kind`] records.
640#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
641enum Level {
642    /// `get_task`, `write_task`, `delete_task`.
643    Task,
644    /// `get_project`, `write_project`, `delete_project`.
645    Project,
646    /// `get_document`, `write_document`, `delete_document`.
647    Document,
648}
649
650/// One record filed under a document's own project, and the location string its source
651/// reports for it.
652///
653/// A reference is a **literal occurrence, in a document's content, of the exact location
654/// string the source reports for a related record** — the `String` inside
655/// [`Location::Path`] or [`Location::Url`]. Both ends of a rewrite come from the plugins'
656/// own reported [`Location`]: nothing here composes an address out of a name, an id or a
657/// root, because a source that reports a canonical absolute path and a source that reports
658/// an issue link are the two things the contract lets this ask about.
659#[derive(Clone)]
660struct Referent {
661    /// Its qualified id at the source.
662    id: GlobalId,
663    /// The origin it records in its own metadata, when it records one.
664    origin: Option<GlobalId>,
665    /// Which of the destination's three interfaces its counterpart would be read from.
666    level: Level,
667    /// The non-empty location string its source reports for it.
668    location: String,
669}
670
671impl Referent {
672    /// The two keys a destination record's own recorded origin is matched against.
673    ///
674    /// **A destination record is this referent's counterpart when its
675    /// [`GlobalId::ORIGIN_KEY`] equals either this referent's own qualified source id, or
676    /// the origin this referent itself records.** In plainer terms: when the destination
677    /// record was copied directly from this referent, or directly from the one predecessor
678    /// this referent itself records.
679    ///
680    /// That reach is exactly one recorded hop of ancestry on each side, and nothing more:
681    ///
682    /// - **A single hop resolves.** The destination record came straight from the referent.
683    /// - **A one-level fan-out resolves.** A record copied from one store into two, with
684    ///   the document arriving by one route and naming records that arrived by the other,
685    ///   so both sides trace to one common predecessor. That is what the second key buys,
686    ///   and it is the only thing it buys.
687    /// - **A chain of two or more hops does not resolve**, on either side, and that is
688    ///   permanent rather than pending: only one origin is ever recorded and every hop
689    ///   overwrites it. [`carried`] removes the key from the incoming metadata outright and
690    ///   [`recorded`] writes the id at the *immediate* source on every path but the
691    ///   copy-back, so A → B → C leaves C keyed by B and A's id gone.
692    ///
693    /// The second key costs no read — the referent's metadata is already in hand. Chasing
694    /// the chain further would need the intermediate stores configured and reachable, which
695    /// would put a third party's availability inside a copy; a durable lineage id would
696    /// identify only records written after it landed, so it would resolve nothing already
697    /// on a destination.
698    fn keys(&self) -> Vec<String> {
699        let mut keys = vec![self.id.to_string()];
700        if let Some(origin) = &self.origin {
701            let recorded = origin.to_string();
702            if recorded != keys[0] {
703                keys.push(recorded);
704            }
705        }
706        keys
707    }
708}
709
710/// What every whole-reference occurrence of one location string becomes.
711///
712/// Three variants rather than a rewrite beside a flag, because the two ways of leaving an
713/// occurrence alone are what the two figures a copy reports are *about*: one is the design
714/// working and the other says the destination or the source holds something a re-run will
715/// never fix. A bool would let a reader of this type read them as the same outcome.
716enum Resolution {
717    /// The destination's own location string for the counterpart.
718    Rewrite(String),
719    /// The destination holds no counterpart, or holds one it reports no location for, so
720    /// the occurrence is left byte-for-byte. Ordinary and expected under the bound this
721    /// design works to.
722    NoCounterpart,
723    /// The correspondence could not be established **confidently** — more than one
724    /// destination record matches the referent's two keys, or two referents report this one
725    /// location string. The occurrence is left byte-for-byte, no record is chosen, and a
726    /// re-run will never clear it.
727    Ambiguous,
728}
729
730/// Every destination record that records an origin, read **once per copy invocation**.
731///
732/// Several documents in one project is the ordinary case, and a walk per document would
733/// multiply reads against a rate limiter for no gain — so this is built once, for the
734/// levels the invocation's own documents really name, and every document and every
735/// referent of that invocation is answered out of it. A copy whose documents hold no
736/// candidate reference builds none at all.
737///
738/// **This is a stricter discipline than [`Engine::scan`], deliberately.** That lookup takes
739/// the first hit and stops, and every consumer of the copy already depends on it doing so;
740/// it chooses the copy's own target, where a caller named the item. This one edits the
741/// content of somebody's document, where a wrong answer is silent corruption of prose a
742/// person will act on — so where more than one record matches, it chooses none. The two
743/// lookups answer different questions and are meant to disagree on a destination holding
744/// duplicates.
745#[derive(Default)]
746struct Counterparts {
747    /// The records at one interface recording one origin.
748    by_origin: BTreeMap<(Level, String), Vec<Held>>,
749}
750
751/// One record a destination walk found: where it is there, and the location string that
752/// destination reports for it — `None` when it reports none.
753type Held = (NativeId, Option<String>);
754
755impl Counterparts {
756    /// Record one destination item, when it records an origin at all.
757    fn note(
758        &mut self,
759        level: Level,
760        id: &NativeId,
761        location: Option<&Location>,
762        metadata: &BTreeMap<String, Value>,
763    ) {
764        let Some(origin) = origin_of(metadata) else {
765            return;
766        };
767        self.by_origin
768            .entry((level, origin.to_string()))
769            .or_default()
770            .push((id.clone(), located(location)));
771    }
772
773    /// What one referent's occurrences become, by the two-key rule.
774    ///
775    /// Where the correspondence cannot be established **confidently**, the text is left
776    /// exactly as it is and no record is chosen — see the note on this type.
777    fn resolve(&self, referent: &Referent) -> Resolution {
778        let mut candidates: Vec<&Held> = Vec::new();
779        for key in referent.keys() {
780            for record in self
781                .by_origin
782                .get(&(referent.level, key))
783                .into_iter()
784                .flatten()
785            {
786                // One destination record matching both keys is one record, not two.
787                if !candidates.iter().any(|held| held.0 == record.0) {
788                    candidates.push(record);
789                }
790            }
791        }
792        match candidates.as_slice() {
793            [] => Resolution::NoCounterpart,
794            // A counterpart the destination reports no location for names nowhere a reader
795            // could go, so the source's own string is left standing rather than removed.
796            [(_, location)] => location
797                .clone()
798                .map_or(Resolution::NoCounterpart, Resolution::Rewrite),
799            _ => Resolution::Ambiguous,
800        }
801    }
802}
803
804impl Engine {
805    /// Copy every item a request names into one configured destination.
806    ///
807    /// This is the whole of the verb, and the command line drives exactly this: a copy a
808    /// Rust caller makes and a copy typed at a shell are the same call, so the two cannot
809    /// answer the same copy differently.
810    ///
811    /// # Errors
812    ///
813    /// Returns [`EngineError`] when the destination is not configured, cannot be built,
814    /// cannot be written, or — for a document copy — declares it has no documents; when an
815    /// id names nothing; when an origin names an item the
816    /// destination no longer holds and `--recreate` was not given; and when the
817    /// destination refuses the write — including a field or a metadata key it cannot
818    /// carry, which it names rather than dropping.
819    pub async fn copy(&self, request: &CopyRequest) -> Result<CopyReport, EngineError> {
820        let destination = self.writable(&request.destination)?;
821        // Before anything is read, and from the declaration rather than from a failed
822        // write: a destination that says it has no documents has nowhere to put one.
823        if request.scope == CopyScope::Documents {
824            documentary(destination)?;
825        }
826        // The sources this command names, each read once before and once after, so what it
827        // spent is the difference between two of each one's own running totals.
828        let mut metered = vec![destination];
829        for id in request.items.as_slice() {
830            if let Some(source) = self.ready().find(|source| source.name() == &id.source)
831                && !metered.iter().any(|held| held.name() == source.name())
832            {
833                metered.push(source);
834            }
835        }
836        let before = readings(&metered).await;
837        let mut journal = Journal::default();
838        match self.copy_all(destination, request, &mut journal).await {
839            Ok((mut report, deliverers)) => {
840                // After the whole copy is complete and outside the journal: what the relation
841                // rule writes is the delivered tasks' own, and a failure there is reported
842                // against that task rather than undoing a copy that landed.
843                for deliverer in &deliverers {
844                    report.delivered.extend(
845                        self.deliver(
846                            &deliverer.destination,
847                            deliverer.category,
848                            &deliverer.now,
849                            &deliverer.before,
850                        )
851                        .await,
852                    );
853                }
854                report.spent = spent_between(&before, &readings(&metered).await);
855                Ok(report)
856            }
857            Err(error) => Err(self.undo(destination, journal, error).await),
858        }
859    }
860
861    /// The copy itself, with everything it writes recorded so a failure can be undone.
862    ///
863    /// The ids named together are **one** copied set, and that is what makes an edge
864    /// between any two of them a real edge at the destination: a copy of two projects at
865    /// once knows that a task in the first depends on a task in the second, and a task
866    /// knows that the project it belongs to is being created beside it. Copying them one
867    /// at a time could not, and wrote the far end as the id it had at its *source* — a
868    /// dangling reference to somewhere the destination has never heard of.
869    async fn copy_all(
870        &self,
871        destination: &ResolvedSource,
872        request: &CopyRequest,
873        journal: &mut Journal,
874    ) -> Result<(CopyReport, Vec<Deliverer>), EngineError> {
875        let mut running = Running::default();
876        // The whole copied set, established before anything is written. For a project
877        // copy that means reading every named project's membership first: the set is the
878        // whole request rather than one project of it.
879        let items = match &request.scope {
880            CopyScope::Tasks | CopyScope::Documents => {
881                let kind = if request.scope == CopyScope::Documents {
882                    Level::Document
883                } else {
884                    Level::Task
885                };
886                running
887                    .resolvable
888                    .extend(request.items.as_slice().iter().cloned());
889                let mut planned = Vec::new();
890                for id in request.items.as_slice() {
891                    planned.push(self.plan(destination, request, kind, id).await?);
892                }
893                self.copy_items(destination, request, planned, None, &mut running, journal)
894                    .await?
895            }
896            CopyScope::Projects { tasks } => {
897                let mut projects = Vec::new();
898                for id in request.items.as_slice() {
899                    let members = if *tasks {
900                        self.project_members(id).await?
901                    } else {
902                        Vec::new()
903                    };
904                    running.resolvable.push(id.clone());
905                    running.resolvable.extend(members.iter().cloned());
906                    projects.push((id.clone(), members));
907                }
908                self.copy_projects(destination, request, &projects, &[], &mut running, journal)
909                    .await?
910            }
911            CopyScope::Members(named) => {
912                let (projects, unrecorded) = self
913                    .named_members(destination, request.items.as_slice(), named, &mut running)
914                    .await?;
915                self.copy_projects(
916                    destination,
917                    request,
918                    &projects,
919                    &unrecorded,
920                    &mut running,
921                    journal,
922                )
923                .await?
924            }
925        };
926        let references = running.references;
927        let delivers_rewritten = running.delivers_rewritten;
928        // Every member's destination id is known once the first pass has landed it, so the
929        // tasks each landed task delivers are settled here rather than after the repair.
930        let deliverers: Vec<Deliverer> = running
931            .landed
932            .iter()
933            .map(|task| Deliverer {
934                destination: task.destination.clone(),
935                category: task.category,
936                now: targets(
937                    &resolved_entries(&mapped_delivers(
938                        &task.delivers,
939                        &task.origin,
940                        destination,
941                        &running.resolvable,
942                        &running.counterparts,
943                    )),
944                    destination.name(),
945                ),
946                before: task.before.clone(),
947            })
948            .collect();
949        self.repair(destination, request, running, journal).await?;
950        Ok((
951            CopyReport {
952                items,
953                references_rewritten: references.rewritten,
954                references_unresolved: references.unresolved,
955                references_ambiguous: references.ambiguous,
956                delivers_rewritten,
957                // Filled in by `copy`, once the copy is complete.
958                delivered: Vec::new(),
959                // Filled in by `copy`, which is the one place both readings are taken.
960                spent: None,
961            },
962            deliverers,
963        ))
964    }
965
966    /// Write every deferred item again, now that every destination id is known.
967    ///
968    /// This is the second half of the two passes an edge between two items of one copy
969    /// needs: the far end's destination id does not exist until it has been created, so
970    /// the item that points at it lands first without that edge and is completed here.
971    /// It runs once for the whole request rather than once per project, because a far end
972    /// may be in a project this copy has not reached yet.
973    async fn repair(
974        &self,
975        destination: &ResolvedSource,
976        request: &CopyRequest,
977        running: Running,
978        journal: &mut Journal,
979    ) -> Result<(), EngineError> {
980        if request.dry_run {
981            return Ok(());
982        }
983        let Running {
984            resolvable,
985            counterparts,
986            deferred,
987            ..
988        } = running;
989        for entry in deferred {
990            let edges = mapped_edges(
991                &entry.item.edges,
992                &entry.item.source.source,
993                destination,
994                &resolvable,
995                &counterparts,
996            );
997            let delivers = delivers_of(&entry.item, destination, &resolvable, &counterparts);
998            self.write(
999                destination,
1000                &entry.item,
1001                Some(entry.destination),
1002                entry.filed,
1003                &resolved(&edges),
1004                &resolved_entries(&delivers),
1005                entry.prior,
1006                journal,
1007            )
1008            .await?;
1009        }
1010        Ok(())
1011    }
1012
1013    /// Put the destination back the way this copy found it, then report why it failed.
1014    ///
1015    /// Undone in reverse, and an item this copy created is removed rather than restored —
1016    /// the entry recording what it looked like a moment after creation is not a state
1017    /// anybody asked for. When the destination cannot take one of them back, the refusal
1018    /// says so and names what is still there, because a user told "the copy failed" about
1019    /// a destination that is not as they left it will copy again over a tree nobody
1020    /// described.
1021    async fn undo(
1022        &self,
1023        destination: &ResolvedSource,
1024        journal: Journal,
1025        error: EngineError,
1026    ) -> EngineError {
1027        let created: Vec<(Level, &NativeId)> = journal
1028            .entries
1029            .iter()
1030            .filter_map(|entry| match entry {
1031                Undo::Created { kind, id } => Some((*kind, id)),
1032                Undo::Updated { .. } => None,
1033            })
1034            .collect();
1035        // The ids and the refusal are one value rather than two, because they are one
1036        // fact: an item is only left behind because the destination refused to take it
1037        // back, so the first refusal carries the first id and neither half can be
1038        // recorded without the other.
1039        let mut unrestored: Option<(LeftBehind, SourceError)> = None;
1040        for entry in journal.entries.iter().rev() {
1041            let outcome = match entry {
1042                Undo::Created { kind, id } => remove(destination, *kind, id).await,
1043                Undo::Updated { id, prior, .. } if !created.contains(&(prior.item.level(), id)) => {
1044                    restore(destination, id, prior).await
1045                }
1046                Undo::Updated { .. } => Ok(()),
1047            };
1048            if let Err(problem) = outcome {
1049                let id = GlobalId::new(destination.name().clone(), entry.id().clone());
1050                match &mut unrestored {
1051                    Some((left_behind, _)) => left_behind.push(id),
1052                    None => unrestored = Some((LeftBehind::new(id), problem)),
1053                }
1054            }
1055        }
1056        match unrestored {
1057            None => error,
1058            Some((left_behind, refusal)) => EngineError::CopyNotUndone {
1059                error: Box::new(error),
1060                left_behind,
1061                refusal,
1062            },
1063        }
1064    }
1065
1066    /// The destination source, once it is established it exists and can be written.
1067    fn writable(&self, name: &SourceName) -> Result<&ResolvedSource, EngineError> {
1068        let name = self.known(name)?;
1069        if let Some(unavailable) = self.unavailable().find(|source| source.name() == &name) {
1070            return Err(EngineError::DestinationUnavailable {
1071                name: name.to_string(),
1072                error: unavailable.error().clone(),
1073            });
1074        }
1075        let source = self
1076            .ready()
1077            .find(|source| source.name() == &name)
1078            .ok_or(EngineError::NoSources)?;
1079        if !source.source().writes().is_supported() {
1080            return Err(EngineError::NotWritable {
1081                name: name.to_string(),
1082                kind: source.kind().to_owned(),
1083            });
1084        }
1085        Ok(source)
1086    }
1087
1088    /// Copy each project and what travels with it: every task in it, the members a member
1089    /// copy names, or nothing at all.
1090    ///
1091    /// Every item of every project is read and its target decided **before any of them is
1092    /// written, and each exactly once.** That is what lets a member copy refuse an edge it
1093    /// cannot resolve while the destination is still as it was found, and what stops a
1094    /// whole copy resolving one target twice — once to learn the ids the project's own
1095    /// edges name, and again to land it — which against a hosted destination is a read per
1096    /// item spent on an answer the command already had.
1097    async fn copy_projects(
1098        &self,
1099        destination: &ResolvedSource,
1100        request: &CopyRequest,
1101        projects: &[(GlobalId, Vec<GlobalId>)],
1102        unrecorded: &[GlobalId],
1103        running: &mut Running,
1104        journal: &mut Journal,
1105    ) -> Result<Vec<CopyOutcome>, EngineError> {
1106        let mut plans = Vec::new();
1107        for (id, members) in projects {
1108            let project = self.plan(destination, request, Level::Project, id).await?;
1109            let mut tasks = Vec::new();
1110            for member in members {
1111                tasks.push(self.plan(destination, request, Level::Task, member).await?);
1112            }
1113            plans.push((project, tasks));
1114        }
1115        for (project, tasks) in &plans {
1116            for item in std::iter::once(project).chain(tasks) {
1117                unrecorded_far_end(item, destination, unrecorded)?;
1118            }
1119        }
1120        let mut outcomes = Vec::new();
1121        for (project, tasks) in plans {
1122            outcomes.extend(
1123                self.copy_project(destination, request, project, tasks, running, journal)
1124                    .await?,
1125            );
1126        }
1127        Ok(outcomes)
1128    }
1129
1130    /// Land one planned project, then the tasks planned beside it.
1131    async fn copy_project(
1132        &self,
1133        destination: &ResolvedSource,
1134        request: &CopyRequest,
1135        project: Planned,
1136        tasks: Vec<Planned>,
1137        running: &mut Running,
1138        journal: &mut Journal,
1139    ) -> Result<Vec<CopyOutcome>, EngineError> {
1140        let (carries_tasks, walks_orphans) = match request.scope {
1141            CopyScope::Projects { tasks } => (tasks, tasks),
1142            CopyScope::Members(_) => (true, false),
1143            CopyScope::Tasks | CopyScope::Documents => (false, false),
1144        };
1145        let id = project.source.clone();
1146        let members: Vec<GlobalId> = tasks.iter().map(|task| task.source.clone()).collect();
1147        // The project lands with every edge it can already resolve — its members' targets
1148        // are known from their plans — so a project that has not changed is not written at
1149        // all. What it *reports* is read the way it always has been: against the ids known
1150        // before its own members' were, unless every edge resolves and nothing differs.
1151        // Reading it any other way would move the word a repeat copy reports for a project
1152        // whose only difference is an edge, which is not this change's to move.
1153        let mut before_members = running.counterparts.clone();
1154        if let Target::Update { id: target, .. } = &project.target {
1155            before_members.insert(id.to_string(), target.clone());
1156        }
1157        for item in std::iter::once(&project).chain(&tasks) {
1158            if let Target::Update { id: target, .. } = &item.target {
1159                running
1160                    .counterparts
1161                    .insert(item.source.to_string(), target.clone());
1162            }
1163        }
1164        let unchanged = match &project.target {
1165            Target::Update { id: target, .. } => {
1166                let settled = mapped_edges(
1167                    &project.edges,
1168                    &id.source,
1169                    destination,
1170                    &running.resolvable,
1171                    &running.counterparts,
1172                );
1173                let first = mapped_edges(
1174                    &project.edges,
1175                    &id.source,
1176                    destination,
1177                    &running.resolvable,
1178                    &before_members,
1179                );
1180                let held = project.held.as_ref();
1181                let unchanged_with = |edges: &[Option<DependencyEdge>]| {
1182                    !changes(
1183                        held,
1184                        &project,
1185                        target,
1186                        &None,
1187                        &resolved(edges),
1188                        &[],
1189                        destination.name(),
1190                    )
1191                };
1192                Some(
1193                    (!settled.iter().any(Option::is_none) && unchanged_with(&settled))
1194                        || unchanged_with(&first),
1195                )
1196            }
1197            Target::Create => None,
1198        };
1199        let mut outcomes = self
1200            .copy_items(destination, request, vec![project], None, running, journal)
1201            .await?;
1202        if let (Some(unchanged), Some(landed)) = (unchanged, outcomes[0].destination().cloned()) {
1203            outcomes[0].action = if unchanged {
1204                CopyAction::Unchanged {
1205                    destination: landed,
1206                }
1207            } else {
1208                CopyAction::Updated {
1209                    destination: landed,
1210                }
1211            };
1212        }
1213        if !carries_tasks {
1214            return Ok(outcomes);
1215        }
1216        // `None` when a dry run would have created the project: nothing was written, so
1217        // there is no destination project id to file the tasks under. Every task is still
1218        // read and still reported, because that is what a dry run is for.
1219        let filed = outcomes.first().and_then(CopyOutcome::destination).cloned();
1220        outcomes.extend(
1221            self.copy_items(
1222                destination,
1223                request,
1224                tasks,
1225                filed.as_ref().map(|project| project.native.clone()),
1226                running,
1227                journal,
1228            )
1229            .await?,
1230        );
1231        // A member copy was told which members it carries, so it cannot tell a member it
1232        // left out from one the source no longer holds, and does not walk for either.
1233        if walks_orphans && let Some(filed) = filed {
1234            outcomes.extend(
1235                self.orphans(destination, &id, &filed.native, &members)
1236                    .await?,
1237            );
1238        }
1239        Ok(outcomes)
1240    }
1241
1242    /// The members a member copy names, per project and in the order they were named, and
1243    /// every member it does not name that records no destination id.
1244    ///
1245    /// Settled before anything is read at the destination. A named id that is a member of
1246    /// none of the projects is refused here, naming it. An unnamed member whose recorded
1247    /// origin names the destination joins the copied set with that id already known, so an
1248    /// edge to it resolves exactly as an edge to a copied item does, and costs no read. An
1249    /// unnamed member recording none is returned, so an edge to it is refused before
1250    /// anything is written — see [`unrecorded_far_end`].
1251    async fn named_members(
1252        &self,
1253        destination: &ResolvedSource,
1254        projects: &[GlobalId],
1255        named: &CopyItems,
1256        running: &mut Running,
1257    ) -> Result<(Vec<(GlobalId, Vec<GlobalId>)>, Vec<GlobalId>), EngineError> {
1258        let mut carried = Vec::new();
1259        let mut unrecorded = Vec::new();
1260        for project in projects {
1261            let held = self.project_member_tasks(project).await?;
1262            let mut members: Vec<GlobalId> = Vec::new();
1263            for id in named.as_slice() {
1264                if held.iter().any(|task| &task.id == id) && !members.contains(id) {
1265                    members.push(id.clone());
1266                }
1267            }
1268            for task in held {
1269                if members.contains(&task.id) {
1270                    continue;
1271                }
1272                match origin_of(&task.item.metadata)
1273                    .filter(|origin| &origin.source == destination.name())
1274                {
1275                    Some(origin) => {
1276                        running
1277                            .counterparts
1278                            .insert(task.id.to_string(), origin.native);
1279                        running.resolvable.push(task.id);
1280                    }
1281                    None => unrecorded.push(task.id),
1282                }
1283            }
1284            running.resolvable.push(project.clone());
1285            running.resolvable.extend(members.iter().cloned());
1286            carried.push((project.clone(), members));
1287        }
1288        if let Some(stray) = named
1289            .as_slice()
1290            .iter()
1291            .find(|id| !carried.iter().any(|(_, members)| members.contains(id)))
1292        {
1293            return Err(EngineError::NotAMember {
1294                id: stray.clone(),
1295                projects: projects.to_vec(),
1296            });
1297        }
1298        Ok((carried, unrecorded))
1299    }
1300
1301    /// Every task the source holds in `project`, by qualified id.
1302    async fn project_members(&self, project: &GlobalId) -> Result<Vec<GlobalId>, EngineError> {
1303        Ok(self
1304            .project_member_tasks(project)
1305            .await?
1306            .into_iter()
1307            .map(|task| task.id)
1308            .collect())
1309    }
1310
1311    /// Every task the source holds in `project`, as the source reported it.
1312    ///
1313    /// The ids alone are what a copy files under a project; the whole task is what a
1314    /// document's references need, because the location a reference names is a field of it.
1315    async fn project_member_tasks(
1316        &self,
1317        project: &GlobalId,
1318    ) -> Result<Vec<Qualified<Task>>, EngineError> {
1319        let mut request = TaskRequest {
1320            sources: vec![project.source.clone()],
1321            filters: Filters::default(),
1322            project: ProjectSelector::Qualified(project.clone()),
1323            paging: Paging {
1324                limit: PROJECT_PAGE,
1325                token: None,
1326            },
1327        };
1328        let mut members = Vec::new();
1329        // Pages by this engine's own token rather than by a source cursor, and the
1330        // asymmetry with the three walks below is deliberate. A source answering two
1331        // cursors with each other advances on every page, so `unrepeated` under the list
1332        // verb never fires; the cycle shows only as a token handed back unchanged, which
1333        // is what this loop pages by. Point it at the source and a project copy spins.
1334        //
1335        // No page bound here for the same reason: `Engine::tasks` merges under the budget
1336        // it was asked, so nothing longer than `PROJECT_PAGE` can arrive. `fits` in
1337        // `fetch::walk` refuses the page a source can really overrun, its own, while
1338        // these very members are read.
1339        let misbehaved = |error| EngineError::SourceRefused {
1340            name: project.source.to_string(),
1341            error,
1342        };
1343        loop {
1344            let asked = request.paging.token.clone();
1345            let response = self.tasks(&request).await?;
1346            if let Some(failure) = response.errors.first() {
1347                return Err(EngineError::SourceRefused {
1348                    name: failure.source.to_string(),
1349                    error: failure.error.clone(),
1350                });
1351            }
1352            unrepeated(
1353                response.next.as_ref(),
1354                asked.as_ref(),
1355                "the tasks of a project were being read for a copy",
1356            )
1357            .map_err(misbehaved)?;
1358            members.extend(response.items);
1359            match response.next {
1360                Some(token) => request.paging.token = Some(token),
1361                None => return Ok(members),
1362            }
1363        }
1364    }
1365
1366    /// Every document the source holds in `project`, as the source reported it.
1367    ///
1368    /// Paged by this engine's own token for the reason the member walk above is, and the
1369    /// note there says why.
1370    async fn project_documents(
1371        &self,
1372        project: &GlobalId,
1373    ) -> Result<Vec<Qualified<Document>>, EngineError> {
1374        let mut request = DocumentRequest {
1375            sources: vec![project.source.clone()],
1376            filters: DocumentFilters::default(),
1377            project: ProjectSelector::Qualified(project.clone()),
1378            paging: Paging {
1379                limit: PROJECT_PAGE,
1380                token: None,
1381            },
1382        };
1383        let mut held = Vec::new();
1384        let misbehaved = |error| EngineError::SourceRefused {
1385            name: project.source.to_string(),
1386            error,
1387        };
1388        loop {
1389            let asked = request.paging.token.clone();
1390            let response = self.documents(&request).await?;
1391            if let Some(failure) = response.errors.first() {
1392                return Err(EngineError::SourceRefused {
1393                    name: failure.source.to_string(),
1394                    error: failure.error.clone(),
1395                });
1396            }
1397            unrepeated(
1398                response.next.as_ref(),
1399                asked.as_ref(),
1400                "the documents of a project were being read for a copy",
1401            )
1402            .map_err(misbehaved)?;
1403            held.extend(response.items);
1404            match response.next {
1405                Some(token) => request.paging.token = Some(token),
1406                None => return Ok(held),
1407            }
1408        }
1409    }
1410
1411    /// Point every reference the documents of this copy hold at the destination's own
1412    /// records, and say how many it could not.
1413    ///
1414    /// A document copied out of a local Markdown store used to arrive naming absolute paths
1415    /// under one checkout on one machine, dead for the only reader the copy exists for,
1416    /// while the destination held its own record for every one of them the whole time. This
1417    /// is what closes that, and it is deliberately **not** a Markdown-link parser: the
1418    /// artifact that motivated it holds bare absolute paths inside backticks in a table
1419    /// cell, which `[text](target)` matching would have left exactly as it found them.
1420    ///
1421    /// The correspondence is re-established from what the *destination* records at
1422    /// [`GlobalId::ORIGIN_KEY`], not from the mapping this copy holds. That mapping is not
1423    /// available when it is needed: `project copy` and `document copy` are separate verbs,
1424    /// one invocation carries one [`CopyScope`], and a project copy carries no documents —
1425    /// so by the time the document is copied, the tasks were written by a process that has
1426    /// exited. Reading the destination is also what makes a document copied on its own
1427    /// work, which a same-run mapping never could.
1428    ///
1429    /// Tasks and projects are not touched. Only a document's content is rewritten, and
1430    /// nothing else about it changes.
1431    async fn rewrite_references(
1432        &self,
1433        destination: &ResolvedSource,
1434        planned: &mut [Planned],
1435        counts: &mut Counted,
1436    ) -> Result<(), EngineError> {
1437        // Read at the source, once per project rather than once per document: several
1438        // documents of one project is the ordinary case.
1439        let mut by_project: BTreeMap<String, Vec<Referent>> = BTreeMap::new();
1440        let mut named: Vec<Vec<Referent>> = Vec::new();
1441        for item in planned.iter() {
1442            named.push(self.named_referents(item, &mut by_project).await?);
1443        }
1444        // The destination is walked only for a copy that really names something, and only
1445        // for the interfaces those referents are read from.
1446        let mut levels: Vec<Level> = Vec::new();
1447        for referent in named.iter().flatten() {
1448            if !levels.contains(&referent.level) {
1449                levels.push(referent.level);
1450            }
1451        }
1452        if levels.is_empty() {
1453            return Ok(());
1454        }
1455        let counterparts = self.counterparts(destination, &levels).await?;
1456        for (item, referents) in planned.iter_mut().zip(named) {
1457            let Item::Document(document) = &mut item.item else {
1458                continue;
1459            };
1460            let Some(content) = &document.content else {
1461                continue;
1462            };
1463            let (rewritten, made) = substitute(content, &table_for(&referents, &counterparts));
1464            document.content = Some(rewritten);
1465            counts.add(made);
1466        }
1467        Ok(())
1468    }
1469
1470    /// The records of one document's own project whose location string its content really
1471    /// holds, as a whole reference.
1472    ///
1473    /// A document with no content, or with no project at the source, names nothing: the
1474    /// referent set is the document's own project, its tasks and its other documents, and
1475    /// there is no such set without a project.
1476    async fn named_referents(
1477        &self,
1478        item: &Planned,
1479        by_project: &mut BTreeMap<String, Vec<Referent>>,
1480    ) -> Result<Vec<Referent>, EngineError> {
1481        let Item::Document(document) = &item.item else {
1482            return Ok(Vec::new());
1483        };
1484        let (Some(content), Some(project)) =
1485            (document.content.as_deref(), document.project.as_ref())
1486        else {
1487            return Ok(Vec::new());
1488        };
1489        if content.is_empty() {
1490            return Ok(Vec::new());
1491        }
1492        let project = GlobalId::new(item.source.source.clone(), project.clone());
1493        let key = project.to_string();
1494        if !by_project.contains_key(&key) {
1495            let read = self.referents(&project).await?;
1496            by_project.insert(key.clone(), read);
1497        }
1498        Ok(by_project[&key]
1499            .iter()
1500            // A document does not name itself: the referent set is every *other* record
1501            // filed under the project. Told by the interface as well as the id, because an
1502            // id alone does not identify a record — a folder of Markdown filing `A.md`
1503            // under both `tasks/` and `documents/` is the ordinary case rather than the
1504            // contrived one, and excluding by id alone would drop that task from the set.
1505            .filter(|referent| referent.level != Level::Document || referent.id != item.source)
1506            .filter(|referent| holds(content, &referent.location))
1507            .cloned()
1508            .collect())
1509    }
1510
1511    /// Every record filed under one project at its own source, with the location string
1512    /// that source reports for it.
1513    ///
1514    /// The project record itself, every task filed under it, and every document filed under
1515    /// it. All three are reads this engine already knows how to make.
1516    async fn referents(&self, project: &GlobalId) -> Result<Vec<Referent>, EngineError> {
1517        let source = self.readable(&project.source)?;
1518        let mut referents = Vec::new();
1519        if let Some(held) = source
1520            .source()
1521            .get_project(&project.native)
1522            .await
1523            .map_err(|error| refused(source, error))?
1524        {
1525            note(
1526                &mut referents,
1527                project.clone(),
1528                Level::Project,
1529                held.location.as_ref(),
1530                &held.metadata,
1531            );
1532        }
1533        for task in self.project_member_tasks(project).await? {
1534            note(
1535                &mut referents,
1536                task.id,
1537                Level::Task,
1538                task.item.location.as_ref(),
1539                &task.item.metadata,
1540            );
1541        }
1542        for document in self.project_documents(project).await? {
1543            note(
1544                &mut referents,
1545                document.id,
1546                Level::Document,
1547                document.item.location.as_ref(),
1548                &document.item.metadata,
1549            );
1550        }
1551        Ok(referents)
1552    }
1553
1554    /// Walk the destination once for every record it holds at the levels named, one page
1555    /// at a time.
1556    ///
1557    /// One page is *read* at a time, and what is kept from each is three fields of each
1558    /// record — its id, the origin it records and the location the destination reports —
1559    /// never the page. That is more than [`Engine::scan`] keeps, and deliberately: the whole
1560    /// point of this walk is that one pass answers every document and every referent of the
1561    /// invocation, so what it learns has to outlive the page it learned it from. Nothing is
1562    /// written down and the index is dropped with the call.
1563    ///
1564    /// The other difference from `scan` is the *answer*: this one keeps every match, so a
1565    /// destination holding two records for one work item is reported as ambiguous rather
1566    /// than resolved to the first.
1567    async fn counterparts(
1568        &self,
1569        destination: &ResolvedSource,
1570        levels: &[Level],
1571    ) -> Result<Counterparts, EngineError> {
1572        let mut found = Counterparts::default();
1573        for level in levels {
1574            // Every cursor this level has already been sent. `unrepeated` below catches a
1575            // source that hands back the cursor it was just given, and its own note says
1576            // why it catches no more than that: a source cycling through two cursors
1577            // advances on every page, and seeing it needs memory the walks that share that
1578            // helper do not keep. This walk does keep memory — it is building an index that
1579            // outlives each page — so here the memory exists and the cycle is caught. The
1580            // walks one level up catch the same defect as a page token handed back
1581            // unchanged; this one pages by the source's own cursor and has no level above
1582            // it, so nothing else would.
1583            let mut asked_before: BTreeSet<String> = BTreeSet::new();
1584            let mut cursor: Option<Cursor> = None;
1585            loop {
1586                if let Some(next) = &cursor
1587                    && !asked_before.insert(next.0.clone())
1588                {
1589                    return Err(refused(
1590                        destination,
1591                        SourceError::Malformed {
1592                            message: "the source returned a cursor it had already been \
1593                                      given while the destination was being walked for the \
1594                                      records a document's references name, so the walk \
1595                                      would never end"
1596                                .to_owned(),
1597                        },
1598                    ));
1599                }
1600                let asked = cursor.clone();
1601                let request = request_for(destination, cursor);
1602                let next = match level {
1603                    Level::Task => {
1604                        let page = destination
1605                            .source()
1606                            .query_tasks(&TaskQuery::default(), &request)
1607                            .await
1608                            .map_err(|error| refused(destination, error))?;
1609                        fits(page.items.len(), request.limit)
1610                            .map_err(|error| refused(destination, error))?;
1611                        for task in &page.items {
1612                            found.note(*level, &task.id, task.location.as_ref(), &task.metadata);
1613                        }
1614                        page.next
1615                    }
1616                    Level::Project => {
1617                        let page = destination
1618                            .source()
1619                            .query_projects(&ProjectQuery::default(), &request)
1620                            .await
1621                            .map_err(|error| refused(destination, error))?;
1622                        fits(page.items.len(), request.limit)
1623                            .map_err(|error| refused(destination, error))?;
1624                        for project in &page.items {
1625                            found.note(
1626                                *level,
1627                                &project.id,
1628                                project.location.as_ref(),
1629                                &project.metadata,
1630                            );
1631                        }
1632                        page.next
1633                    }
1634                    Level::Document => {
1635                        let page = destination
1636                            .source()
1637                            .query_documents(&DocumentQuery::default(), &request)
1638                            .await
1639                            .map_err(|error| refused(destination, error))?;
1640                        fits(page.items.len(), request.limit)
1641                            .map_err(|error| refused(destination, error))?;
1642                        for document in &page.items {
1643                            found.note(
1644                                *level,
1645                                &document.id,
1646                                document.location.as_ref(),
1647                                &document.metadata,
1648                            );
1649                        }
1650                        page.next
1651                    }
1652                };
1653                unrepeated(
1654                    next.as_ref(),
1655                    asked.as_ref(),
1656                    "the destination was being walked for the records a document's \
1657                     references name",
1658                )
1659                .map_err(|error| refused(destination, error))?;
1660                match next {
1661                    Some(next) => cursor = Some(next),
1662                    None => break,
1663                }
1664            }
1665        }
1666        Ok(found)
1667    }
1668
1669    /// Destination tasks filed under the copied project whose origin the source no longer
1670    /// holds.
1671    ///
1672    /// A copy never deletes, so each is left exactly as it is and reported.
1673    async fn orphans(
1674        &self,
1675        destination: &ResolvedSource,
1676        project: &GlobalId,
1677        at_destination: &NativeId,
1678        copied: &[GlobalId],
1679    ) -> Result<Vec<CopyOutcome>, EngineError> {
1680        let mut orphans = Vec::new();
1681        let mut cursor: Option<Cursor> = None;
1682        loop {
1683            let asked = cursor.clone();
1684            let request = request_for(destination, cursor);
1685            let page: Page<Task> = destination
1686                .source()
1687                .query_tasks(&TaskQuery::default(), &request)
1688                .await
1689                .map_err(|error| refused(destination, error))?;
1690            fits(page.items.len(), request.limit).map_err(|error| refused(destination, error))?;
1691            for task in &page.items {
1692                if task.project.as_ref() != Some(at_destination) {
1693                    continue;
1694                }
1695                let Some(origin) = origin_of(&task.metadata) else {
1696                    continue;
1697                };
1698                if origin.source != project.source || copied.contains(&origin) {
1699                    continue;
1700                }
1701                orphans.push(CopyOutcome {
1702                    source: origin,
1703                    action: CopyAction::Orphaned {
1704                        destination: GlobalId::new(destination.name().clone(), task.id.clone()),
1705                    },
1706                });
1707            }
1708            unrepeated(
1709                page.next.as_ref(),
1710                asked.as_ref(),
1711                "the destination was being read for items the copy left behind",
1712            )
1713            .map_err(|error| refused(destination, error))?;
1714            match page.next {
1715                Some(next) => cursor = Some(next),
1716                None => return Ok(orphans),
1717            }
1718        }
1719    }
1720
1721    /// Resolve and write every item planned, holding back the ones whose edges are not
1722    /// resolvable yet.
1723    ///
1724    /// An edge between two items of one copy can point at a member whose destination id
1725    /// does not exist until it has been created, so the item that points at it lands
1726    /// without that edge and is handed to `deferred`. [`Engine::repair`] finishes it once
1727    /// the *whole* request has landed — not once this call has, because the far end may
1728    /// be in another project of the same command.
1729    async fn copy_items(
1730        &self,
1731        destination: &ResolvedSource,
1732        request: &CopyRequest,
1733        mut planned: Vec<Planned>,
1734        project: Option<NativeId>,
1735        running: &mut Running,
1736        journal: &mut Journal,
1737    ) -> Result<Vec<CopyOutcome>, EngineError> {
1738        // Only a document's own content names other records, and only once every document
1739        // of this call has been read: the destination is walked once for all of them, and
1740        // the content the rest of this call lands is the rewritten one — which is what
1741        // makes a repeat copy of an already-rewritten document report `unchanged`.
1742        if planned
1743            .iter()
1744            .any(|item| item.item.level() == Level::Document)
1745        {
1746            self.rewrite_references(destination, &mut planned, &mut running.references)
1747                .await?;
1748        }
1749
1750        for item in &planned {
1751            if let Target::Update { id, .. } = &item.target {
1752                running
1753                    .counterparts
1754                    .insert(item.source.to_string(), id.clone());
1755            }
1756            if let Item::Task(task) = &item.item {
1757                running.delivers_rewritten +=
1758                    members_named(&task.delivers, &item.source.source, &running.resolvable);
1759            }
1760        }
1761
1762        // Resolved once per item, and used by both passes: the repair pass writes the
1763        // same item again, and re-deriving this there could file it somewhere else.
1764        let mut filed = Vec::new();
1765        for item in &planned {
1766            filed.push(
1767                self.filed(destination, item, project.clone(), running)
1768                    .await?,
1769            );
1770        }
1771
1772        let mut outcomes = Vec::new();
1773        let mut unresolved = Vec::new();
1774        let mut priors = Vec::new();
1775        for (index, item) in planned.iter().enumerate() {
1776            let edges = mapped_edges(
1777                &item.edges,
1778                &item.source.source,
1779                destination,
1780                &running.resolvable,
1781                &running.counterparts,
1782            );
1783            let delivers = delivers_of(
1784                item,
1785                destination,
1786                &running.resolvable,
1787                &running.counterparts,
1788            );
1789            if edges.iter().any(Option::is_none) || delivers.iter().any(Option::is_none) {
1790                unresolved.push(index);
1791            }
1792            let resolvable_delivers = resolved_entries(&delivers);
1793            let (outcome, prior) = self
1794                .land(
1795                    destination,
1796                    request,
1797                    item,
1798                    filed[index].clone(),
1799                    Pointing {
1800                        edges: &edges,
1801                        delivers: &resolvable_delivers,
1802                    },
1803                    journal,
1804                )
1805                .await?;
1806            if let Some(id) = outcome.destination() {
1807                running
1808                    .counterparts
1809                    .insert(item.source.to_string(), id.native.clone());
1810            }
1811            if !request.dry_run
1812                && let (Item::Task(task), Some(landed)) = (&item.item, outcome.destination())
1813            {
1814                running.landed.push(LandedTask {
1815                    destination: landed.clone(),
1816                    origin: item.source.source.clone(),
1817                    delivers: task.delivers.clone(),
1818                    before: match prior.as_ref().map(|prior| &prior.item) {
1819                        Some(Item::Task(held)) => targets(&held.delivers, destination.name()),
1820                        _ => Vec::new(),
1821                    },
1822                    category: task.status.category,
1823                });
1824            }
1825            outcomes.push(outcome);
1826            priors.push(prior);
1827        }
1828
1829        if !request.dry_run {
1830            for (index, item) in planned.into_iter().enumerate() {
1831                if !unresolved.contains(&index) {
1832                    continue;
1833                }
1834                // Every item a copy that is not a dry run lands has a destination id: the
1835                // one outcome without one is a dry run that would have created, and this
1836                // block does not run for a dry run.
1837                let id = outcomes[index]
1838                    .destination()
1839                    .expect("a copy that writes lands every item it planned")
1840                    .clone();
1841                running.deferred.push(Deferred {
1842                    item,
1843                    filed: filed[index].clone(),
1844                    destination: id.native,
1845                    prior: priors[index].clone(),
1846                });
1847            }
1848        }
1849        Ok(outcomes)
1850    }
1851
1852    /// Read one item and its forward edges, and decide where it is going.
1853    async fn plan(
1854        &self,
1855        destination: &ResolvedSource,
1856        request: &CopyRequest,
1857        kind: Level,
1858        id: &GlobalId,
1859    ) -> Result<Planned, EngineError> {
1860        let source = self.readable(&id.source)?;
1861        if kind == Level::Document {
1862            documentary(source)?;
1863        }
1864        let item = match kind {
1865            Level::Task => source
1866                .source()
1867                .get_task(&id.native)
1868                .await
1869                .map_err(|error| refused(source, error))?
1870                .map(|task| Item::Task(Box::new(task))),
1871            Level::Project => source
1872                .source()
1873                .get_project(&id.native)
1874                .await
1875                .map_err(|error| refused(source, error))?
1876                .map(|project| Item::Project(Box::new(project))),
1877            Level::Document => source
1878                .source()
1879                .get_document(&id.native)
1880                .await
1881                .map_err(|error| refused(source, error))?
1882                .map(|document| Item::Document(Box::new(document))),
1883        }
1884        .ok_or_else(|| EngineError::NoSuchItem { id: id.to_string() })?;
1885        let edges = forward_edges(source, &id.native, item.level()).await?;
1886        let (target, held) = self.target(destination, request, id, &item).await?;
1887        Ok(Planned {
1888            source: id.clone(),
1889            item,
1890            edges,
1891            target,
1892            held,
1893        })
1894    }
1895
1896    /// Which destination item this one corresponds to, by the two origin rules and the
1897    /// caller's escape, and what the destination holds there.
1898    async fn target(
1899        &self,
1900        destination: &ResolvedSource,
1901        request: &CopyRequest,
1902        id: &GlobalId,
1903        item: &Item,
1904    ) -> Result<(Target, Option<Prior>), EngineError> {
1905        let (title, metadata) = described(item);
1906        if let Some(origin) = origin_of(metadata)
1907            && &origin.source == destination.name()
1908        {
1909            // The read that says the origin still names something is the read of what it
1910            // holds, so rule 1 costs one round trip rather than two.
1911            if let Some(held) = self
1912                .prior(destination, item.level(), &origin.native)
1913                .await?
1914            {
1915                return Ok((
1916                    Target::Update {
1917                        id: origin.native,
1918                        found: Found::Origin,
1919                    },
1920                    Some(held),
1921                ));
1922            }
1923            if !request.recreate {
1924                return Err(EngineError::StaleOrigin {
1925                    item: id.to_string(),
1926                    origin: origin.to_string(),
1927                });
1928            }
1929        }
1930        if let Some(found) = self
1931            .scan(destination, item.level(), &Wanted::Origin(id.to_string()))
1932            .await?
1933        {
1934            let held = self.prior(destination, item.level(), &found).await?;
1935            return Ok((
1936                Target::Update {
1937                    id: found,
1938                    found: Found::Search,
1939                },
1940                held,
1941            ));
1942        }
1943        let wanted = match &request.match_by {
1944            Some(MatchBy::Title) => Some(Wanted::Title(title.to_owned())),
1945            Some(MatchBy::Metadata(key)) => metadata
1946                .get(key)
1947                .map(|value| Wanted::Metadata(key.clone(), value.clone())),
1948            None => None,
1949        };
1950        if let Some(wanted) = wanted
1951            && let Some(found) = self.scan(destination, item.level(), &wanted).await?
1952        {
1953            let held = self.prior(destination, item.level(), &found).await?;
1954            return Ok((
1955                Target::Update {
1956                    id: found,
1957                    found: Found::Search,
1958                },
1959                held,
1960            ));
1961        }
1962        Ok((Target::Create, None))
1963    }
1964
1965    /// Walk the destination one page at a time, looking for `wanted`.
1966    ///
1967    /// One page is held at a time and nothing is written down, which is the same bound
1968    /// every other compensation in this engine works under.
1969    async fn scan(
1970        &self,
1971        destination: &ResolvedSource,
1972        kind: Level,
1973        wanted: &Wanted,
1974    ) -> Result<Option<NativeId>, EngineError> {
1975        let mut cursor: Option<Cursor> = None;
1976        loop {
1977            let asked = cursor.clone();
1978            let request = request_for(destination, cursor);
1979            let next = match kind {
1980                Level::Task => {
1981                    let page = destination
1982                        .source()
1983                        .query_tasks(&TaskQuery::default(), &request)
1984                        .await
1985                        .map_err(|error| refused(destination, error))?;
1986                    fits(page.items.len(), request.limit)
1987                        .map_err(|error| refused(destination, error))?;
1988                    for task in &page.items {
1989                        if wanted.found(&task.title, &task.metadata) {
1990                            return Ok(Some(task.id.clone()));
1991                        }
1992                    }
1993                    page.next
1994                }
1995                Level::Project => {
1996                    let page = destination
1997                        .source()
1998                        .query_projects(&ProjectQuery::default(), &request)
1999                        .await
2000                        .map_err(|error| refused(destination, error))?;
2001                    fits(page.items.len(), request.limit)
2002                        .map_err(|error| refused(destination, error))?;
2003                    for project in &page.items {
2004                        if wanted.found(&project.title, &project.metadata) {
2005                            return Ok(Some(project.id.clone()));
2006                        }
2007                    }
2008                    page.next
2009                }
2010                Level::Document => {
2011                    let page = destination
2012                        .source()
2013                        .query_documents(&DocumentQuery::default(), &request)
2014                        .await
2015                        .map_err(|error| refused(destination, error))?;
2016                    fits(page.items.len(), request.limit)
2017                        .map_err(|error| refused(destination, error))?;
2018                    for document in &page.items {
2019                        if wanted.found(&document.title, &document.metadata) {
2020                            return Ok(Some(document.id.clone()));
2021                        }
2022                    }
2023                    page.next
2024                }
2025            };
2026            unrepeated(
2027                next.as_ref(),
2028                asked.as_ref(),
2029                "the destination was being scanned for the item to update",
2030            )
2031            .map_err(|error| refused(destination, error))?;
2032            match next {
2033                Some(next) => cursor = Some(next),
2034                None => return Ok(None),
2035            }
2036        }
2037    }
2038
2039    /// Write one planned item, or say what a dry run would have done.
2040    ///
2041    /// Answers with what the destination held there beforehand as well, which is what
2042    /// makes an item written twice restorable to what it was rather than to what this
2043    /// copy's first pass left.
2044    async fn land(
2045        &self,
2046        destination: &ResolvedSource,
2047        request: &CopyRequest,
2048        item: &Planned,
2049        project: Option<NativeId>,
2050        pointing: Pointing<'_>,
2051        journal: &mut Journal,
2052    ) -> Result<(CopyOutcome, Option<Prior>), EngineError> {
2053        let Pointing { edges, delivers } = pointing;
2054        let target = match &item.target {
2055            Target::Update { id, .. } => Some(id.clone()),
2056            Target::Create => None,
2057        };
2058        // The one read of the destination item, made where its target was found, used to
2059        // decide whether the write changes anything and — if the copy cannot finish — to
2060        // put that item back.
2061        let prior = item.held.clone();
2062        let edges = resolved(edges);
2063        let qualified = |native: NativeId| GlobalId::new(destination.name().clone(), native);
2064        if let Some(id) = &target
2065            && !changes(
2066                prior.as_ref(),
2067                item,
2068                id,
2069                &project,
2070                &edges,
2071                delivers,
2072                destination.name(),
2073            )
2074        {
2075            return Ok((
2076                CopyOutcome {
2077                    source: item.source.clone(),
2078                    action: CopyAction::Unchanged {
2079                        destination: qualified(id.clone()),
2080                    },
2081                },
2082                prior,
2083            ));
2084        }
2085        if request.dry_run {
2086            return Ok((
2087                CopyOutcome {
2088                    source: item.source.clone(),
2089                    action: match target {
2090                        Some(id) => CopyAction::Updated {
2091                            destination: qualified(id),
2092                        },
2093                        // Null only here: nothing was created, so there is no id to report.
2094                        None => CopyAction::Created { destination: None },
2095                    },
2096                },
2097                prior,
2098            ));
2099        }
2100        let updating = target.is_some();
2101        let written = qualified(
2102            self.write(
2103                destination,
2104                item,
2105                target,
2106                project,
2107                &edges,
2108                delivers,
2109                prior.clone(),
2110                journal,
2111            )
2112            .await?,
2113        );
2114        Ok((
2115            CopyOutcome {
2116                source: item.source.clone(),
2117                action: if updating {
2118                    CopyAction::Updated {
2119                        destination: written,
2120                    }
2121                } else {
2122                    CopyAction::Created {
2123                        destination: Some(written),
2124                    }
2125                },
2126            },
2127            prior,
2128        ))
2129    }
2130
2131    /// Which destination project this item is filed under, when it is filed at all.
2132    ///
2133    /// A task copied as part of a project copy is filed under that project's counterpart,
2134    /// which the copy has just established. A task copied on its own has to find it.
2135    async fn filed(
2136        &self,
2137        destination: &ResolvedSource,
2138        item: &Planned,
2139        project: Option<NativeId>,
2140        running: &mut Running,
2141    ) -> Result<Option<NativeId>, EngineError> {
2142        match (&item.item, project) {
2143            (Item::Task(task), None) => {
2144                self.counterpart(destination, item, task.project.as_ref(), running)
2145                    .await
2146            }
2147            (Item::Document(document), None) => {
2148                self.counterpart(destination, item, document.project.as_ref(), running)
2149                    .await
2150            }
2151            (Item::Task(_) | Item::Document(_), filed) => Ok(filed),
2152            (Item::Project(_), _) => Ok(None),
2153        }
2154    }
2155
2156    /// The destination project this task's own project corresponds to, when there is one.
2157    ///
2158    /// A task copied on its own keeps its source's project id when the destination holds
2159    /// no counterpart: the field is opaque to this engine, and dropping it would lose
2160    /// what the source said.
2161    ///
2162    /// Looked for once per source project per command. A project this command itself
2163    /// copied is already known, and one an earlier task of this command was filed under
2164    /// was already looked for; walking the destination again for either would spend a
2165    /// scan per task on an answer the command holds.
2166    async fn counterpart(
2167        &self,
2168        destination: &ResolvedSource,
2169        item: &Planned,
2170        project: Option<&NativeId>,
2171        running: &mut Running,
2172    ) -> Result<Option<NativeId>, EngineError> {
2173        let Some(project) = project else {
2174            return Ok(None);
2175        };
2176        let qualified = GlobalId::new(item.source.source.clone(), project.clone()).to_string();
2177        if let Some(landed) = running.counterparts.get(&qualified) {
2178            return Ok(Some(landed.clone()));
2179        }
2180        if let Some(looked) = running.filings.get(&qualified) {
2181            return Ok(looked.clone());
2182        }
2183        let found = self
2184            .scan(
2185                destination,
2186                Level::Project,
2187                &Wanted::Origin(qualified.clone()),
2188            )
2189            .await?;
2190        let filed = Some(found.unwrap_or_else(|| project.clone()));
2191        running.filings.insert(qualified, filed.clone());
2192        Ok(filed)
2193    }
2194
2195    /// What the destination holds at one id, item and forward edges together.
2196    ///
2197    /// One read for both purposes it serves — deciding whether a write changes anything,
2198    /// and putting the item back if the copy cannot finish — because a second read of the
2199    /// same item is a second round trip against a hosted destination for nothing.
2200    async fn prior(
2201        &self,
2202        destination: &ResolvedSource,
2203        kind: Level,
2204        id: &NativeId,
2205    ) -> Result<Option<Prior>, EngineError> {
2206        let held = match kind {
2207            Level::Task => destination
2208                .source()
2209                .get_task(id)
2210                .await
2211                .map_err(|error| refused(destination, error))?
2212                .map(|task| Item::Task(Box::new(task))),
2213            Level::Project => destination
2214                .source()
2215                .get_project(id)
2216                .await
2217                .map_err(|error| refused(destination, error))?
2218                .map(|project| Item::Project(Box::new(project))),
2219            Level::Document => destination
2220                .source()
2221                .get_document(id)
2222                .await
2223                .map_err(|error| refused(destination, error))?
2224                .map(|document| Item::Document(Box::new(document))),
2225        };
2226        let Some(item) = held else {
2227            return Ok(None);
2228        };
2229        let edges = forward_edges(destination, id, kind).await?;
2230        Ok(Some(Prior { item, edges }))
2231    }
2232
2233    /// Hand one item to the destination's own write interface, recording how to take it
2234    /// back.
2235    // llmlint: ignore[suppressions_justified] A write is the item, where it is going, what
2236    // it is filed under, its edges, what was there before and the journal that records how
2237    // to put it back. Each is a distinct decision made by a different part of the copy, and
2238    // grouping them would only move the argument list to a constructor.
2239    #[allow(clippy::too_many_arguments)]
2240    async fn write(
2241        &self,
2242        destination: &ResolvedSource,
2243        item: &Planned,
2244        target: Option<NativeId>,
2245        project: Option<NativeId>,
2246        edges: &[DependencyEdge],
2247        delivers: &[TaskRef],
2248        prior: Option<Prior>,
2249        journal: &mut Journal,
2250    ) -> Result<NativeId, EngineError> {
2251        let created_kind = item.item.level();
2252        let suggested = target.clone().unwrap_or_else(|| item.item.id().clone());
2253        // Settled before the journal takes `prior`, and from that same read: what the
2254        // destination holds at the origin key is what a copy-back leaves there, and what it
2255        // holds as `delivered_by` is what the item keeps.
2256        let origin = recorded(item, prior.as_ref());
2257        let landing = outgoing(item, suggested, project, &origin, delivers, prior.as_ref());
2258        // Recorded *before* the write rather than after it. A destination's own write is
2259        // several calls — `docs/plugin-protocol.md` §4.9 — and one of them failing leaves
2260        // the ones before it applied. No source can put those back, because only this
2261        // journal holds what was there; recorded after a successful write, an update that
2262        // stopped part way was the one way a copy could end and leave the destination
2263        // altered. A restore of an item the write never reached rewrites what is already
2264        // there, which costs one mutation and is what "either complete or it never
2265        // happened" is worth.
2266        if let (Some(id), Some(prior)) = (target.clone(), prior) {
2267            journal.record(Undo::Updated { id, prior });
2268        }
2269        let landed = match landing {
2270            Item::Task(task) => destination
2271                .source()
2272                .write_task(&ItemWrite {
2273                    target: target.clone(),
2274                    item: *task,
2275                    depends_on: edges.to_vec(),
2276                })
2277                .await
2278                .map_err(|error| refused(destination, error))?,
2279            Item::Project(project) => destination
2280                .source()
2281                .write_project(&ItemWrite {
2282                    target: target.clone(),
2283                    item: *project,
2284                    depends_on: edges.to_vec(),
2285                })
2286                .await
2287                .map_err(|error| refused(destination, error))?,
2288            // No edges, and that is the contract: a document takes part in no dependency
2289            // graph, so there is nothing here for `depends_on` to carry.
2290            Item::Document(document) => destination
2291                .source()
2292                .write_document(&ItemWrite {
2293                    target: target.clone(),
2294                    item: *document,
2295                    depends_on: Vec::new(),
2296                })
2297                .await
2298                .map_err(|error| refused(destination, error))?,
2299        };
2300        // A created item can only be journalled here: its id is what the write answers
2301        // with. A create that fails leaves nothing behind — §4.9 makes taking the item
2302        // back the source's own duty, because a write that refused must not leave an item
2303        // nobody asked for.
2304        if target.is_none() {
2305            journal.record(Undo::Created {
2306                kind: created_kind,
2307                id: landed.clone(),
2308            });
2309        }
2310        Ok(landed)
2311    }
2312
2313    /// A configured source that built, for reading an item out of.
2314    fn readable(&self, name: &SourceName) -> Result<&ResolvedSource, EngineError> {
2315        let name = self.known(name)?;
2316        if let Some(unavailable) = self.unavailable().find(|source| source.name() == &name) {
2317            return Err(EngineError::SourceRefused {
2318                name: name.to_string(),
2319                error: unavailable.error().clone(),
2320            });
2321        }
2322        self.ready()
2323            .find(|source| source.name() == &name)
2324            .ok_or(EngineError::NoSources)
2325    }
2326}
2327
2328/// How many tasks of a project are read at once while walking it.
2329const PROJECT_PAGE: std::num::NonZeroU32 = std::num::NonZeroU32::new(50).expect("50 is not zero");
2330
2331/// Each source's own running totals, in the order the sources were given.
2332///
2333/// A reading a source could not take is read as that source not metering: what a command
2334/// spent is a report about the work, and a failed reading must not become a failure of the
2335/// work itself. That holds when only one of a source's two readings failed as well, since
2336/// a difference needs both ends.
2337async fn readings(sources: &[&ResolvedSource]) -> Vec<Option<Metering>> {
2338    let mut read = Vec::with_capacity(sources.len());
2339    for source in sources {
2340        read.push(source.source().metering().await.ok().flatten());
2341    }
2342    read
2343}
2344
2345/// What the sources spent between two readings of them, or `None` when none of them meters.
2346///
2347/// A source that does not meter contributes nothing and does not make the total zero: a
2348/// command whose sources all declined reports that it cannot say, not that it spent nothing.
2349/// A budget is keyed by its name and unit together, so two sources naming one budget add up
2350/// and two units of one budget stay apart.
2351///
2352/// A source's two readings are a plugin's word, so they are held to [`Metering`]'s contract
2353/// before either is believed, and a pair that breaks it is that source not metering — see
2354/// [`difference`].
2355fn spent_between(before: &[Option<Metering>], after: &[Option<Metering>]) -> Option<Spent> {
2356    let mut metered = false;
2357    let mut requests = 0_u64;
2358    let mut budgets: BTreeMap<(String, String), (u64, u64)> = BTreeMap::new();
2359    for (before, after) in before.iter().zip(after) {
2360        let (Some(before), Some(after)) = (before, after) else {
2361            continue;
2362        };
2363        let Some((sent, spent)) = difference(before, after) else {
2364            continue;
2365        };
2366        metered = true;
2367        requests = requests.saturating_add(sent);
2368        for (key, measured, modelled) in spent {
2369            let total = budgets.entry(key).or_default();
2370            total.0 = total.0.saturating_add(measured).saturating_add(modelled);
2371            total.1 = total.1.saturating_add(modelled);
2372        }
2373    }
2374    metered.then(|| Spent {
2375        requests,
2376        budgets: budgets
2377            .into_iter()
2378            .map(|((budget, unit), (amount, modelled))| BudgetSpent {
2379                budget,
2380                unit,
2381                amount,
2382                lower_bound: modelled > 0,
2383            })
2384            .collect(),
2385    })
2386}
2387
2388/// One budget's name and unit, and the measured and modelled amounts spent against it.
2389type BudgetDifference = ((String, String), u64, u64);
2390
2391/// What one source sent and spent between two of its readings, or `None` when the pair
2392/// cannot be a running total's.
2393///
2394/// A running total never falls, never drops a budget it has named, and names every budget
2395/// it keeps, once; a pair that breaks any of that is a source that reset its figures or
2396/// reported something else, and a difference taken over it would be a number that measures
2397/// nothing. Such a source is reported as not metering rather than as having spent a clamped
2398/// zero.
2399fn difference(before: &Metering, after: &Metering) -> Option<(u64, Vec<BudgetDifference>)> {
2400    let sent = after.requests.checked_sub(before.requests)?;
2401    for reading in [before, after] {
2402        let mut named = BTreeSet::new();
2403        for budget in &reading.budgets {
2404            if budget.budget.is_empty()
2405                || budget.unit.is_empty()
2406                || !named.insert((&budget.budget, &budget.unit))
2407            {
2408                return None;
2409            }
2410        }
2411    }
2412    let held = |from: &Metering, budget: &onetaskgraph_plugin_api::Metered| {
2413        from.budgets
2414            .iter()
2415            .find(|held| held.budget == budget.budget && held.unit == budget.unit)
2416            .cloned()
2417    };
2418    if before
2419        .budgets
2420        .iter()
2421        .any(|budget| held(after, budget).is_none())
2422    {
2423        return None;
2424    }
2425    let mut spent = Vec::with_capacity(after.budgets.len());
2426    for budget in &after.budgets {
2427        let earlier = held(before, budget);
2428        let measured = budget
2429            .measured
2430            .checked_sub(earlier.as_ref().map_or(0, |held| held.measured))?;
2431        let modelled = budget
2432            .modelled
2433            .checked_sub(earlier.as_ref().map_or(0, |held| held.modelled))?;
2434        spent.push((
2435            (budget.budget.clone(), budget.unit.clone()),
2436            measured,
2437            modelled,
2438        ));
2439    }
2440    Some((sent, spent))
2441}
2442
2443/// One page request against `source`, at the largest page it will serve.
2444fn request_for(source: &ResolvedSource, cursor: Option<Cursor>) -> PageRequest {
2445    PageRequest {
2446        cursor,
2447        limit: source.source().capabilities().max_page_size.max(1),
2448    }
2449}
2450
2451/// Whether writing this item would change what the destination already holds.
2452///
2453/// A free function over the state already read rather than a method that reads it again:
2454/// the same answer is wanted where the item is landed and where a repeat copy of a project
2455/// decides whether it settled, and a second read there is a second round trip for nothing.
2456fn changes(
2457    held: Option<&Prior>,
2458    item: &Planned,
2459    target: &NativeId,
2460    project: &Option<NativeId>,
2461    edges: &[DependencyEdge],
2462    delivers: &[TaskRef],
2463    destination: &SourceName,
2464) -> bool {
2465    let Some(held) = held else {
2466        return true;
2467    };
2468    let outgoing = outgoing(
2469        item,
2470        target.clone(),
2471        project.clone(),
2472        &recorded(item, Some(held)),
2473        delivers,
2474        Some(held),
2475    );
2476    !same(&held.item, &outgoing, destination) || !same_edges(&held.edges, edges)
2477}
2478
2479/// Remove one item this copy created, through the destination's own write interface.
2480async fn remove(
2481    destination: &ResolvedSource,
2482    kind: Level,
2483    id: &NativeId,
2484) -> Result<(), SourceError> {
2485    match kind {
2486        Level::Task => destination.source().delete_task(id).await,
2487        Level::Project => destination.source().delete_project(id).await,
2488        Level::Document => destination.source().delete_document(id).await,
2489    }
2490}
2491
2492/// Write one item back exactly as the destination held it before this copy.
2493async fn restore(
2494    destination: &ResolvedSource,
2495    id: &NativeId,
2496    prior: &Prior,
2497) -> Result<(), SourceError> {
2498    match &prior.item {
2499        Item::Task(task) => destination
2500            .source()
2501            .write_task(&ItemWrite {
2502                target: Some(id.clone()),
2503                item: (**task).clone(),
2504                depends_on: prior.edges.clone(),
2505            })
2506            .await
2507            .map(|_| ()),
2508        Item::Project(project) => destination
2509            .source()
2510            .write_project(&ItemWrite {
2511                target: Some(id.clone()),
2512                item: (**project).clone(),
2513                depends_on: prior.edges.clone(),
2514            })
2515            .await
2516            .map(|_| ()),
2517        Item::Document(document) => destination
2518            .source()
2519            .write_document(&ItemWrite {
2520                target: Some(id.clone()),
2521                item: (**document).clone(),
2522                depends_on: Vec::new(),
2523            })
2524            .await
2525            .map(|_| ()),
2526    }
2527}
2528
2529/// Refuse a document copy addressed to a source that declares it has none.
2530///
2531/// Read off the declaration rather than by asking, which is what "not asked" means: the
2532/// engine learned at the handshake that this source holds no documents, so it refuses
2533/// naming the source and its plugin instead of sending a read that would be refused there.
2534/// Applied at both ends of a copy — a source with no documents holds nothing to copy out,
2535/// and a destination with none has nowhere to put one.
2536fn documentary(source: &ResolvedSource) -> Result<(), EngineError> {
2537    if source.source().capabilities().documents.is_native() {
2538        return Ok(());
2539    }
2540    Err(EngineError::NoDocuments {
2541        name: source.name().to_string(),
2542        kind: source.kind().to_owned(),
2543    })
2544}
2545
2546/// One source failing while a copy was mid-flight.
2547fn refused(source: &ResolvedSource, error: SourceError) -> EngineError {
2548    EngineError::SourceRefused {
2549        name: source.name().to_string(),
2550        error,
2551    }
2552}
2553
2554/// Every forward edge at one item, walked to exhaustion one page at a time.
2555async fn forward_edges(
2556    source: &ResolvedSource,
2557    id: &NativeId,
2558    kind: Level,
2559) -> Result<Vec<DependencyEdge>, EngineError> {
2560    // A document has no edges to walk, and asking for them would mean asking a source for
2561    // a graph the contract says nothing may point into.
2562    if kind == Level::Document {
2563        return Ok(Vec::new());
2564    }
2565    let mut edges = Vec::new();
2566    let mut cursor: Option<Cursor> = None;
2567    loop {
2568        let asked = cursor.clone();
2569        let request = request_for(source, cursor);
2570        let page = match kind {
2571            Level::Task | Level::Document => {
2572                source
2573                    .source()
2574                    .task_dependencies(id, Direction::DependsOn, &request)
2575                    .await
2576            }
2577            Level::Project => {
2578                source
2579                    .source()
2580                    .project_dependencies(id, Direction::DependsOn, &request)
2581                    .await
2582            }
2583        }
2584        .map_err(|error| refused(source, error))?;
2585        fits(page.items.len(), request.limit).map_err(|error| refused(source, error))?;
2586        edges.extend(page.items);
2587        unrepeated(
2588            page.next.as_ref(),
2589            asked.as_ref(),
2590            "an item's dependencies were being read for a copy",
2591        )
2592        .map_err(|error| refused(source, error))?;
2593        match page.next {
2594            Some(next) => cursor = Some(next),
2595            None => return Ok(edges),
2596        }
2597    }
2598}
2599
2600/// The location string one record reports, when it reports a usable one.
2601///
2602/// Either variant's own `String`, and `None` for a record the source gave no location for
2603/// or gave an empty string for: there is nothing to look for in a document's content and
2604/// nothing to point a reader at.
2605fn located(location: Option<&Location>) -> Option<String> {
2606    let (Location::Path(held) | Location::Url(held)) = location?;
2607    (!held.is_empty()).then(|| held.clone())
2608}
2609
2610/// Record one candidate referent, when its source said where it is.
2611fn note(
2612    into: &mut Vec<Referent>,
2613    id: GlobalId,
2614    level: Level,
2615    location: Option<&Location>,
2616    metadata: &BTreeMap<String, Value>,
2617) {
2618    if let Some(location) = located(location) {
2619        into.push(Referent {
2620            id,
2621            origin: origin_of(metadata),
2622            level,
2623            location,
2624        });
2625    }
2626}
2627
2628/// What every location string this document names becomes, longest first.
2629///
2630/// Longest first because a shorter location may start where a longer one does — a
2631/// project's directory and a task's file under it — and the longer of the two is the
2632/// record that occurrence names.
2633fn table_for(referents: &[Referent], counterparts: &Counterparts) -> Vec<(String, Resolution)> {
2634    let mut table: Vec<(String, Resolution)> = Vec::new();
2635    for referent in referents {
2636        // Two referents reporting one location string: an occurrence of it cannot be
2637        // attributed to either, and a rewrite would be *confidently wrong* rather than
2638        // merely unhelpful. So neither is chosen and both occurrences are counted.
2639        if let Some(held) = table
2640            .iter_mut()
2641            .find(|(location, _)| location == &referent.location)
2642        {
2643            held.1 = Resolution::Ambiguous;
2644            continue;
2645        }
2646        table.push((referent.location.clone(), counterparts.resolve(referent)));
2647    }
2648    table.sort_by_key(|(location, _)| std::cmp::Reverse(location.len()));
2649    table
2650}
2651
2652/// Whether `content` holds `location` at least once, stopped on both sides.
2653fn holds(content: &str, location: &str) -> bool {
2654    (0..content.len()).any(|at| delimited_at(content, at, location))
2655}
2656
2657/// Whether `location` occurs at `at` **stopped on both sides** — by
2658/// [`stops_a_location`], or by the end of the content — rather than as part of a longer
2659/// location-like string.
2660///
2661/// A location string occurring inside a longer one is a different string naming a
2662/// different record: `/…/tasks/p/t.md` must not be rewritten inside `/…/tasks/p/t.md.bak`,
2663/// `https://example.invalid/1` must not be rewritten inside `https://example.invalid/12`,
2664/// and a project's location that is a directory prefix of a task's must not be rewritten
2665/// inside that task's. What deciding it this way costs is stated on [`stops_a_location`].
2666fn delimited_at(content: &str, at: usize, location: &str) -> bool {
2667    if !content.is_char_boundary(at) || !content[at..].starts_with(location) {
2668        return false;
2669    }
2670    let before = content[..at].chars().next_back();
2671    let after = content[at + location.len()..].chars().next();
2672    stops_a_location(before) && stops_a_location(after)
2673}
2674
2675/// Whether a character cannot continue a path or a link, so a location string beside one
2676/// ends there.
2677///
2678/// Stated as what *stops* a location rather than as what one may contain, because the
2679/// second list is unbounded — a path may hold very nearly any byte, and a URL more. Every
2680/// character not named here continues, which is what leaves the three cases above alone;
2681/// the end of the content counts as a stop. The set is what the artifact this exists for
2682/// really wraps a bare path in — a backtick in a table cell — plus the delimiters prose
2683/// and Markdown put next to one.
2684///
2685/// **Sentence punctuation is deliberately absent, and that is a stated cost rather than an
2686/// oversight.** `.`, `!`, `?` and `:` each equally *continue* a real location — `/…/t.md`
2687/// and `/…/t.md.bak` are two files, `…/1` and `…/1?q=2` two pages — so admitting them as
2688/// stops would rewrite one record's location into another's. The price is that a location
2689/// written bare at the end of a sentence is not recognised at all: its text is left
2690/// byte-for-byte and it is counted in neither figure, exactly as a reference to another
2691/// project's record is. That is the direction to be wrong in, because this edits the
2692/// content of somebody's document, where a confidently wrong rewrite is worse than one
2693/// that never happens.
2694fn stops_a_location(character: Option<char>) -> bool {
2695    match character {
2696        None => true,
2697        Some(character) => character.is_whitespace() || "`\"'()[]{}<>|,;".contains(character),
2698    }
2699}
2700
2701/// One document's content with every whole reference rewritten, and what that took.
2702///
2703/// A location string with no confident counterpart is left **byte-for-byte** as it was
2704/// rather than removed or guessed at, and so is every character of the content that is not
2705/// a rewritten reference. Nothing is added and nothing is reformatted.
2706fn substitute(content: &str, table: &[(String, Resolution)]) -> (String, Counted) {
2707    let mut written = String::with_capacity(content.len());
2708    let mut counts = Counted::default();
2709    let mut at = 0;
2710    while at < content.len() {
2711        if let Some((location, resolution)) = table
2712            .iter()
2713            .find(|(location, _)| delimited_at(content, at, location))
2714        {
2715            match resolution {
2716                Resolution::Rewrite(there) => {
2717                    written.push_str(there);
2718                    counts.rewritten += 1;
2719                }
2720                // Both left-alone outcomes count as unresolved on the branch that counts
2721                // them, which is what really holds `ambiguous` at or below `unresolved`.
2722                Resolution::NoCounterpart => {
2723                    written.push_str(location);
2724                    counts.unresolved += 1;
2725                }
2726                Resolution::Ambiguous => {
2727                    written.push_str(location);
2728                    counts.unresolved += 1;
2729                    counts.ambiguous += 1;
2730                }
2731            }
2732            at += location.len();
2733            continue;
2734        }
2735        let character = content[at..]
2736            .chars()
2737            .next()
2738            .expect("a character at a boundary this walk only ever lands on");
2739        written.push(character);
2740        at += character.len_utf8();
2741    }
2742    (written, counts)
2743}
2744
2745/// The origin one item records, when it records a usable one.
2746fn origin_of(metadata: &BTreeMap<String, Value>) -> Option<GlobalId> {
2747    metadata
2748        .get(GlobalId::ORIGIN_KEY)?
2749        .as_str()?
2750        .parse::<GlobalId>()
2751        .ok()
2752}
2753
2754/// The title and metadata of either kind of item.
2755fn described(item: &Item) -> (&str, &BTreeMap<String, Value>) {
2756    match item {
2757        Item::Task(task) => (&task.title, &task.metadata),
2758        Item::Project(project) => (&project.title, &project.metadata),
2759        Item::Document(document) => (&document.title, &document.metadata),
2760    }
2761}
2762
2763/// The item as the destination should hold it.
2764///
2765/// `url`, `location`, `created_at` and `updated_at` are the destination's own and are
2766/// never written — where the *source* holds an item says nothing about where the
2767/// destination does, which is why a copied document does not arrive claiming the path or
2768/// the link its source reported. The two reserved keys this product encodes typed fields
2769/// under are removed, because those fields travel as themselves — leaving the encoding
2770/// beside them would have the destination hold one thing twice, and disagree with itself
2771/// the moment one changed.
2772///
2773/// A task's `delivers` is `delivers`, already resolved against the destination, and its
2774/// `delivered_by` is the one the destination holds — never the source's, and empty for an
2775/// item this copy creates: that list is the store's to keep, at the destination.
2776fn outgoing(
2777    item: &Planned,
2778    id: NativeId,
2779    project: Option<NativeId>,
2780    origin: &Origin,
2781    delivers: &[TaskRef],
2782    held: Option<&Prior>,
2783) -> Item {
2784    match &item.item {
2785        Item::Task(task) => Item::Task(Box::new(Task {
2786            id,
2787            url: None,
2788            location: None,
2789            created_at: None,
2790            updated_at: None,
2791            project,
2792            metadata: carried(&task.metadata, origin),
2793            delivers: delivers.to_vec(),
2794            delivered_by: match held.map(|held| &held.item) {
2795                Some(Item::Task(held)) => held.delivered_by.clone(),
2796                _ => Vec::new(),
2797            },
2798            ..(**task).clone()
2799        })),
2800        Item::Project(project) => Item::Project(Box::new(Project {
2801            id,
2802            url: None,
2803            location: None,
2804            created_at: None,
2805            updated_at: None,
2806            metadata: carried(&project.metadata, origin),
2807            ..(**project).clone()
2808        })),
2809        Item::Document(document) => Item::Document(Box::new(Document {
2810            id,
2811            url: None,
2812            location: None,
2813            created_at: None,
2814            updated_at: None,
2815            project,
2816            metadata: carried(&document.metadata, origin),
2817            ..(**document).clone()
2818        })),
2819    }
2820}
2821
2822/// The metadata a copy carries: the caller's own keys untouched, and the origin settled.
2823///
2824/// The key is removed before it is settled rather than overwritten, because the item being
2825/// copied carries an origin of its own and [`Origin::Keeps`] must not let it through.
2826fn carried(metadata: &BTreeMap<String, Value>, origin: &Origin) -> BTreeMap<String, Value> {
2827    let mut carried = metadata.clone();
2828    carried.remove(Repository::METADATA_KEY);
2829    carried.remove(DependencyEdge::RECORDED_KEY);
2830    carried.remove(TaskRef::DELIVERS_KEY);
2831    carried.remove(TaskRef::DELIVERED_BY_KEY);
2832    carried.remove(GlobalId::ORIGIN_KEY);
2833    let held = match origin {
2834        Origin::Records(id) => Some(Value::String(id.to_string())),
2835        Origin::Keeps(held) => held.clone(),
2836    };
2837    if let Some(held) = held {
2838        carried.insert(GlobalId::ORIGIN_KEY.to_owned(), held);
2839    }
2840    carried
2841}
2842
2843/// What one landed item records at [`GlobalId::ORIGIN_KEY`].
2844enum Origin {
2845    /// The qualified id this item was copied from, as the id type rather than as its
2846    /// spelling: the key holds a [`GlobalId`] and nothing else may be recorded there.
2847    Records(GlobalId),
2848    /// Whatever the destination already holds there — `None` when it holds nothing, which
2849    /// is written as the key being absent rather than as a null.
2850    ///
2851    /// A [`Value`] and not a [`GlobalId`], because this variant does not interpret what it
2852    /// carries: it is the destination's own metadata entry, held for the length of one
2853    /// write and put back exactly as it was read. Parsing it would turn a value a
2854    /// destination holds and this engine cannot read into a value this engine deletes,
2855    /// which is the opposite of what keeping it means.
2856    Keeps(Option<Value>),
2857}
2858
2859/// Which of the two a copy of this item does.
2860///
2861/// A copy that reached its target by rule 1 is a copy-back: the item being copied names
2862/// the destination item, so the destination is the *original* and the id being copied
2863/// belongs to the copy that came out of it. Recording that id there would overwrite the
2864/// original's own provenance — and with it the correspondence every later copy from the
2865/// source it was authored in depends on. That copy would then match nothing and create a
2866/// second item beside the one it meant to update, which is the whole failure: nothing is
2867/// reported, and whoever reads that board now has two. So a copy-back leaves the
2868/// destination's origin exactly as the destination holds it, absent included, and every
2869/// other copy records the id it was copied from.
2870fn recorded(item: &Planned, held: Option<&Prior>) -> Origin {
2871    if let Target::Update {
2872        found: Found::Origin,
2873        ..
2874    } = &item.target
2875    {
2876        return Origin::Keeps(
2877            held.and_then(|held| described(&held.item).1.get(GlobalId::ORIGIN_KEY).cloned()),
2878        );
2879    }
2880    Origin::Records(item.source.clone())
2881}
2882
2883/// Whether the destination already reads exactly as this copy would leave it.
2884///
2885/// The destination's own `url` and timestamps are excluded because a copy never writes
2886/// them, so a difference there is not one this copy would close.
2887fn same(held: &Item, outgoing: &Item, destination: &SourceName) -> bool {
2888    match (held, outgoing) {
2889        (Item::Task(held), Item::Task(outgoing)) => {
2890            // Qualified before they are compared, so `T-1` and `folder:T-1` at the destination
2891            // `folder` are the one entry they are.
2892            targets(&held.delivers, destination) == targets(&outgoing.delivers, destination)
2893                && targets(&held.delivered_by, destination)
2894                    == targets(&outgoing.delivered_by, destination)
2895                && held.title == outgoing.title
2896                && held.content == outgoing.content
2897                && held.status == outgoing.status
2898                && held.labels == outgoing.labels
2899                && held.project == outgoing.project
2900                && held.metadata == outgoing.metadata
2901                && held.repositories == outgoing.repositories
2902        }
2903        (Item::Project(held), Item::Project(outgoing)) => {
2904            held.title == outgoing.title
2905                && held.content == outgoing.content
2906                && held.status == outgoing.status
2907                && held.labels == outgoing.labels
2908                && held.metadata == outgoing.metadata
2909                && held.repositories == outgoing.repositories
2910        }
2911        // No status, because a document has none; no edges, because it is in no graph.
2912        (Item::Document(held), Item::Document(outgoing)) => {
2913            held.title == outgoing.title
2914                && held.content == outgoing.content
2915                && held.labels == outgoing.labels
2916                && held.project == outgoing.project
2917                && held.metadata == outgoing.metadata
2918                && held.repositories == outgoing.repositories
2919        }
2920        _ => false,
2921    }
2922}
2923
2924/// Whether the destination's forward edges already say what this copy would write.
2925fn same_edges(held: &[DependencyEdge], outgoing: &[DependencyEdge]) -> bool {
2926    let ends = |edges: &[DependencyEdge]| {
2927        let mut ends: Vec<(String, ItemKind, DependencyKind)> = edges
2928            .iter()
2929            .map(|edge| (edge.to.id().to_owned(), edge.to.kind, edge.kind))
2930            .collect();
2931        ends.sort_by(|left, right| left.0.cmp(&right.0));
2932        ends
2933    };
2934    ends(held) == ends(outgoing)
2935}
2936
2937/// Each read edge as the destination should record it, or `None` when its far end is a
2938/// member of this copy whose destination id is not known yet.
2939fn mapped_edges(
2940    edges: &[DependencyEdge],
2941    origin: &SourceName,
2942    destination: &ResolvedSource,
2943    copied: &[GlobalId],
2944    written: &BTreeMap<String, NativeId>,
2945) -> Vec<Option<DependencyEdge>> {
2946    edges
2947        .iter()
2948        .map(|edge| {
2949            let far = GlobalId::new(origin.clone(), NativeId(edge.to.id().to_owned()));
2950            let id = if let Some(native) = names(&edge.to, destination.name()) {
2951                // A far end already qualified to the destination's own source is that
2952                // source's own item, so it is written the way that source names its own:
2953                // unqualified. Leaving it qualified would have the destination hold an
2954                // edge into itself written as if it left, which is the one spelling the
2955                // reserved key exists to keep for edges that really do.
2956                Some(native)
2957            } else if edge.to.is_qualified() || origin == destination.name() {
2958                // Already naming a source of its own, or a copy inside one source where
2959                // the far end's own id is the destination's id.
2960                Some(edge.to.id().to_owned())
2961            } else if copied.contains(&far) {
2962                written.get(&far.to_string()).map(|native| native.0.clone())
2963            } else {
2964                Some(far.to_string())
2965            }?;
2966            DependencyEndpoint::new(id, edge.to.kind)
2967                .ok()
2968                .map(|to| DependencyEdge {
2969                    from: edge.from.clone(),
2970                    to,
2971                    kind: edge.kind,
2972                })
2973        })
2974        .collect()
2975}
2976
2977/// Each `delivers` entry of one planned task as the destination should hold it, or `None`
2978/// where it names a member of this copy whose destination id is not known yet. Empty for
2979/// anything but a task.
2980fn delivers_of(
2981    item: &Planned,
2982    destination: &ResolvedSource,
2983    copied: &[GlobalId],
2984    written: &BTreeMap<String, NativeId>,
2985) -> Vec<Option<TaskRef>> {
2986    match &item.item {
2987        Item::Task(task) => mapped_delivers(
2988            &task.delivers,
2989            &item.source.source,
2990            destination,
2991            copied,
2992            written,
2993        ),
2994        Item::Project(_) | Item::Document(_) => Vec::new(),
2995    }
2996}
2997
2998/// Each `delivers` entry as the destination should hold it, or `None` when it names a member
2999/// of this copy whose destination id is not known yet.
3000///
3001/// An entry naming a member of the copied set becomes that member's own id at the
3002/// destination — bare, because the member is the destination's, unless the id holds a colon
3003/// a bare entry would be misread by. Every other entry is carried through qualified, so a
3004/// bare one read at `origin` goes on naming a task of `origin`.
3005fn mapped_delivers(
3006    entries: &[TaskRef],
3007    origin: &SourceName,
3008    destination: &ResolvedSource,
3009    copied: &[GlobalId],
3010    written: &BTreeMap<String, NativeId>,
3011) -> Vec<Option<TaskRef>> {
3012    entries
3013        .iter()
3014        .map(|entry| {
3015            let qualified = entry.in_source(origin);
3016            let Ok(far) = qualified.as_str().parse::<GlobalId>() else {
3017                return Some(qualified);
3018            };
3019            if !copied.contains(&far) {
3020                return Some(qualified);
3021            }
3022            let native = written.get(&far.to_string())?;
3023            Some(if native.as_str().contains(':') {
3024                TaskRef::qualified(destination.name(), native)
3025            } else {
3026                TaskRef::new(native.as_str())
3027                    .unwrap_or_else(|_| TaskRef::qualified(destination.name(), native))
3028            })
3029        })
3030        .collect()
3031}
3032
3033/// How many of one task's `delivers` entries name a member of the copied set.
3034fn members_named(entries: &[TaskRef], origin: &SourceName, copied: &[GlobalId]) -> u64 {
3035    let named = entries
3036        .iter()
3037        .filter(|entry| {
3038            entry
3039                .in_source(origin)
3040                .as_str()
3041                .parse::<GlobalId>()
3042                .is_ok_and(|far| copied.contains(&far))
3043        })
3044        .count();
3045    u64::try_from(named).unwrap_or(u64::MAX)
3046}
3047
3048/// The entries that could be resolved, which is every one of them on the second pass.
3049fn resolved_entries(entries: &[Option<TaskRef>]) -> Vec<TaskRef> {
3050    entries.iter().flatten().cloned().collect()
3051}
3052
3053/// The native id a qualified endpoint names at `destination`, when it names one there.
3054fn names(endpoint: &DependencyEndpoint, destination: &SourceName) -> Option<String> {
3055    if !endpoint.is_qualified() {
3056        return None;
3057    }
3058    let id: GlobalId = endpoint.id().parse().ok()?;
3059    (&id.source == destination).then_some(id.native.0)
3060}
3061
3062/// The edges that could be resolved, which is every one of them on the second pass.
3063fn resolved(edges: &[Option<DependencyEdge>]) -> Vec<DependencyEdge> {
3064    edges.iter().flatten().cloned().collect()
3065}
3066
3067/// Refuse an item of a member copy whose edge names a member that copy does not carry
3068/// and whose destination id nothing records.
3069///
3070/// `unrecorded` is empty for every other copy, which is what makes this a no-op there. An
3071/// edge already naming a source of its own, and every edge of a copy inside one source,
3072/// is written as it was read by [`mapped_edges`], so neither can need a recorded origin.
3073fn unrecorded_far_end(
3074    item: &Planned,
3075    destination: &ResolvedSource,
3076    unrecorded: &[GlobalId],
3077) -> Result<(), EngineError> {
3078    if &item.source.source == destination.name() {
3079        return Ok(());
3080    }
3081    for edge in &item.edges {
3082        if edge.to.is_qualified() {
3083            continue;
3084        }
3085        let far = GlobalId::new(
3086            item.source.source.clone(),
3087            NativeId(edge.to.id().to_owned()),
3088        );
3089        if unrecorded.contains(&far) {
3090            return Err(EngineError::UnrecordedMember {
3091                item: item.source.clone(),
3092                member: far,
3093                destination: destination.name().clone(),
3094            });
3095        }
3096    }
3097    Ok(())
3098}