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