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 items 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//! item it was copied from carries [`MetadataKey::COPIES_KEY`], an object naming its
7//! counterpart at each destination it was copied into. The rules below read exactly those —
8//! so nothing here is written down outside the plugin that owns the item, and the invariant
9//! this engine is built around is untouched.
10//!
11//! 0. **Follow the link.** An item whose `onetaskgraph.copies` names an item at the
12//!    destination has that item read by id, and when that item's own origin names this one
13//!    back it is the target: one read, and no scan. A link naming nothing there any more is
14//!    refused as `stale-link` unless `--recreate` is given, for the reason a stale origin
15//!    is; a link naming an item that no longer names this one back — somebody re-pointed it
16//!    — is ignored, and the rules below decide.
17//! 1. **Follow the origin.** An item already carrying an origin whose source half is the
18//!    destination names the destination item *directly*, and the copy updates it. This is
19//!    the half that makes an edit's copy-back an update: the local file came from the
20//!    remote item and knows which one.
21//! 2. **Search by origin.** Otherwise the destination is scanned, one page at a time, for
22//!    an item whose origin is the id being copied. Found, the copy updates it; not found,
23//!    the copy creates one carrying that origin.
24//!
25//! Which rule found the item decides what the copy records there. A copy that got its
26//! target from rule 1 is a copy-back — the destination is the *original*, and the item
27//! being copied is the one that came from it — so the destination keeps the origin it
28//! already holds, holding none included. Every other copy records the id it was copied
29//! from. See [`recorded`] for what stamping a copy-back's own id there costs.
30//!
31//! The same distinction decides the link. Once the whole copy has landed, every item whose
32//! target was found by any rule but rule 1 — or was created — has its link for this
33//! destination recorded or refreshed through its source's narrow metadata write, and that
34//! write joins the undo journal like every destination write. Rule 1 records nothing: that
35//! correspondence is already written down, on the destination item.
36//!
37//! A destination write is at the user's explicit request, names its destination, goes
38//! through that source's own write interface into that source's own store, and is never
39//! read back to answer a query. That is what makes it a write and not a cache.
40
41use std::collections::{BTreeMap, BTreeSet};
42
43use onetaskgraph_plugin_api::{
44    Capabilities, Cursor, DependencyEdge, DependencyEndpoint, DependencyKind, Direction, Document,
45    DocumentQuery, ItemKind, ItemWrite, Location, MetadataKey, MetadataMatch, Metering, NativeId,
46    Page, PageRequest, Project, ProjectQuery, Repository, SourceError, SourceName, StatusCategory,
47    Task, TaskQuery, TaskRef, TextFields, TextQuery,
48};
49use schemars::JsonSchema;
50use serde::{Deserialize, Serialize};
51use serde_json::Value;
52
53use crate::GlobalId;
54use crate::config::Placement;
55use crate::resolve::ResolvedSource;
56use crate::template::{Sha256Digest, TemplateProvenance, body_digest};
57
58use super::delivery::{Delivered, targets};
59use super::fetch::{fits, unrepeated};
60use super::local::ProjectSelector;
61use super::narrow::holds_priority;
62use super::{
63    DocumentFilters, DocumentRequest, Engine, EngineError, Filters, LeftBehind, Paging, Qualified,
64    TaskRequest,
65};
66
67/// A request to copy work into one configured destination.
68#[derive(Debug, Clone)]
69pub struct CopyRequest {
70    /// The qualified items to copy, in the order they were named.
71    pub items: CopyItems,
72    /// What those ids name, and what comes with them.
73    pub scope: CopyScope,
74    /// The configured source to copy into — a source name, never a qualified id.
75    pub destination: SourceName,
76    /// How to re-establish a correspondence the two origin rules cannot find.
77    pub match_by: Option<MatchBy>,
78    /// Whether an origin naming nothing at the destination falls through to the search
79    /// rule instead of refusing.
80    pub recreate: bool,
81    /// Whether the caller asserts the destination holds no counterpart of any item named,
82    /// so each is created there without the correspondence lookup — no origin search of the
83    /// destination at all.
84    ///
85    /// Sound for a caller that has just asked the destination itself: the origin query is
86    /// an index that lags a write, so repeating it here would be behind by exactly as much as
87    /// the caller's own was and could find nothing the caller's did not. Refused beside
88    /// [`match_by`](Self::match_by) and [`recreate`](Self::recreate), which are ways of
89    /// looking, and for an item that itself records a counterpart at the destination — a
90    /// link or an origin naming it — which is a carrier the caller's assertion overlooked.
91    // llmlint: ignore-block[invalid_states_unrepresentable] One enum of the ways a copy finds its target would make `create` beside `match_by` or `recreate` unrepresentable, but only by replacing `match_by` and `recreate`, which every Rust caller of this request sets by name and the command line maps flag for flag — rewriting what each existing caller writes. `create` is one more field beside them, as `recreate` and `dry_run` were, and the one combination it makes possible that means nothing is refused by `Engine::copy`, as `EngineError::CreateWith`, before anything is read.
92    pub create: bool,
93    // llmlint: ignore-end[invalid_states_unrepresentable]
94    /// Whether to perform every read and no write.
95    pub dry_run: bool,
96}
97
98/// A way of looking for a counterpart that [`CopyRequest::create`] says not to take, named as
99/// the command line spells it.
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum CopyLookup {
102    /// [`CopyRequest::match_by`], `--match-by`.
103    MatchBy,
104    /// [`CopyRequest::recreate`], `--recreate`.
105    Recreate,
106}
107
108impl std::fmt::Display for CopyLookup {
109    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        formatter.write_str(match self {
111            Self::MatchBy => "--match-by",
112            Self::Recreate => "--recreate",
113        })
114    }
115}
116
117/// The items one copy names: at least one, because a copy naming none is not a copy.
118///
119/// A newtype rather than a bare `Vec`, for the reason [`Repository`] is one: the empty
120/// list is not a copy of nothing, it is a caller mistake, and a type that can hold it
121/// leaves every reader to decide what it means — a report with no entries, an error, a
122/// silent success. None of those is better than not being able to say it.
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub struct CopyItems(Vec<GlobalId>);
125
126impl CopyItems {
127    /// The items a caller named, or `None` when they named none.
128    #[must_use]
129    pub fn new(items: Vec<GlobalId>) -> Option<Self> {
130        (!items.is_empty()).then_some(Self(items))
131    }
132
133    /// The items, in the order they were named.
134    #[must_use]
135    pub fn as_slice(&self) -> &[GlobalId] {
136        &self.0
137    }
138}
139
140/// What the ids a copy names are, and what travels with them.
141///
142/// One value rather than a kind beside a flag, because three of the four combinations
143/// those two would make are real and the fourth — tasks, with the tasks of each also
144/// copied — means nothing. The member list is a variant for the same reason: it narrows a
145/// project copy that carries its tasks, and it has nothing to say to the other three.
146#[derive(Debug, Clone, PartialEq, Eq)]
147pub enum CopyScope {
148    /// The ids name tasks, and only those tasks are copied.
149    Tasks,
150    /// The ids name projects.
151    Projects {
152        /// Whether the tasks in each project are copied too.
153        tasks: bool,
154    },
155    /// The ids name projects, and of the tasks in them exactly these are copied.
156    ///
157    /// A member the list does not name is not read at the destination, not compared, not
158    /// written and not reported, and no walk for what the copy left behind runs. A named
159    /// member with no counterpart there is still created: the list narrows which members
160    /// are read and written, never which outcomes are possible.
161    ///
162    /// An edge from a copied item to a member the list does not name resolves to the
163    /// destination id that member's own [`GlobalId::ORIGIN_KEY`] records at the source,
164    /// without reading the destination for it — or, for a copy into a source with `routes`,
165    /// the id its own `onetaskgraph.copies` link records in any source the copy reaches.
166    /// When that member records neither, the copy is refused before anything is written.
167    Members(CopyItems),
168    /// The ids name documents, and only those documents are copied.
169    ///
170    /// Nothing travels with a document: it takes part in no dependency graph, and it holds
171    /// nothing of its own the way a project holds tasks.
172    Documents,
173}
174
175/// The caller-named escape for a correspondence neither origin rule can find.
176///
177/// A person editing Markdown who deletes or corrupts the origin key leaves an item rule 1
178/// cannot use and rule 2 cannot find, and the next copy would create a second item. This
179/// is how that is re-established without hand-editing ids.
180#[derive(Debug, Clone, PartialEq, Eq)]
181pub enum MatchBy {
182    /// Match the item whose title is the same.
183    Title,
184    /// Match the item whose value at this metadata key is the same.
185    Metadata(String),
186}
187
188impl MatchBy {
189    /// The spelling a caller types, `title` or any metadata key.
190    #[must_use]
191    pub fn parse(key: &str) -> Self {
192        if key == "title" {
193            Self::Title
194        } else {
195            Self::Metadata(key.to_owned())
196        }
197    }
198}
199
200/// What a copy did, one entry per item.
201///
202/// The same per-item outcomes reach every consumer: the machine-readable output renders
203/// this, the rendered output renders this, and a Rust caller is handed it.
204#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
205pub struct CopyReport {
206    /// One entry per item the copy considered, in the order it considered them.
207    pub items: Vec<CopyOutcome>,
208    // Three flat scalars rather than one nested object, and a `!skip_serializing_if` beside
209    // every skip: both are load-bearing for the generated SDKs rather than matters of
210    // taste, and AGENTS.md's note on what a copied document's references are pointed at is
211    // where that reasoning lives.
212    /// Reference occurrences the copy rewrote to the destination's own location for the
213    /// record they name.
214    ///
215    /// A silent bound is indistinguishable from a bug, so the copy says what it did to the
216    /// references the documents it carried hold. This and the two below are totals over the
217    /// whole invocation rather than figures per document, and all three default to zero, so
218    /// a consumer written against the output before they existed is unaffected.
219    ///
220    /// **What these figures do not claim.** The referent set is a document's own project,
221    /// so a reference to a record in a *different* project is never recognised at all and
222    /// cannot appear in [`Self::references_unresolved`] either. These are the references
223    /// the copy recognised; they are not a census of every reference a document holds.
224    /// Noticing an out-of-scope reference would need exactly the unbounded destination walk
225    /// this design refuses.
226    #[serde(default, skip_serializing_if = "nothing_to_report")]
227    #[schemars(!skip_serializing_if)]
228    pub references_rewritten: u64,
229    /// Reference occurrences the copy recognised and left byte-for-byte as they were,
230    /// because the correspondence could not be established.
231    #[serde(default, skip_serializing_if = "nothing_to_report")]
232    #[schemars(!skip_serializing_if)]
233    pub references_unresolved: u64,
234    /// How many of [`Self::references_unresolved`] were left alone because the
235    /// correspondence was **ambiguous** rather than merely absent. A sub-count, never
236    /// larger than it.
237    ///
238    /// Split out because the two mean different things to a reader. A reference with no
239    /// counterpart is ordinary and expected under the bound above — the design working. An
240    /// ambiguous one says the destination holds duplicate records for one work item, or the
241    /// source reports one location for two records, and re-running the copy will never
242    /// clear it.
243    #[serde(default, skip_serializing_if = "nothing_to_report")]
244    #[schemars(!skip_serializing_if)]
245    // llmlint: ignore[invalid_states_unrepresentable] JSON Schema cannot express an
246    // inequality between two numbers, so a private constructor here would hold this in one
247    // consumer of three while both SDKs' generated models went on admitting it. What holds
248    // it is `substitute`: `Resolution` has no variant that counts an occurrence ambiguous
249    // without counting it unresolved.
250    pub references_ambiguous: u64,
251    /// `delivers` entries the copy rewrote to the destination's own id for a member of the
252    /// copied set, over the whole invocation. Every other entry arrives qualified and is not
253    /// counted. Left out when zero, as the three figures above are.
254    #[serde(default, skip_serializing_if = "nothing_to_report")]
255    #[schemars(!skip_serializing_if)]
256    pub delivers_rewritten: u64,
257    /// One entry per delivered task the copy kept in step with a task it landed, after the
258    /// whole copy was complete — see `task status set`, which reports the same entries. Left
259    /// out when there were none.
260    ///
261    /// A failed entry does not undo the copy: the tasks it landed stay landed, and the
262    /// command exits `4`.
263    #[serde(default, skip_serializing_if = "Vec::is_empty")]
264    #[schemars(!skip_serializing_if)]
265    pub delivered: Vec<Delivered>,
266    /// What this copy spent, summed over the sources in the command that meter their own
267    /// requests — and absent, never zero, when none of them does.
268    ///
269    /// Omitted from the wire when absent, like the three figures above, so a consumer
270    /// written against the output before it existed reads the same document.
271    #[serde(default, skip_serializing_if = "Option::is_none")]
272    pub spent: Option<Spent>,
273}
274
275/// Whether one of [`CopyReport`]'s reference figures has anything to say.
276///
277/// A copy that recognised no reference reports that by leaving the figure out rather than
278/// by writing a nought, so the machine output of a task or project copy is exactly what it
279/// was before these figures existed. The human rendering says it in words either way,
280/// because a reader there needs to be told the copy looked.
281fn nothing_to_report(figure: &u64) -> bool {
282    *figure == 0
283}
284
285/// What one command spent, summed over the sources in it that meter their own requests.
286///
287/// **Source-owned.** Every figure is what a source said it sent and spent while the command
288/// ran, read through [`TaskSource::metering`](onetaskgraph_plugin_api::TaskSource::metering)
289/// before the command and again after it. The engine adds the differences up by name and
290/// interprets none of them, which is why the budget and unit names are open vocabulary.
291#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
292pub struct Spent {
293    /// How many HTTP requests those sources sent for this command.
294    pub requests: u64,
295    /// What those requests spent, one entry per budget and unit, ordered by budget name.
296    pub budgets: Vec<BudgetSpent>,
297}
298
299/// What one command spent against one budget.
300#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
301pub struct BudgetSpent {
302    /// The budget, as the source names it — `graphql`, `rest`.
303    // 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.
304    pub budget: String,
305    /// What it is metered in — `points`, `requests`.
306    // llmlint: ignore[invalid_states_unrepresentable] As `budget` above.
307    pub unit: String,
308    /// How much was spent against it, in that unit.
309    pub amount: u64,
310    /// Whether any part of `amount` was modelled by a source rather than reported by its
311    /// backend or counted, which makes `amount` a lower bound on what the backend charged
312    /// rather than a measurement of it.
313    pub lower_bound: bool,
314}
315
316/// One document's reference figures, before they are folded into the invocation's.
317#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
318struct Counted {
319    /// Occurrences rewritten.
320    rewritten: u64,
321    /// Occurrences recognised and left alone.
322    unresolved: u64,
323    /// How many of those were ambiguous.
324    ambiguous: u64,
325}
326
327impl Counted {
328    /// Fold one document's figures into the invocation's.
329    fn add(&mut self, other: Self) {
330        self.rewritten += other.rewritten;
331        self.unresolved += other.unresolved;
332        self.ambiguous += other.ambiguous;
333    }
334}
335
336/// What happened to one item.
337///
338/// `action` and `destination` are one value rather than two fields side by side: an
339/// updated item without a destination id, or an orphan without one, are states this type
340/// must not be able to say — the id *is* what those outcomes are about. The one outcome
341/// that legitimately has none is a dry run that would create, because nothing was
342/// created and there is no id to report.
343#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
344pub struct CopyOutcome {
345    /// The qualified id the item was read from.
346    pub source: GlobalId,
347    /// What happened to it, and where.
348    #[serde(flatten)]
349    pub action: CopyAction,
350    /// Which source a routed copy placed it in, and the index of the route entry that
351    /// matched — `null` there when none did.
352    ///
353    /// Present for every item a copy into a source declaring `routes` landed, a dry run's
354    /// included, so a would-be create still says where it would go; absent for a copy into
355    /// a source with none, whose output is what it always was, and for an orphan, which was
356    /// not placed at all.
357    #[serde(default, skip_serializing_if = "Option::is_none")]
358    pub placed: Option<Placement>,
359}
360
361/// Which rule found the destination counterpart an item was updated at, or found already
362/// reading as it does.
363///
364/// Tried in this order, and the first that answers is the one reported: the link the item
365/// records, its own origin, a search of the destination for an item recording it as its
366/// origin, and the caller's `--match-by`. When none answers, the item is created and says
367/// so as [`NoCounterpart`] instead.
368#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
369#[serde(rename_all = "kebab-case")]
370pub enum CopyVia {
371    /// The item's `onetaskgraph.copies` entry for the destination named an item whose own
372    /// origin names this one back. Found by one read by id, and no scan.
373    Link,
374    /// The item's own `onetaskgraph.origin` named the destination item: a copy-back.
375    Origin,
376    /// A search of the destination for an item recording this one as its origin.
377    Scan,
378    /// A search of the destination by the caller's `--match-by`.
379    Match,
380}
381
382/// What a created item reports as its `via`: no rule found a counterpart.
383///
384/// One word, and a type of its own rather than a fifth [`CopyVia`], so an item updated at a
385/// counterpart cannot say none was found and a created one cannot name a rule that found
386/// one.
387#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
388#[serde(rename_all = "kebab-case")]
389pub enum NoCounterpart {
390    /// No counterpart was found, so one was created — or, in a dry run, would have been.
391    #[default]
392    Created,
393}
394
395/// What a copy did to the `onetaskgraph.copies` entry the copied item holds for the
396/// destination.
397#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
398#[serde(rename_all = "kebab-case")]
399pub enum CopyLink {
400    /// The entry was written, because the item held none for this destination or held one
401    /// naming another item.
402    Recorded,
403    /// Nothing was written: the entry already named this destination item, or the item was
404    /// found by its own origin, where the correspondence is already recorded on the other
405    /// side.
406    Unchanged,
407    /// The item's source cannot hold the entry — it has no write side, or it refuses the
408    /// narrow metadata write — so the copy landed without recording it.
409    Unrecorded,
410}
411
412impl CopyOutcome {
413    /// The qualified id this outcome landed on, when it landed on one.
414    #[must_use]
415    pub fn destination(&self) -> Option<&GlobalId> {
416        self.action.destination()
417    }
418}
419
420/// The four things a copy can do to one item, and the id each of them is about.
421#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
422#[serde(tag = "action", rename_all = "kebab-case")]
423pub enum CopyAction {
424    // llmlint: ignore[names_match_behavior] `created` is Contract D's serialized action
425    // for both a completed create and a dry run that would create; the optional destination
426    // distinguishes those cases, and renaming this public variant would break Rust callers.
427    /// The destination held no counterpart, so one was created.
428    Created {
429        /// The id it was created under, or `null` for a dry run that would have created
430        /// one — there is no id, because nothing was.
431        destination: Option<GlobalId>,
432        /// That no rule found a counterpart. The one word it can be, so a report written
433        /// before there was a `via` reads as saying it.
434        #[serde(default)]
435        via: NoCounterpart,
436        /// What the copy did to the link the copied item records for this destination at
437        /// `onetaskgraph.copies`; absent for a dry run, which writes nothing.
438        #[serde(default, skip_serializing_if = "Option::is_none")]
439        // llmlint: ignore[invalid_states_unrepresentable] Whether a copy is a dry run is a
440        // property of the request, which the report does not carry, and the contract this
441        // lands — shared with the follow-up tooling built against it — says a dry run leaves
442        // `link` out rather than naming a sixth word for it. What holds it is `Engine::link`:
443        // the one writer of this field, run for every item a copy that writes has landed and
444        // for none of a dry run's, which `a_dry_run_says_which_rule_would_answer_and_records_no_link`
445        // and every `link` assertion in tests/copy_link.rs drive.
446        link: Option<CopyLink>,
447    },
448    /// The destination held a counterpart and it now reads as the source does.
449    Updated {
450        /// The item that was updated.
451        destination: GlobalId,
452        /// Which rule found it.
453        via: CopyVia,
454        /// What the copy did to the copied item's link; absent for a dry run.
455        #[serde(default, skip_serializing_if = "Option::is_none")]
456        // llmlint: ignore[invalid_states_unrepresentable] As `link` on `Created` above.
457        link: Option<CopyLink>,
458    },
459    /// The destination held a counterpart that already read that way; nothing was written.
460    Unchanged {
461        /// The item that already said it.
462        destination: GlobalId,
463        /// Which rule found it.
464        via: CopyVia,
465        /// What the copy did to the copied item's link; absent for a dry run.
466        #[serde(default, skip_serializing_if = "Option::is_none")]
467        // llmlint: ignore[invalid_states_unrepresentable] As `link` on `Created` above.
468        link: Option<CopyLink>,
469    },
470    /// The destination holds a counterpart the source no longer does. A copy never
471    /// deletes, so it was left exactly as it is — and no rule was asked where it goes, and
472    /// no link was touched, because it was not copied.
473    Orphaned {
474        /// The item that was left alone.
475        destination: GlobalId,
476    },
477}
478
479impl CopyAction {
480    /// The qualified id this action is about, when there is one.
481    #[must_use]
482    pub fn destination(&self) -> Option<&GlobalId> {
483        match self {
484            Self::Created { destination, .. } => destination.as_ref(),
485            Self::Updated { destination, .. }
486            | Self::Unchanged { destination, .. }
487            | Self::Orphaned { destination } => Some(destination),
488        }
489    }
490
491    /// Which rule found the counterpart, or `None` when none did — the item was created —
492    /// and for an orphan, which no rule was asked about.
493    #[must_use]
494    pub fn found_by(&self) -> Option<CopyVia> {
495        match self {
496            Self::Updated { via, .. } | Self::Unchanged { via, .. } => Some(*via),
497            Self::Created { .. } | Self::Orphaned { .. } => None,
498        }
499    }
500
501    /// What became of the copied item's link, or `None` for a dry run and for an orphan.
502    #[must_use]
503    pub fn link(&self) -> Option<CopyLink> {
504        match self {
505            Self::Created { link, .. }
506            | Self::Updated { link, .. }
507            | Self::Unchanged { link, .. } => *link,
508            Self::Orphaned { .. } => None,
509        }
510    }
511
512    /// The link slot of an item this copy landed, `None` for an orphan.
513    fn link_slot(&mut self) -> Option<&mut Option<CopyLink>> {
514        match self {
515            Self::Created { link, .. }
516            | Self::Updated { link, .. }
517            | Self::Unchanged { link, .. } => Some(link),
518            Self::Orphaned { .. } => None,
519        }
520    }
521
522    /// The word this action serializes as, taken from its own `Serialize`.
523    ///
524    /// Read back off the wire form rather than written out again in a `match`, for the
525    /// reason `render::wire` gives: a second spelling of `unchanged` would be a second
526    /// place for it to drift from the one a caller reads.
527    #[must_use]
528    pub fn name(&self) -> String {
529        serde_json::to_value(self).expect("a contract enum serialises")["action"]
530            .as_str()
531            .expect("an internally tagged enum carries its tag")
532            .to_owned()
533    }
534}
535
536/// What one item points at, resolved as far as the copy has got: its forward edges and the
537/// tasks it delivers.
538struct Pointing<'a> {
539    /// Its forward edges, `None` where the far end is a member not landed yet.
540    edges: &'a [Option<DependencyEdge>],
541    /// The tasks it delivers that can already be named at the destination.
542    delivers: &'a [TaskRef],
543}
544
545/// Where one item is going at the destination.
546enum Target {
547    /// Update the destination item with this id, reached by the rule named.
548    Update {
549        /// The destination item this copy updates.
550        id: NativeId,
551        /// Which rule found it.
552        found: Found,
553    },
554    /// Create one.
555    Create,
556}
557
558/// Which of the rules above found the destination item a copy is updating.
559///
560/// Every one is the same instruction — update that item — and rule 1 is a different answer
561/// about the origin, which is why the distinction is carried this far rather than dropped
562/// where it is made. See [`recorded`]. It is also what the report's [`CopyVia`] says and
563/// what decides whether the copied item's link is written.
564#[derive(Clone, Copy, PartialEq, Eq)]
565enum Found {
566    /// The item's own link named it, and it names the item back.
567    Link,
568    /// Rule 1: the item being copied already named it, so this copy is a copy-back.
569    Origin,
570    /// Rule 2: the destination was searched for an item recording this one as its origin.
571    Scan,
572    /// The caller's matching escape.
573    Match,
574}
575
576impl Found {
577    /// The word the report says it in.
578    fn via(self) -> CopyVia {
579        match self {
580            Self::Link => CopyVia::Link,
581            Self::Origin => CopyVia::Origin,
582            Self::Scan => CopyVia::Scan,
583            Self::Match => CopyVia::Match,
584        }
585    }
586}
587
588impl Target {
589    /// Which rule found this target, or `None` for one this copy creates.
590    fn found(&self) -> Option<Found> {
591        match self {
592            Self::Update { found, .. } => Some(*found),
593            Self::Create => None,
594        }
595    }
596}
597
598/// What a scan of the destination is looking for.
599enum Wanted {
600    /// An item recording this qualified id as its origin.
601    Origin(String),
602    /// An item whose title is this.
603    Title(String),
604    /// An item holding this value at this metadata key.
605    Metadata(String, Value),
606}
607
608impl Wanted {
609    /// Whether one destination item is the one being looked for.
610    fn found(&self, title: &str, metadata: &BTreeMap<String, Value>) -> bool {
611        match self {
612            Self::Origin(id) => {
613                metadata.get(GlobalId::ORIGIN_KEY) == Some(&Value::String(id.clone()))
614            }
615            Self::Title(wanted) => title == wanted,
616            Self::Metadata(key, value) => metadata.get(key) == Some(value),
617        }
618    }
619
620    /// The task query that asks `capabilities`' source for this item rather than for all of
621    /// them, when that source applies the predicate itself.
622    ///
623    /// Only ever a narrowing the source declared it applies, so a source that cannot is
624    /// asked for everything exactly as before; and never the last word, because
625    /// [`found`](Self::found) still confirms each row. A text search is wider than an equal
626    /// title, and a source may answer a match it cannot yet see as absent — which is the
627    /// same answer the whole-store walk gave for an item its own listing was behind on.
628    ///
629    /// A title or a value with no letter or digit is never pushed down: a source whose search
630    /// indexes words cannot answer it and may refuse it, and the walk it replaces answered it
631    /// already, so it is asked for everything as it always was.
632    fn task_query(&self, capabilities: &Capabilities) -> TaskQuery {
633        match self {
634            Self::Origin(id) if capabilities.filter_by_origin.is_native() => TaskQuery {
635                origin: Some(id.clone()),
636                ..TaskQuery::default()
637            },
638            Self::Title(title) if capabilities.search_title.is_native() && has_words(title) => {
639                TaskQuery {
640                    text: Some(TextQuery {
641                        terms: title.clone(),
642                        fields: TextFields::Title,
643                    }),
644                    ..TaskQuery::default()
645                }
646            }
647            Self::Metadata(key, Value::String(value))
648                if capabilities.filter_by_metadata.is_native() && has_words(value) =>
649            {
650                MetadataMatch::new(key.clone(), Vec::new(), value.clone())
651                    .map(|wanted| TaskQuery {
652                        metadata: vec![wanted],
653                        ..TaskQuery::default()
654                    })
655                    .unwrap_or_default()
656            }
657            _ => TaskQuery::default(),
658        }
659    }
660}
661
662/// Whether `terms` holds a letter or a digit — a word a search that indexes words can find.
663fn has_words(terms: &str) -> bool {
664    terms.chars().any(char::is_alphanumeric)
665}
666
667/// What the destination held before this copy touched one item.
668///
669/// Read once, in [`Engine::land`], and used three times over: to decide whether the write
670/// would change anything, to repair the item's edges once the rest of the copy has landed,
671/// and — if the copy cannot finish — to put the item back exactly as it was.
672#[derive(Clone)]
673struct Prior {
674    /// The item as the destination held it.
675    item: Item,
676    /// Its forward edges there.
677    edges: Vec<DependencyEdge>,
678}
679
680/// One item that landed with an edge whose far end was not written yet.
681///
682/// Held until every item of the whole copy has landed, because the far end may be in
683/// another project of the same command: a copy of two projects at once is one copied set,
684/// not two, and an edge across them is remapped rather than written as a foreign id.
685struct Deferred {
686    /// The item, as it was read and resolved.
687    item: Planned,
688    /// The destination project it was filed under.
689    filed: Option<NativeId>,
690    /// Where it landed.
691    destination: NativeId,
692    /// What the destination held there before, when it held anything.
693    prior: Option<Prior>,
694}
695
696/// What one item's undo has to do to put the destination back.
697///
698/// Each entry names the source it was written to, because a routed copy writes into more
699/// than one, and it is undone in every one of them.
700enum Undo {
701    /// The copy created it, so undoing means removing it.
702    Created {
703        /// The source it was created in.
704        at: SourceName,
705        /// Which write interface removes it.
706        kind: Level,
707        /// The destination id it was created under.
708        id: NativeId,
709    },
710    /// The copy overwrote something, so undoing means writing that something back.
711    ///
712    /// No `kind` beside the id, unlike the variant above: what was there says which of the
713    /// two write interfaces takes it back, and a second spelling of that could disagree
714    /// with it.
715    Updated {
716        /// The source it was overwritten in.
717        at: SourceName,
718        /// The destination id that was overwritten.
719        id: NativeId,
720        /// What was there before.
721        prior: Prior,
722    },
723}
724
725impl Undo {
726    /// The source this entry was written to.
727    fn at(&self) -> &SourceName {
728        match self {
729            Self::Created { at, .. } | Self::Updated { at, .. } => at,
730        }
731    }
732
733    /// The destination id this entry is about.
734    fn id(&self) -> &NativeId {
735        match self {
736            Self::Created { id, .. } | Self::Updated { id, .. } => id,
737        }
738    }
739
740    /// Which of the destination's three write interfaces this entry belongs to.
741    ///
742    /// An id alone does not identify a destination item: nothing stops a destination
743    /// numbering its tasks and its projects in one namespace, and a local-Markdown store
744    /// filing `alpha.md` under both is the ordinary case rather than the contrived one.
745    /// So this pairs with `id` wherever one entry has to be told from another.
746    fn kind(&self) -> Level {
747        match self {
748            Self::Created { kind, .. } => *kind,
749            // Read off what was there, for the reason the variant carries no `kind` of
750            // its own: two spellings of one fact can disagree, and this one cannot.
751            Self::Updated { prior, .. } => prior.item.level(),
752        }
753    }
754}
755
756/// Everything one copy has written, in the order it wrote it, so a copy that cannot finish
757/// can undo its own writes.
758///
759/// This is not state the engine keeps: it lives for the length of one `copy` call and is
760/// dropped with it, so the invariant that nothing of a user's work is written down outside
761/// the plugin that owns it is untouched.
762#[derive(Default)]
763struct Journal {
764    /// One entry per destination item this copy first touched, in that order.
765    entries: Vec<Undo>,
766    /// One entry per copied item whose `onetaskgraph.copies` link this copy wrote, in that
767    /// order.
768    ///
769    /// Kept apart from `entries` because these are the *sources'* items rather than the
770    /// destination's, and an id alone does not say which store it is in: a copy inside one
771    /// source can write a link on an item and land on another item with the same kind.
772    links: Vec<Unlink>,
773}
774
775/// How to take back one link this copy wrote on a copied item.
776struct Unlink {
777    /// The item, at its own source.
778    item: GlobalId,
779    /// Which of that source's interfaces holds it.
780    level: Level,
781    /// What it held at `onetaskgraph.copies` before, or `None` when it held nothing there.
782    before: Option<Value>,
783}
784
785impl Journal {
786    /// Record what has to happen to put one destination item back.
787    ///
788    /// The *first* entry for an id is the one that matters and later ones are dropped: an
789    /// item written twice — once as it lands, once when its edges are repaired — was only
790    /// ever one thing before this copy started, and that is what undoing it restores.
791    fn record(&mut self, entry: Undo) {
792        if self.entries.iter().any(|held| {
793            held.at() == entry.at() && held.kind() == entry.kind() && held.id() == entry.id()
794        }) {
795            return;
796        }
797        self.entries.push(entry);
798    }
799}
800
801/// What one copy invocation carries from the item it lands to the next.
802///
803/// Like the [`Journal`] beside it, this lives for the length of one `copy` call and is
804/// dropped with it: nothing here is state the engine keeps, and nothing in it is read back
805/// to answer a later query.
806#[derive(Default)]
807struct Running {
808    /// Every item an edge of this command resolves to a destination id rather than writing as
809    /// it was read: the ids named, what travels with them, and — for a member copy — each
810    /// member the copy does not carry whose own recorded origin already names its
811    /// destination item. That last kind is resolvable without being copied.
812    resolvable: Vec<GlobalId>,
813    /// The destination item each of those corresponds to, as soon as it is known — whether
814    /// this command found it, landed it, or read it off a member's recorded origin.
815    ///
816    /// Keyed by the qualified id's own rendering, which is what a recorded origin holds
817    /// anyway — making `GlobalId` orderable for a local map would put an ordering on a
818    /// contract type for a reason no caller of it has.
819    ///
820    /// Qualified, because a routed copy lands items in more than one source, and whether an
821    /// edge to one is written bare or qualified depends on where both of its ends landed.
822    counterparts: BTreeMap<String, GlobalId>,
823    /// The items held back for [`Engine::repair`], across the whole request.
824    deferred: Vec<Deferred>,
825    /// The reference figures, one total for the whole invocation rather than one per
826    /// document.
827    references: Counted,
828    /// The destination project each source project corresponds to, once this command has
829    /// looked for it — so a second task filed under the same project does not walk the
830    /// destination for it again.
831    ///
832    /// Keyed by the source project and the source the item lands in, because a routed task
833    /// lands in its home's member project rather than in the home.
834    filings: BTreeMap<(String, SourceName), Option<NativeId>>,
835    /// Every home project this copy has learned the member projects of, by the home's
836    /// qualified id: which members it holds, and whether this copy added one.
837    homes: BTreeMap<String, Home>,
838    /// `delivers` entries naming a member of the copied set, over the whole invocation.
839    delivers_rewritten: u64,
840    /// Every task this copy landed, for the relation rule once the whole copy is complete.
841    landed: Vec<LandedTask>,
842    /// Every item this copy landed, in the order it landed them, for the link each records
843    /// once the whole copy has landed.
844    linking: Vec<Linking>,
845}
846
847/// The member project a routed write was filed under, and what finding it wrote.
848pub(super) struct Filed {
849    /// The member project, or `None` when the home is not there to have one.
850    pub(super) project: Option<NativeId>,
851    /// The member created and the home's list rewritten, if either was.
852    journal: Journal,
853}
854
855/// One home project, and the member projects it names at `onetaskgraph.members`.
856struct Home {
857    /// The home, where it landed.
858    id: GlobalId,
859    /// Its title and status, which a member created for it takes.
860    title: String,
861    /// See `title`.
862    status: onetaskgraph_plugin_api::Status,
863    /// The members it names, at most one per source.
864    members: Vec<GlobalId>,
865    /// Whether this copy added or replaced one, so the home's own list is written once the
866    /// copy has landed.
867    grew: bool,
868}
869
870impl Home {
871    /// A home as its source holds it, with the members it already names.
872    ///
873    /// # Errors
874    ///
875    /// The home's own source refusing, as malformed, a member list [`members_of`] cannot
876    /// read: a copy filing into a plan it cannot read whole would add a second member beside
877    /// one it failed to see.
878    fn of(id: GlobalId, held: &Project) -> Result<Self, EngineError> {
879        let members =
880            members_of(&id, &held.metadata).map_err(|message| EngineError::SourceRefused {
881                name: id.source.to_string(),
882                error: SourceError::Malformed { message },
883            })?;
884        Ok(Self {
885            id,
886            title: held.title.clone(),
887            status: held.status.clone(),
888            members,
889            grew: false,
890        })
891    }
892}
893
894/// The member projects the home `home` records at [`MetadataKey::MEMBERS_KEY`]; none when it
895/// records the key not at all.
896///
897/// # Errors
898///
899/// Why, when the key holds anything but a list of qualified ids, each in a different source
900/// from the home and from the others — the one shape the routed write that keeps it writes.
901pub(crate) fn members_of(
902    home: &GlobalId,
903    metadata: &BTreeMap<String, Value>,
904) -> Result<Vec<GlobalId>, String> {
905    let Some(value) = metadata.get(MetadataKey::MEMBERS_KEY) else {
906        return Ok(Vec::new());
907    };
908    let refuse = |why: String| {
909        format!(
910            "{home} records {key} as {value}, which {why}; a home's member list is a list of \
911             qualified project ids, at most one per source and none in the home's own",
912            key = MetadataKey::MEMBERS_KEY
913        )
914    };
915    let Value::Array(entries) = value else {
916        return Err(refuse("is not a list".to_owned()));
917    };
918    let mut members: Vec<GlobalId> = Vec::new();
919    for entry in entries {
920        let member = entry
921            .as_str()
922            .and_then(|entry| entry.parse::<GlobalId>().ok())
923            .ok_or_else(|| refuse(format!("holds {entry}, which is not a qualified id")))?;
924        if member.source == home.source || members.iter().any(|kept| kept.source == member.source) {
925            return Err(refuse(format!(
926                "names {member}, a second project in {}",
927                member.source
928            )));
929        }
930        members.push(member);
931    }
932    Ok(members)
933}
934
935/// One item a copy landed, and what settling its `onetaskgraph.copies` link needs.
936struct Linking {
937    /// The item, at its own source.
938    item: GlobalId,
939    /// Which of that source's interfaces holds it.
940    level: Level,
941    /// Which rule found where it landed, or `None` when this copy created it.
942    found: Option<Found>,
943    /// Where it landed.
944    destination: GlobalId,
945    /// What it held at `onetaskgraph.copies` when it was read, if anything.
946    held: Option<Value>,
947}
948
949/// One task a copy landed, and what the relation rule reads of it once the copy is complete.
950struct LandedTask {
951    /// Where it landed.
952    destination: GlobalId,
953    /// The source it was read from, which a bare `delivers` entry names a task of.
954    origin: SourceName,
955    /// Its `delivers`, as its source reported them.
956    delivers: Vec<TaskRef>,
957    /// The `delivers` the destination held there before this copy, qualified.
958    before: Vec<GlobalId>,
959    /// Its status category, as the copy wrote it.
960    category: StatusCategory,
961}
962
963/// A task a copy landed, with the tasks it delivers now and delivered before, qualified.
964struct Deliverer {
965    destination: GlobalId,
966    category: StatusCategory,
967    now: Vec<GlobalId>,
968    before: Vec<GlobalId>,
969}
970
971/// One item, read and resolved, on its way into the destination.
972struct Planned {
973    /// Where it came from.
974    source: GlobalId,
975    /// The item as its source reported it.
976    item: Item,
977    /// Its forward edges, as its source reported them.
978    edges: Vec<DependencyEdge>,
979    /// Where it is going.
980    target: Target,
981    /// What the destination held at that target, read once where the target was found.
982    ///
983    /// The read that decided an item exists is the read of what it holds, so the two are
984    /// one round trip rather than two against a hosted destination.
985    held: Option<Prior>,
986    /// The source it lands in: the copy's destination, or the source a route sent it to.
987    to: SourceName,
988    /// What the report says placed it there, for a copy into a source with routes.
989    placed: Option<Placement>,
990}
991
992/// One item read out of its source, before anything decides where it lands.
993struct Read {
994    /// Where it came from.
995    source: GlobalId,
996    /// The item as its source reported it.
997    item: Item,
998}
999
1000/// A task, a project or a document, so the copy path is written once.
1001#[derive(Clone)]
1002enum Item {
1003    /// A task.
1004    Task(Box<Task>),
1005    /// A project.
1006    Project(Box<Project>),
1007    /// A document.
1008    Document(Box<Document>),
1009}
1010
1011impl Item {
1012    fn id(&self) -> &NativeId {
1013        match self {
1014            Self::Task(task) => &task.id,
1015            Self::Project(project) => &project.id,
1016            Self::Document(document) => &document.id,
1017        }
1018    }
1019
1020    fn level(&self) -> Level {
1021        match self {
1022            Self::Task(_) => Level::Task,
1023            Self::Project(_) => Level::Project,
1024            Self::Document(_) => Level::Document,
1025        }
1026    }
1027}
1028
1029/// Which of a destination's three read-and-write interfaces one item belongs to.
1030///
1031/// Deliberately not [`ItemKind`]: that enum names what a *dependency endpoint* points at,
1032/// and the contract gives it no document variant because nothing may point at a document.
1033/// This one names which pair of methods reads and writes an item, which is a different
1034/// question with a third answer.
1035///
1036/// Ordered so it can key a map of what a destination holds, per interface: an id alone
1037/// does not identify a destination item, for the reason [`Undo::kind`] records.
1038#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1039enum Level {
1040    /// `get_task`, `write_task`, `delete_task`.
1041    Task,
1042    /// `get_project`, `write_project`, `delete_project`.
1043    Project,
1044    /// `get_document`, `write_document`, `delete_document`.
1045    Document,
1046}
1047
1048/// One record filed under a document's own project, and the location string its source
1049/// reports for it.
1050///
1051/// A reference is a **literal occurrence, in a document's content, of the exact location
1052/// string the source reports for a related record** — the `String` inside
1053/// [`Location::Path`] or [`Location::Url`]. Both ends of a rewrite come from the plugins'
1054/// own reported [`Location`]: nothing here composes an address out of a name, an id or a
1055/// root, because a source that reports a canonical absolute path and a source that reports
1056/// an issue link are the two things the contract lets this ask about.
1057#[derive(Clone)]
1058struct Referent {
1059    /// Its qualified id at the source.
1060    id: GlobalId,
1061    /// The origin it records in its own metadata, when it records one.
1062    origin: Option<GlobalId>,
1063    /// Which of the destination's three interfaces its counterpart would be read from.
1064    level: Level,
1065    /// The non-empty location string its source reports for it.
1066    location: String,
1067}
1068
1069impl Referent {
1070    /// The two keys a destination record's own recorded origin is matched against.
1071    ///
1072    /// **A destination record is this referent's counterpart when its
1073    /// [`GlobalId::ORIGIN_KEY`] equals either this referent's own qualified source id, or
1074    /// the origin this referent itself records.** In plainer terms: when the destination
1075    /// record was copied directly from this referent, or directly from the one predecessor
1076    /// this referent itself records.
1077    ///
1078    /// That reach is exactly one recorded hop of ancestry on each side, and nothing more:
1079    ///
1080    /// - **A single hop resolves.** The destination record came straight from the referent.
1081    /// - **A one-level fan-out resolves.** A record copied from one store into two, with
1082    ///   the document arriving by one route and naming records that arrived by the other,
1083    ///   so both sides trace to one common predecessor. That is what the second key buys,
1084    ///   and it is the only thing it buys.
1085    /// - **A chain of two or more hops does not resolve**, on either side, and that is
1086    ///   permanent rather than pending: only one origin is ever recorded and every hop
1087    ///   overwrites it. [`carried`] removes the key from the incoming metadata outright and
1088    ///   [`recorded`] writes the id at the *immediate* source on every path but the
1089    ///   copy-back, so A → B → C leaves C keyed by B and A's id gone.
1090    ///
1091    /// The second key costs no read — the referent's metadata is already in hand. Chasing
1092    /// the chain further would need the intermediate stores configured and reachable, which
1093    /// would put a third party's availability inside a copy; a durable lineage id would
1094    /// identify only records written after it landed, so it would resolve nothing already
1095    /// on a destination.
1096    fn keys(&self) -> Vec<String> {
1097        let mut keys = vec![self.id.to_string()];
1098        if let Some(origin) = &self.origin {
1099            let recorded = origin.to_string();
1100            if recorded != keys[0] {
1101                keys.push(recorded);
1102            }
1103        }
1104        keys
1105    }
1106}
1107
1108/// What every whole-reference occurrence of one location string becomes.
1109///
1110/// Three variants rather than a rewrite beside a flag, because the two ways of leaving an
1111/// occurrence alone are what the two figures a copy reports are *about*: one is the design
1112/// working and the other says the destination or the source holds something a re-run will
1113/// never fix. A bool would let a reader of this type read them as the same outcome.
1114enum Resolution {
1115    /// The destination's own location string for the counterpart.
1116    Rewrite(String),
1117    /// The destination holds no counterpart, or holds one it reports no location for, so
1118    /// the occurrence is left byte-for-byte. Ordinary and expected under the bound this
1119    /// design works to.
1120    NoCounterpart,
1121    /// The correspondence could not be established **confidently** — more than one
1122    /// destination record matches the referent's two keys, or two referents report this one
1123    /// location string. The occurrence is left byte-for-byte, no record is chosen, and a
1124    /// re-run will never clear it.
1125    Ambiguous,
1126}
1127
1128/// Every destination record that records an origin, read **once per copy invocation**.
1129///
1130/// Several documents in one project is the ordinary case, and a walk per document would
1131/// multiply reads against a rate limiter for no gain — so this is built once, for the
1132/// levels the invocation's own documents really name, and every document and every
1133/// referent of that invocation is answered out of it. A copy whose documents hold no
1134/// candidate reference builds none at all.
1135///
1136/// **This is a stricter discipline than [`Engine::scan`], deliberately.** That lookup takes
1137/// the first hit and stops, and every consumer of the copy already depends on it doing so;
1138/// it chooses the copy's own target, where a caller named the item. This one edits the
1139/// content of somebody's document, where a wrong answer is silent corruption of prose a
1140/// person will act on — so where more than one record matches, it chooses none. The two
1141/// lookups answer different questions and are meant to disagree on a destination holding
1142/// duplicates.
1143#[derive(Default)]
1144struct Counterparts {
1145    /// The records at one interface recording one origin.
1146    by_origin: BTreeMap<(Level, String), Vec<Held>>,
1147}
1148
1149/// One record a destination walk found: where it is there, and the location string that
1150/// destination reports for it — `None` when it reports none.
1151type Held = (NativeId, Option<String>);
1152
1153impl Counterparts {
1154    /// Record one destination item, when it records an origin at all.
1155    fn note(
1156        &mut self,
1157        level: Level,
1158        id: &NativeId,
1159        location: Option<&Location>,
1160        metadata: &BTreeMap<String, Value>,
1161    ) {
1162        let Some(origin) = origin_of(metadata) else {
1163            return;
1164        };
1165        self.by_origin
1166            .entry((level, origin.to_string()))
1167            .or_default()
1168            .push((id.clone(), located(location)));
1169    }
1170
1171    /// What one referent's occurrences become, by the two-key rule.
1172    ///
1173    /// Where the correspondence cannot be established **confidently**, the text is left
1174    /// exactly as it is and no record is chosen — see the note on this type.
1175    fn resolve(&self, referent: &Referent) -> Resolution {
1176        let mut candidates: Vec<&Held> = Vec::new();
1177        for key in referent.keys() {
1178            for record in self
1179                .by_origin
1180                .get(&(referent.level, key))
1181                .into_iter()
1182                .flatten()
1183            {
1184                // One destination record matching both keys is one record, not two.
1185                if !candidates.iter().any(|held| held.0 == record.0) {
1186                    candidates.push(record);
1187                }
1188            }
1189        }
1190        match candidates.as_slice() {
1191            [] => Resolution::NoCounterpart,
1192            // A counterpart the destination reports no location for names nowhere a reader
1193            // could go, so the source's own string is left standing rather than removed.
1194            [(_, location)] => location
1195                .clone()
1196                .map_or(Resolution::NoCounterpart, Resolution::Rewrite),
1197            _ => Resolution::Ambiguous,
1198        }
1199    }
1200}
1201
1202impl Engine {
1203    /// Copy every item a request names into one configured destination.
1204    ///
1205    /// This is the whole of the verb, and the command line drives exactly this: a copy a
1206    /// Rust caller makes and a copy typed at a shell are the same call, so the two cannot
1207    /// answer the same copy differently.
1208    ///
1209    /// # Errors
1210    ///
1211    /// Returns [`EngineError`] when the destination is not configured, cannot be built,
1212    /// cannot be written, or — for a document copy — declares it has no documents; when an
1213    /// id names nothing; when an origin names an item the
1214    /// destination no longer holds and `--recreate` was not given; and when the
1215    /// destination refuses the write — including a field or a metadata key it cannot
1216    /// carry, which it names rather than dropping.
1217    pub async fn copy(&self, request: &CopyRequest) -> Result<CopyReport, EngineError> {
1218        // Before anything is read: `--create` says there is nothing to look for, and the other
1219        // two say how to look.
1220        if request.create {
1221            for (given, flag) in [
1222                (request.match_by.is_some(), CopyLookup::MatchBy),
1223                (request.recreate, CopyLookup::Recreate),
1224            ] {
1225                if given {
1226                    return Err(EngineError::CreateWith { flag });
1227                }
1228            }
1229        }
1230        let destination = self.writable(&request.destination)?;
1231        // Before anything is read, and from the declaration rather than from a failed
1232        // write: a destination that says it has no documents has nowhere to put one.
1233        if request.scope == CopyScope::Documents {
1234            documentary(destination)?;
1235        }
1236        // The sources this command names, each read once before and once after, so what it
1237        // spent is the difference between two of each one's own running totals.
1238        let mut metered = vec![destination];
1239        for routed in self.routes.reachable(&request.destination) {
1240            if let Some(source) = self.ready().find(|source| source.name() == &routed)
1241                && !metered.iter().any(|held| held.name() == source.name())
1242            {
1243                metered.push(source);
1244            }
1245        }
1246        for id in request.items.as_slice() {
1247            if let Some(source) = self.ready().find(|source| source.name() == &id.source)
1248                && !metered.iter().any(|held| held.name() == source.name())
1249            {
1250                metered.push(source);
1251            }
1252        }
1253        let before = readings(&metered).await;
1254        let mut journal = Journal::default();
1255        match self.copy_all(request, &mut journal).await {
1256            Ok((mut report, deliverers)) => {
1257                // After the whole copy is complete and outside the journal: what the relation
1258                // rule writes is the delivered tasks' own, and a failure there is reported
1259                // against that task rather than undoing a copy that landed.
1260                for deliverer in &deliverers {
1261                    report.delivered.extend(
1262                        self.deliver(
1263                            &deliverer.destination,
1264                            deliverer.category,
1265                            &deliverer.now,
1266                            &deliverer.before,
1267                        )
1268                        .await,
1269                    );
1270                }
1271                report.spent = spent_between(&before, &readings(&metered).await);
1272                Ok(report)
1273            }
1274            Err(error) => Err(self.undo(journal, error).await),
1275        }
1276    }
1277
1278    /// The copy itself, with everything it writes recorded so a failure can be undone.
1279    ///
1280    /// The ids named together are **one** copied set, and that is what makes an edge
1281    /// between any two of them a real edge at the destination: a copy of two projects at
1282    /// once knows that a task in the first depends on a task in the second, and a task
1283    /// knows that the project it belongs to is being created beside it. Copying them one
1284    /// at a time could not, and wrote the far end as the id it had at its *source* — a
1285    /// dangling reference to somewhere the destination has never heard of.
1286    async fn copy_all(
1287        &self,
1288        request: &CopyRequest,
1289        journal: &mut Journal,
1290    ) -> Result<(CopyReport, Vec<Deliverer>), EngineError> {
1291        let mut running = Running::default();
1292        // The whole copied set, established before anything is written. For a project
1293        // copy that means reading every named project's membership first: the set is the
1294        // whole request rather than one project of it.
1295        let items = match &request.scope {
1296            CopyScope::Tasks | CopyScope::Documents => {
1297                let kind = if request.scope == CopyScope::Documents {
1298                    Level::Document
1299                } else {
1300                    Level::Task
1301                };
1302                running
1303                    .resolvable
1304                    .extend(request.items.as_slice().iter().cloned());
1305                let mut planned = Vec::new();
1306                for id in request.items.as_slice() {
1307                    planned.push(self.plan(request, kind, id, &mut running).await?);
1308                }
1309                self.copy_items(request, planned, None, &mut running, journal)
1310                    .await?
1311            }
1312            CopyScope::Projects { tasks } => {
1313                let mut projects = Vec::new();
1314                for id in request.items.as_slice() {
1315                    let members = if *tasks {
1316                        self.project_members(id).await?
1317                    } else {
1318                        Vec::new()
1319                    };
1320                    running.resolvable.push(id.clone());
1321                    running.resolvable.extend(members.iter().cloned());
1322                    projects.push((id.clone(), members));
1323                }
1324                self.copy_projects(request, &projects, &[], &mut running, journal)
1325                    .await?
1326            }
1327            CopyScope::Members(named) => {
1328                let (projects, unrecorded) =
1329                    self.named_members(request, named, &mut running).await?;
1330                self.copy_projects(request, &projects, &unrecorded, &mut running, journal)
1331                    .await?
1332            }
1333        };
1334        let references = running.references;
1335        let delivers_rewritten = running.delivers_rewritten;
1336        // Every member's destination id is known once the first pass has landed it, so the
1337        // tasks each landed task delivers are settled here rather than after the repair.
1338        let deliverers: Vec<Deliverer> = running
1339            .landed
1340            .iter()
1341            .map(|task| Deliverer {
1342                destination: task.destination.clone(),
1343                category: task.category,
1344                now: targets(
1345                    &resolved_entries(&mapped_delivers(
1346                        &task.delivers,
1347                        &task.origin,
1348                        &task.destination.source,
1349                        &running.resolvable,
1350                        &running.counterparts,
1351                    )),
1352                    &task.destination.source,
1353                ),
1354                before: task.before.clone(),
1355            })
1356            .collect();
1357        let linking = std::mem::take(&mut running.linking);
1358        let homes = std::mem::take(&mut running.homes);
1359        self.repair(request, running, journal).await?;
1360        if !request.dry_run {
1361            self.record_members(homes, journal).await?;
1362        }
1363        let mut items = items;
1364        self.link(linking, &mut items, journal).await?;
1365        Ok((
1366            CopyReport {
1367                items,
1368                references_rewritten: references.rewritten,
1369                references_unresolved: references.unresolved,
1370                references_ambiguous: references.ambiguous,
1371                delivers_rewritten,
1372                // Filled in by `copy`, once the copy is complete.
1373                delivered: Vec::new(),
1374                // Filled in by `copy`, which is the one place both readings are taken.
1375                spent: None,
1376            },
1377            deliverers,
1378        ))
1379    }
1380
1381    /// Write every deferred item again, now that every destination id is known.
1382    ///
1383    /// This is the second half of the two passes an edge between two items of one copy
1384    /// needs: the far end's destination id does not exist until it has been created, so
1385    /// the item that points at it lands first without that edge and is completed here.
1386    /// It runs once for the whole request rather than once per project, because a far end
1387    /// may be in a project this copy has not reached yet.
1388    async fn repair(
1389        &self,
1390        request: &CopyRequest,
1391        running: Running,
1392        journal: &mut Journal,
1393    ) -> Result<(), EngineError> {
1394        if request.dry_run {
1395            return Ok(());
1396        }
1397        let Running {
1398            resolvable,
1399            counterparts,
1400            deferred,
1401            ..
1402        } = running;
1403        for entry in deferred {
1404            let destination = self.writable(&entry.item.to)?;
1405            let edges = mapped_edges(
1406                &entry.item.edges,
1407                &entry.item.source.source,
1408                &entry.item.to,
1409                &resolvable,
1410                &counterparts,
1411            );
1412            let delivers = delivers_of(&entry.item, &entry.item.to, &resolvable, &counterparts);
1413            self.write(
1414                destination,
1415                &entry.item,
1416                Some(entry.destination),
1417                entry.filed,
1418                &resolved(&edges),
1419                &resolved_entries(&delivers),
1420                entry.prior,
1421                journal,
1422            )
1423            .await?;
1424        }
1425        Ok(())
1426    }
1427
1428    /// Put the destination back the way this copy found it, then report why it failed.
1429    ///
1430    /// Undone in reverse, and an item this copy created is removed rather than restored —
1431    /// the entry recording what it looked like a moment after creation is not a state
1432    /// anybody asked for. When the destination cannot take one of them back, the refusal
1433    /// says so and names what is still there, because a user told "the copy failed" about
1434    /// a destination that is not as they left it will copy again over a tree nobody
1435    /// described.
1436    async fn undo(&self, journal: Journal, error: EngineError) -> EngineError {
1437        let created: Vec<(&SourceName, Level, &NativeId)> = journal
1438            .entries
1439            .iter()
1440            .filter_map(|entry| match entry {
1441                Undo::Created { at, kind, id } => Some((at, *kind, id)),
1442                Undo::Updated { .. } => None,
1443            })
1444            .collect();
1445        // The ids and the refusal are one value rather than two, because they are one
1446        // fact: an item is only left behind because the destination refused to take it
1447        // back, so the first refusal carries the first id and neither half can be
1448        // recorded without the other.
1449        let mut unrestored: Option<(LeftBehind, SourceError)> = None;
1450        // The links first, because they were written last: once the whole copy had landed.
1451        for entry in journal.links.iter().rev() {
1452            if let Err(problem) = self.unlink(entry).await {
1453                match &mut unrestored {
1454                    Some((left_behind, _)) => left_behind.push(entry.item.clone()),
1455                    None => unrestored = Some((LeftBehind::new(entry.item.clone()), problem)),
1456                }
1457            }
1458        }
1459        for entry in journal.entries.iter().rev() {
1460            let outcome = match self.writable(entry.at()) {
1461                Err(failed) => Err(SourceError::Refused {
1462                    message: failed.to_string(),
1463                }),
1464                Ok(destination) => match entry {
1465                    Undo::Created { kind, id, .. } => remove(destination, *kind, id).await,
1466                    Undo::Updated { at, id, prior }
1467                        if !created.contains(&(at, prior.item.level(), id)) =>
1468                    {
1469                        restore(destination, id, prior).await
1470                    }
1471                    Undo::Updated { .. } => Ok(()),
1472                },
1473            };
1474            if let Err(problem) = outcome {
1475                let id = GlobalId::new(entry.at().clone(), entry.id().clone());
1476                match &mut unrestored {
1477                    Some((left_behind, _)) => left_behind.push(id),
1478                    None => unrestored = Some((LeftBehind::new(id), problem)),
1479                }
1480            }
1481        }
1482        match unrestored {
1483            None => error,
1484            Some((left_behind, refusal)) => EngineError::CopyNotUndone {
1485                error: Box::new(error),
1486                left_behind,
1487                refusal,
1488            },
1489        }
1490    }
1491
1492    /// Record, on every item this copy landed, where it landed — once the whole copy has
1493    /// landed, so a copy that fails before this point has written nothing on any source.
1494    ///
1495    /// Each outcome is told what happened to its item's link. The items landed are exactly
1496    /// the outcomes that are not orphans, in the order they landed; a dry run records
1497    /// nothing, so this is never asked about one.
1498    async fn link(
1499        &self,
1500        linking: Vec<Linking>,
1501        items: &mut [CopyOutcome],
1502        journal: &mut Journal,
1503    ) -> Result<(), EngineError> {
1504        let mut landed = items
1505            .iter_mut()
1506            .filter(|outcome| !matches!(outcome.action, CopyAction::Orphaned { .. }));
1507        for entry in linking {
1508            let slot = landed
1509                .next()
1510                .filter(|outcome| outcome.source == entry.item)
1511                .and_then(|outcome| outcome.action.link_slot())
1512                .expect("every item a copy landed is reported, in the order it landed");
1513            *slot = Some(self.linked(entry, journal).await?);
1514        }
1515        Ok(())
1516    }
1517
1518    /// Write one landed item's `onetaskgraph.copies` entry for the destination, when it
1519    /// does not already say this, and say what was done.
1520    ///
1521    /// An item found by its own origin records nothing: that correspondence is already
1522    /// recorded, on the destination item. A source that cannot hold the entry — it has no
1523    /// write side, or refuses the narrow metadata write — leaves the copy as it landed and
1524    /// is reported so; any other failure fails the copy, which then undoes every write it
1525    /// made, this one's journal entry included.
1526    async fn linked(&self, entry: Linking, journal: &mut Journal) -> Result<CopyLink, EngineError> {
1527        // Keyed by the source the item really landed in, which a routed copy decides per item.
1528        let landed_in = entry.destination.source.clone();
1529        if entry.found == Some(Found::Origin) {
1530            return Ok(CopyLink::Unchanged);
1531        }
1532        let wanted = Value::String(entry.destination.to_string());
1533        // Only the entries that are links are kept. Anything else under the key — a value
1534        // that is not an object, or an entry a person spelled wrong — is not a link this or
1535        // any later copy can read, so it is replaced by what is, exactly as an entry naming
1536        // another item is rewritten.
1537        let mut links = well_formed(entry.held.as_ref());
1538        if links.get(landed_in.as_str()) == Some(&wanted) {
1539            return Ok(CopyLink::Unchanged);
1540        }
1541        let source = self.readable(&entry.item.source)?;
1542        if !source.source().writes().is_supported() {
1543            return Ok(CopyLink::Unrecorded);
1544        }
1545        links.insert(landed_in.to_string(), wanted);
1546        // Journalled before the write, for the reason a destination write is: a write that
1547        // stopped part way is only put back by what this journal holds.
1548        journal.links.push(Unlink {
1549            item: entry.item.clone(),
1550            level: entry.level,
1551            before: entry.held,
1552        });
1553        match set_link(
1554            source,
1555            entry.level,
1556            &entry.item.native,
1557            &Value::Object(links),
1558        )
1559        .await
1560        {
1561            Ok(true) => Ok(CopyLink::Recorded),
1562            // Nothing was written: the source no longer holds the item, or said it cannot
1563            // hold the key.
1564            Ok(false) | Err(SourceError::Refused { .. }) => {
1565                journal.links.pop();
1566                Ok(CopyLink::Unrecorded)
1567            }
1568            // §4.18 once let a plugin refuse any key in this namespace as malformed, and one
1569            // written then still may. Its answer says nothing about whether it wrote, so the
1570            // journal entry stays and a later failure puts the item back regardless.
1571            Err(SourceError::Malformed { .. }) => Ok(CopyLink::Unrecorded),
1572            Err(error) => Err(refused(source, error)),
1573        }
1574    }
1575
1576    /// Put one copied item's `onetaskgraph.copies` back to what it held before this copy.
1577    ///
1578    /// A key it held is written back through the same narrow write. A key it did not hold
1579    /// cannot be removed through that write, so the item is read as it now is and written
1580    /// back whole, without the key, through the source's own write interface — the same
1581    /// way an overwritten destination item is restored.
1582    async fn unlink(&self, entry: &Unlink) -> Result<(), SourceError> {
1583        let as_source_error = |error: EngineError| match error {
1584            EngineError::SourceRefused { error, .. } => error,
1585            other => SourceError::Refused {
1586                message: other.to_string(),
1587            },
1588        };
1589        let source = self.readable(&entry.item.source).map_err(as_source_error)?;
1590        if let Some(before) = &entry.before {
1591            return set_link(source, entry.level, &entry.item.native, before)
1592                .await
1593                .map(|_| ());
1594        }
1595        let Some(mut held) = self
1596            .prior(source, entry.level, &entry.item.native)
1597            .await
1598            .map_err(as_source_error)?
1599        else {
1600            return Ok(());
1601        };
1602        metadata_of(&mut held.item).remove(MetadataKey::COPIES_KEY);
1603        restore(source, &entry.item.native, &held).await
1604    }
1605
1606    /// The member project `home` names in `to`, created and recorded on the home when it
1607    /// names none — for a write that is not a copy, such as `task create`, routed away from
1608    /// the source its project is in. `None` when the home itself is not there.
1609    ///
1610    /// What it wrote is handed back, so a write that then fails takes it back through
1611    /// [`Engine::unfile`] and leaves no member nobody asked for.
1612    pub(super) async fn member_project(
1613        &self,
1614        home: &GlobalId,
1615        to: &SourceName,
1616    ) -> Result<Filed, EngineError> {
1617        let mut running = Running::default();
1618        let mut journal = Journal::default();
1619        let mut filed = self
1620            .member_in(false, home, to, &mut running, &mut journal)
1621            .await;
1622        if filed.is_ok() {
1623            let homes = std::mem::take(&mut running.homes);
1624            if let Err(error) = self.record_members(homes, &mut journal).await {
1625                filed = Err(error);
1626            }
1627        }
1628        match filed {
1629            Ok(project) => Ok(Filed { project, journal }),
1630            Err(error) => Err(self.undo(journal, error).await),
1631        }
1632    }
1633
1634    /// Take back what [`Engine::member_project`] wrote, because the write it filed for failed
1635    /// with `error`.
1636    pub(super) async fn unfile(&self, filed: Filed, error: EngineError) -> EngineError {
1637        self.undo(filed.journal, error).await
1638    }
1639
1640    /// The destination source, once it is established it exists and can be written.
1641    fn writable(&self, name: &SourceName) -> Result<&ResolvedSource, EngineError> {
1642        let name = self.known(name)?;
1643        if let Some(unavailable) = self.unavailable().find(|source| source.name() == &name) {
1644            return Err(EngineError::DestinationUnavailable {
1645                name: name.to_string(),
1646                error: unavailable.error().clone(),
1647            });
1648        }
1649        let source = self
1650            .ready()
1651            .find(|source| source.name() == &name)
1652            .ok_or(EngineError::NoSources)?;
1653        if !source.source().writes().is_supported() {
1654            return Err(EngineError::NotWritable {
1655                name: name.to_string(),
1656                kind: source.kind().to_owned(),
1657            });
1658        }
1659        Ok(source)
1660    }
1661
1662    /// Copy each project and what travels with it: every task in it, the members a member
1663    /// copy names, or nothing at all.
1664    ///
1665    /// Every item of every project is read and its target decided **before any of them is
1666    /// written, and each exactly once.** That is what lets a member copy refuse an edge it
1667    /// cannot resolve while the destination is still as it was found, and what stops a
1668    /// whole copy resolving one target twice — once to learn the ids the project's own
1669    /// edges name, and again to land it — which against a hosted destination is a read per
1670    /// item spent on an answer the command already had.
1671    async fn copy_projects(
1672        &self,
1673        request: &CopyRequest,
1674        projects: &[(GlobalId, Vec<GlobalId>)],
1675        unrecorded: &[GlobalId],
1676        running: &mut Running,
1677        journal: &mut Journal,
1678    ) -> Result<Vec<CopyOutcome>, EngineError> {
1679        let mut plans = Vec::new();
1680        for (id, members) in projects {
1681            if self.routes.routes(&request.destination) {
1682                // Every task is read before the project is aimed, because where its home goes
1683                // is decided by where they do.
1684                let project = self.read(Level::Project, id).await?;
1685                let mut reads = Vec::new();
1686                for member in members {
1687                    reads.push(self.read(Level::Task, member).await?);
1688                }
1689                let home = self.home_placement(request, &project, &reads).await?;
1690                let project = self.aim(request, project, home).await?;
1691                let mut tasks = Vec::new();
1692                for read in reads {
1693                    let placement = match &read.item {
1694                        Item::Task(task) => {
1695                            self.routes.place(&request.destination, &task.repositories)
1696                        }
1697                        Item::Project(_) | Item::Document(_) => {
1698                            self.placed_at(request, &request.destination)
1699                        }
1700                    };
1701                    tasks.push(self.aim(request, read, placement).await?);
1702                }
1703                plans.push((project, tasks));
1704            } else {
1705                let project = self.plan(request, Level::Project, id, running).await?;
1706                let mut tasks = Vec::new();
1707                for member in members {
1708                    tasks.push(self.plan(request, Level::Task, member, running).await?);
1709                }
1710                plans.push((project, tasks));
1711            }
1712        }
1713        for (project, tasks) in &plans {
1714            for item in std::iter::once(project).chain(tasks) {
1715                unrecorded_far_end(item, unrecorded)?;
1716            }
1717        }
1718        let mut outcomes = Vec::new();
1719        for (project, tasks) in plans {
1720            outcomes.extend(
1721                self.copy_project(request, project, tasks, running, journal)
1722                    .await?,
1723            );
1724        }
1725        Ok(outcomes)
1726    }
1727
1728    /// Where a routed project copy lands the project: its **home**.
1729    ///
1730    /// A home that already exists, in any source the copy can reach, stays where it is —
1731    /// found by the project's own link or origin. Otherwise the home follows its tasks only
1732    /// when every one copied routes to one source, and so does the project's own
1733    /// repositories when it names any; any other project's home is the copy's destination.
1734    /// With no task copied, the project's own repositories alone decide.
1735    async fn home_placement(
1736        &self,
1737        request: &CopyRequest,
1738        project: &Read,
1739        tasks: &[Read],
1740    ) -> Result<Placement, EngineError> {
1741        let Item::Project(held) = &project.item else {
1742            return Ok(self.placed_at(request, &request.destination));
1743        };
1744        let mut placements: Vec<Placement> = tasks
1745            .iter()
1746            .filter_map(|read| match &read.item {
1747                Item::Task(task) => {
1748                    Some(self.routes.place(&request.destination, &task.repositories))
1749                }
1750                Item::Project(_) | Item::Document(_) => None,
1751            })
1752            .collect();
1753        if !held.repositories.is_empty() {
1754            placements.push(self.routes.place(&request.destination, &held.repositories));
1755        }
1756        let rule = match placements.first() {
1757            Some(first)
1758                if placements
1759                    .iter()
1760                    .all(|placement| placement.destination == first.destination) =>
1761            {
1762                first.clone()
1763            }
1764            _ => self.placed_at(request, &request.destination),
1765        };
1766        for elsewhere in self.routes.reachable(&request.destination) {
1767            if elsewhere == rule.destination {
1768                continue;
1769            }
1770            let there = self.writable(&elsewhere)?;
1771            let named = link_of(&held.metadata, &elsewhere)
1772                .or_else(|| origin_of(&held.metadata).filter(|origin| origin.source == elsewhere));
1773            let Some(named) = named else {
1774                continue;
1775            };
1776            let Some(found) = self.prior(there, Level::Project, &named.native).await? else {
1777                continue;
1778            };
1779            let names_back = origin_of(described(&found.item).1).as_ref() == Some(&project.source);
1780            if names_back || origin_of(&held.metadata).as_ref() == Some(&named) {
1781                return Ok(self.placed_at(request, &elsewhere));
1782            }
1783        }
1784        Ok(rule)
1785    }
1786
1787    /// Land one planned project, then the tasks planned beside it.
1788    async fn copy_project(
1789        &self,
1790        request: &CopyRequest,
1791        project: Planned,
1792        tasks: Vec<Planned>,
1793        running: &mut Running,
1794        journal: &mut Journal,
1795    ) -> Result<Vec<CopyOutcome>, EngineError> {
1796        let (carries_tasks, walks_orphans) = match request.scope {
1797            CopyScope::Projects { tasks } => (tasks, tasks),
1798            CopyScope::Members(_) => (true, false),
1799            CopyScope::Tasks | CopyScope::Documents => (false, false),
1800        };
1801        let id = project.source.clone();
1802        let members: Vec<GlobalId> = tasks.iter().map(|task| task.source.clone()).collect();
1803        // The project lands with every edge it can already resolve — its members' targets
1804        // are known from their plans — so a project that has not changed is not written at
1805        // all. What it *reports* is read the way it always has been: against the ids known
1806        // before its own members' were, unless every edge resolves and nothing differs.
1807        // Reading it any other way would move the word a repeat copy reports for a project
1808        // whose only difference is an edge, which is not this change's to move.
1809        let mut before_members = running.counterparts.clone();
1810        if let Target::Update { id: target, .. } = &project.target {
1811            before_members.insert(
1812                id.to_string(),
1813                GlobalId::new(project.to.clone(), target.clone()),
1814            );
1815        }
1816        for item in std::iter::once(&project).chain(&tasks) {
1817            if let Target::Update { id: target, .. } = &item.target {
1818                running.counterparts.insert(
1819                    item.source.to_string(),
1820                    GlobalId::new(item.to.clone(), target.clone()),
1821                );
1822            }
1823        }
1824        let home_source = project.to.clone();
1825        let unchanged = match &project.target {
1826            Target::Update { id: target, .. } => {
1827                let settled = mapped_edges(
1828                    &project.edges,
1829                    &id.source,
1830                    &home_source,
1831                    &running.resolvable,
1832                    &running.counterparts,
1833                );
1834                let first = mapped_edges(
1835                    &project.edges,
1836                    &id.source,
1837                    &home_source,
1838                    &running.resolvable,
1839                    &before_members,
1840                );
1841                let held = project.held.as_ref();
1842                let unchanged_with = |edges: &[Option<DependencyEdge>]| {
1843                    !changes(
1844                        held,
1845                        &project,
1846                        target,
1847                        &None,
1848                        &resolved(edges),
1849                        &[],
1850                        &home_source,
1851                    )
1852                };
1853                Some(
1854                    (!settled.iter().any(Option::is_none) && unchanged_with(&settled))
1855                        || unchanged_with(&first),
1856                )
1857            }
1858            Target::Create => None,
1859        };
1860        // A project this copy creates holds nothing it did not file itself, so nothing filed
1861        // under it can be an orphan and the destination is not walked for one.
1862        let created = matches!(project.target, Target::Create);
1863        let mut outcomes = self
1864            .copy_items(request, vec![project], None, running, journal)
1865            .await?;
1866        if let (Some(unchanged), Some(landed), Some(via)) = (
1867            unchanged,
1868            outcomes[0].destination().cloned(),
1869            outcomes[0].action.found_by(),
1870        ) {
1871            let link = outcomes[0].action.link();
1872            outcomes[0].action = if unchanged {
1873                CopyAction::Unchanged {
1874                    destination: landed,
1875                    via,
1876                    link,
1877                }
1878            } else {
1879                CopyAction::Updated {
1880                    destination: landed,
1881                    via,
1882                    link,
1883                }
1884            };
1885        }
1886        if !carries_tasks {
1887            return Ok(outcomes);
1888        }
1889        // `None` when a dry run would have created the project: nothing was written, so
1890        // there is no destination project id to file the tasks under. Every task is still
1891        // read and still reported, because that is what a dry run is for.
1892        let filed = outcomes.first().and_then(CopyOutcome::destination).cloned();
1893        let filing = match &filed {
1894            None => None,
1895            Some(home) => {
1896                let mut filing = Filing::from([(home.source.clone(), home.native.clone())]);
1897                // A task routed to another source than its home's is filed under the home's
1898                // member project there, found or created before any of those tasks lands.
1899                let mut elsewhere: Vec<SourceName> = Vec::new();
1900                for task in &tasks {
1901                    if task.to != home.source && !elsewhere.contains(&task.to) {
1902                        elsewhere.push(task.to.clone());
1903                    }
1904                }
1905                for to in elsewhere {
1906                    if let Some(member) = self
1907                        .member_in(request.dry_run, home, &to, running, journal)
1908                        .await?
1909                    {
1910                        filing.insert(to, member);
1911                    }
1912                }
1913                Some(filing)
1914            }
1915        };
1916        outcomes.extend(
1917            self.copy_items(request, tasks, filing.as_ref(), running, journal)
1918                .await?,
1919        );
1920        // A member copy was told which members it carries, so it cannot tell a member it
1921        // left out from one the source no longer holds, and does not walk for either.
1922        if walks_orphans
1923            && !created
1924            && let Some(filed) = filed
1925        {
1926            let destination = self.writable(&filed.source)?;
1927            outcomes.extend(
1928                self.orphans(destination, &id, &filed.native, &members)
1929                    .await?,
1930            );
1931            // A routed plan's tasks are filed under its members too, so each member is walked
1932            // in its own source for what the source plan no longer holds.
1933            if self.routes.routes(&request.destination) {
1934                for member in self.members_named(&filed, running).await? {
1935                    let there = self.writable(&member.source)?;
1936                    outcomes.extend(self.orphans(there, &id, &member.native, &members).await?);
1937                }
1938            }
1939        }
1940        Ok(outcomes)
1941    }
1942
1943    /// The member projects `home` names: the list this copy already holds for it, grown
1944    /// members included, or else the one the home itself records.
1945    async fn members_named(
1946        &self,
1947        home: &GlobalId,
1948        running: &Running,
1949    ) -> Result<Vec<GlobalId>, EngineError> {
1950        if let Some(held) = running.homes.get(&home.to_string()) {
1951            return Ok(held.members.clone());
1952        }
1953        let at = self.writable(&home.source)?;
1954        let Some(held) = at
1955            .source()
1956            .get_project(&home.native)
1957            .await
1958            .map_err(|error| refused(at, error))?
1959        else {
1960            return Ok(Vec::new());
1961        };
1962        members_of(home, &held.metadata).map_err(|message| EngineError::SourceRefused {
1963            name: home.source.to_string(),
1964            error: SourceError::Malformed { message },
1965        })
1966    }
1967
1968    /// The members a member copy names, per project and in the order they were named, and
1969    /// every member it does not name that records no destination id.
1970    ///
1971    /// Settled before anything is read at the destination. A named id that is a member of
1972    /// none of the projects is refused here, naming it. An unnamed member whose recorded
1973    /// origin names the destination joins the copied set with that id already known, so an
1974    /// edge to it resolves exactly as an edge to a copied item does, and costs no read. An
1975    /// unnamed member recording none is returned, so an edge to it is refused before
1976    /// anything is written — see [`unrecorded_far_end`].
1977    async fn named_members(
1978        &self,
1979        request: &CopyRequest,
1980        named: &CopyItems,
1981        running: &mut Running,
1982    ) -> Result<(Vec<(GlobalId, Vec<GlobalId>)>, Vec<GlobalId>), EngineError> {
1983        let projects = request.items.as_slice();
1984        // A routed copy can land a member in any source it reaches, so an origin naming any
1985        // of them is a destination id already known.
1986        let reachable = self.routes.reachable(&request.destination);
1987        let mut carried = Vec::new();
1988        let mut unrecorded = Vec::new();
1989        for project in projects {
1990            let held = self.project_member_tasks(project).await?;
1991            let mut members: Vec<GlobalId> = Vec::new();
1992            for id in named.as_slice() {
1993                if held.iter().any(|task| &task.id == id) && !members.contains(id) {
1994                    members.push(id.clone());
1995                }
1996            }
1997            for task in held {
1998                if members.contains(&task.id) {
1999                    continue;
2000                }
2001                // A routed copy also reads the member's own link, because a plan authored
2002                // where it is copied from records where each task landed rather than an
2003                // origin, and its tasks may have landed in any source the copy reaches.
2004                let linked = || {
2005                    reachable
2006                        .iter()
2007                        .find_map(|there| link_of(&task.item.metadata, there))
2008                        .filter(|_| self.routes.routes(&request.destination))
2009                };
2010                match origin_of(&task.item.metadata)
2011                    .filter(|origin| reachable.contains(&origin.source))
2012                    .or_else(linked)
2013                {
2014                    Some(origin) => {
2015                        running.counterparts.insert(task.id.to_string(), origin);
2016                        running.resolvable.push(task.id);
2017                    }
2018                    None => unrecorded.push(task.id),
2019                }
2020            }
2021            running.resolvable.push(project.clone());
2022            running.resolvable.extend(members.iter().cloned());
2023            carried.push((project.clone(), members));
2024        }
2025        if let Some(stray) = named
2026            .as_slice()
2027            .iter()
2028            .find(|id| !carried.iter().any(|(_, members)| members.contains(id)))
2029        {
2030            return Err(EngineError::NotAMember {
2031                id: stray.clone(),
2032                projects: projects.to_vec(),
2033            });
2034        }
2035        Ok((carried, unrecorded))
2036    }
2037
2038    /// Every task the source holds in `project`, by qualified id.
2039    async fn project_members(&self, project: &GlobalId) -> Result<Vec<GlobalId>, EngineError> {
2040        Ok(self
2041            .project_member_tasks(project)
2042            .await?
2043            .into_iter()
2044            .map(|task| task.id)
2045            .collect())
2046    }
2047
2048    /// Every task the source holds in `project`, as the source reported it.
2049    ///
2050    /// The ids alone are what a copy files under a project; the whole task is what a
2051    /// document's references need, because the location a reference names is a field of it.
2052    async fn project_member_tasks(
2053        &self,
2054        project: &GlobalId,
2055    ) -> Result<Vec<Qualified<Task>>, EngineError> {
2056        let mut request = TaskRequest {
2057            sources: vec![project.source.clone()],
2058            filters: Filters::default(),
2059            project: ProjectSelector::Qualified(project.clone()),
2060            priorities: Vec::new(),
2061            commented_since: None,
2062            metadata: Vec::new(),
2063            origin: None,
2064            include_members: false,
2065            paging: Paging {
2066                limit: PROJECT_PAGE,
2067                token: None,
2068            },
2069        };
2070        let mut members = Vec::new();
2071        // Pages by this engine's own token rather than by a source cursor, and the
2072        // asymmetry with the three walks below is deliberate. A source answering two
2073        // cursors with each other advances on every page, so `unrepeated` under the list
2074        // verb never fires; the cycle shows only as a token handed back unchanged, which
2075        // is what this loop pages by. Point it at the source and a project copy spins.
2076        //
2077        // No page bound here for the same reason: `Engine::tasks` merges under the budget
2078        // it was asked, so nothing longer than `PROJECT_PAGE` can arrive. `fits` in
2079        // `fetch::walk` refuses the page a source can really overrun, its own, while
2080        // these very members are read.
2081        let misbehaved = |error| EngineError::SourceRefused {
2082            name: project.source.to_string(),
2083            error,
2084        };
2085        loop {
2086            let asked = request.paging.token.clone();
2087            let response = self.tasks(&request).await?;
2088            if let Some(failure) = response.errors.first() {
2089                return Err(EngineError::SourceRefused {
2090                    name: failure.source.to_string(),
2091                    error: failure.error.clone(),
2092                });
2093            }
2094            unrepeated(
2095                response.next.as_ref(),
2096                asked.as_ref(),
2097                "the tasks of a project were being read for a copy",
2098            )
2099            .map_err(misbehaved)?;
2100            members.extend(response.items);
2101            match response.next {
2102                Some(token) => request.paging.token = Some(token),
2103                None => return Ok(members),
2104            }
2105        }
2106    }
2107
2108    /// Every document the source holds in `project`, as the source reported it.
2109    ///
2110    /// Paged by this engine's own token for the reason the member walk above is, and the
2111    /// note there says why.
2112    async fn project_documents(
2113        &self,
2114        project: &GlobalId,
2115    ) -> Result<Vec<Qualified<Document>>, EngineError> {
2116        let mut request = DocumentRequest {
2117            sources: vec![project.source.clone()],
2118            filters: DocumentFilters::default(),
2119            project: ProjectSelector::Qualified(project.clone()),
2120            paging: Paging {
2121                limit: PROJECT_PAGE,
2122                token: None,
2123            },
2124        };
2125        let mut held = Vec::new();
2126        let misbehaved = |error| EngineError::SourceRefused {
2127            name: project.source.to_string(),
2128            error,
2129        };
2130        loop {
2131            let asked = request.paging.token.clone();
2132            let response = self.documents(&request).await?;
2133            if let Some(failure) = response.errors.first() {
2134                return Err(EngineError::SourceRefused {
2135                    name: failure.source.to_string(),
2136                    error: failure.error.clone(),
2137                });
2138            }
2139            unrepeated(
2140                response.next.as_ref(),
2141                asked.as_ref(),
2142                "the documents of a project were being read for a copy",
2143            )
2144            .map_err(misbehaved)?;
2145            held.extend(response.items);
2146            match response.next {
2147                Some(token) => request.paging.token = Some(token),
2148                None => return Ok(held),
2149            }
2150        }
2151    }
2152
2153    /// Point every reference the documents of this copy hold at the destination's own
2154    /// records, and say how many it could not.
2155    ///
2156    /// A document copied out of a local Markdown store used to arrive naming absolute paths
2157    /// under one checkout on one machine, dead for the only reader the copy exists for,
2158    /// while the destination held its own record for every one of them the whole time. This
2159    /// is what closes that, and it is deliberately **not** a Markdown-link parser: the
2160    /// artifact that motivated it holds bare absolute paths inside backticks in a table
2161    /// cell, which `[text](target)` matching would have left exactly as it found them.
2162    ///
2163    /// The correspondence is re-established from what the *destination* records at
2164    /// [`GlobalId::ORIGIN_KEY`], not from the mapping this copy holds. That mapping is not
2165    /// available when it is needed: `project copy` and `document copy` are separate verbs,
2166    /// one invocation carries one [`CopyScope`], and a project copy carries no documents —
2167    /// so by the time the document is copied, the tasks were written by a process that has
2168    /// exited. Reading the destination is also what makes a document copied on its own
2169    /// work, which a same-run mapping never could.
2170    ///
2171    // llmlint: ignore-block[contracts_have_one_source_or_a_drift_gate] This comment is required to state the rule; it is held by the journeys `a_rendering_whose_references_a_copy_rewrites_records_the_digest_of_what_it_was_given`, `a_copy_carries_provenance_it_cannot_vouch_for_verbatim` and `a_rendering_whose_references_a_copy_leaves_alone_carries_its_provenance_unchanged` in `crates/onetaskgraph/tests/e2e/rendered.rs`, and the entry's fields are `TemplateProvenance`'s own.
2172    /// Tasks and projects are not touched. A document's content is rewritten, and so is
2173    /// one thing beside it: when this substitutes at least one reference into a rendering
2174    /// that still hashes to the `body_digest` its [`TemplateProvenance`] records, the copy
2175    /// records as `body_digest` the digest of the rendering as this rewrote its references,
2176    /// carrying the entry's other three fields; any other entry, or none, is carried
2177    /// verbatim. The answers that rendering was made from, and the `answers_digest` naming
2178    /// them, still hold the pre-copy locations, so the copy is not a fresh rendering of them
2179    /// and a regenerate from them reproduces the pre-copy content.
2180    // llmlint: ignore-end[contracts_have_one_source_or_a_drift_gate]
2181    async fn rewrite_references(
2182        &self,
2183        planned: &mut [Planned],
2184        counts: &mut Counted,
2185    ) -> Result<(), EngineError> {
2186        // Read at the source, once per project rather than once per document: several
2187        // documents of one project is the ordinary case.
2188        let mut by_project: BTreeMap<String, Vec<Referent>> = BTreeMap::new();
2189        let mut named: Vec<Vec<Referent>> = Vec::new();
2190        for item in planned.iter() {
2191            named.push(self.named_referents(item, &mut by_project).await?);
2192        }
2193        // The destination is walked only for a copy that really names something, and only
2194        // for the interfaces those referents are read from — in each source a document of
2195        // this call lands in, which a routed copy decides per document.
2196        let mut levels: BTreeMap<SourceName, Vec<Level>> = BTreeMap::new();
2197        for (item, referents) in planned.iter().zip(&named) {
2198            for referent in referents {
2199                let wanted = levels.entry(item.to.clone()).or_default();
2200                if !wanted.contains(&referent.level) {
2201                    wanted.push(referent.level);
2202                }
2203            }
2204        }
2205        if levels.is_empty() {
2206            return Ok(());
2207        }
2208        let mut walked: BTreeMap<SourceName, Counterparts> = BTreeMap::new();
2209        for (to, levels) in levels {
2210            let destination = self.writable(&to)?;
2211            walked.insert(to, self.counterparts(destination, &levels).await?);
2212        }
2213        for (item, referents) in planned.iter_mut().zip(named) {
2214            let Some(counterparts) = walked.get(&item.to) else {
2215                continue;
2216            };
2217            let Item::Document(document) = &mut item.item else {
2218                continue;
2219            };
2220            let Some(content) = &document.content else {
2221                continue;
2222            };
2223            let (rewritten, made) = substitute(content, &table_for(&referents, counterparts));
2224            if made.rewritten > 0
2225                && let Some(provenance) = restamped(&document.metadata, content, &rewritten)
2226            {
2227                document
2228                    .metadata
2229                    .insert(TemplateProvenance::KEY.to_owned(), provenance.to_value());
2230            }
2231            document.content = Some(rewritten);
2232            counts.add(made);
2233        }
2234        Ok(())
2235    }
2236
2237    /// The records of one document's own project whose location string its content really
2238    /// holds, as a whole reference.
2239    ///
2240    /// A document with no content, or with no project at the source, names nothing: the
2241    /// referent set is the document's own project, its tasks and its other documents, and
2242    /// there is no such set without a project.
2243    async fn named_referents(
2244        &self,
2245        item: &Planned,
2246        by_project: &mut BTreeMap<String, Vec<Referent>>,
2247    ) -> Result<Vec<Referent>, EngineError> {
2248        let Item::Document(document) = &item.item else {
2249            return Ok(Vec::new());
2250        };
2251        let (Some(content), Some(project)) =
2252            (document.content.as_deref(), document.project.as_ref())
2253        else {
2254            return Ok(Vec::new());
2255        };
2256        if content.is_empty() {
2257            return Ok(Vec::new());
2258        }
2259        let project = GlobalId::new(item.source.source.clone(), project.clone());
2260        let key = project.to_string();
2261        if !by_project.contains_key(&key) {
2262            let read = self.referents(&project).await?;
2263            by_project.insert(key.clone(), read);
2264        }
2265        Ok(by_project[&key]
2266            .iter()
2267            // A document does not name itself: the referent set is every *other* record
2268            // filed under the project. Told by the interface as well as the id, because an
2269            // id alone does not identify a record — a folder of Markdown filing `A.md`
2270            // under both `tasks/` and `documents/` is the ordinary case rather than the
2271            // contrived one, and excluding by id alone would drop that task from the set.
2272            .filter(|referent| referent.level != Level::Document || referent.id != item.source)
2273            .filter(|referent| holds(content, &referent.location))
2274            .cloned()
2275            .collect())
2276    }
2277
2278    /// Every record filed under one project at its own source, with the location string
2279    /// that source reports for it.
2280    ///
2281    /// The project record itself, every task filed under it, and every document filed under
2282    /// it. All three are reads this engine already knows how to make.
2283    async fn referents(&self, project: &GlobalId) -> Result<Vec<Referent>, EngineError> {
2284        let source = self.readable(&project.source)?;
2285        let mut referents = Vec::new();
2286        if let Some(held) = source
2287            .source()
2288            .get_project(&project.native)
2289            .await
2290            .map_err(|error| refused(source, error))?
2291        {
2292            note(
2293                &mut referents,
2294                project.clone(),
2295                Level::Project,
2296                held.location.as_ref(),
2297                &held.metadata,
2298            );
2299        }
2300        for task in self.project_member_tasks(project).await? {
2301            note(
2302                &mut referents,
2303                task.id,
2304                Level::Task,
2305                task.item.location.as_ref(),
2306                &task.item.metadata,
2307            );
2308        }
2309        for document in self.project_documents(project).await? {
2310            note(
2311                &mut referents,
2312                document.id,
2313                Level::Document,
2314                document.item.location.as_ref(),
2315                &document.item.metadata,
2316            );
2317        }
2318        Ok(referents)
2319    }
2320
2321    /// Walk the destination once for every record it holds at the levels named, one page
2322    /// at a time.
2323    ///
2324    /// One page is *read* at a time, and what is kept from each is three fields of each
2325    /// record — its id, the origin it records and the location the destination reports —
2326    /// never the page. That is more than [`Engine::scan`] keeps, and deliberately: the whole
2327    /// point of this walk is that one pass answers every document and every referent of the
2328    /// invocation, so what it learns has to outlive the page it learned it from. Nothing is
2329    /// written down and the index is dropped with the call.
2330    ///
2331    /// The other difference from `scan` is the *answer*: this one keeps every match, so a
2332    /// destination holding two records for one work item is reported as ambiguous rather
2333    /// than resolved to the first.
2334    async fn counterparts(
2335        &self,
2336        destination: &ResolvedSource,
2337        levels: &[Level],
2338    ) -> Result<Counterparts, EngineError> {
2339        let mut found = Counterparts::default();
2340        for level in levels {
2341            // Every cursor this level has already been sent. `unrepeated` below catches a
2342            // source that hands back the cursor it was just given, and its own note says
2343            // why it catches no more than that: a source cycling through two cursors
2344            // advances on every page, and seeing it needs memory the walks that share that
2345            // helper do not keep. This walk does keep memory — it is building an index that
2346            // outlives each page — so here the memory exists and the cycle is caught. The
2347            // walks one level up catch the same defect as a page token handed back
2348            // unchanged; this one pages by the source's own cursor and has no level above
2349            // it, so nothing else would.
2350            let mut asked_before: BTreeSet<String> = BTreeSet::new();
2351            let mut cursor: Option<Cursor> = None;
2352            loop {
2353                if let Some(next) = &cursor
2354                    && !asked_before.insert(next.0.clone())
2355                {
2356                    return Err(refused(
2357                        destination,
2358                        SourceError::Malformed {
2359                            message: "the source returned a cursor it had already been \
2360                                      given while the destination was being walked for the \
2361                                      records a document's references name, so the walk \
2362                                      would never end"
2363                                .to_owned(),
2364                        },
2365                    ));
2366                }
2367                let asked = cursor.clone();
2368                let request = request_for(destination, cursor);
2369                let next = match level {
2370                    Level::Task => {
2371                        let page = destination
2372                            .source()
2373                            .query_tasks(&TaskQuery::default(), &request)
2374                            .await
2375                            .map_err(|error| refused(destination, error))?;
2376                        fits(page.items.len(), request.limit)
2377                            .map_err(|error| refused(destination, error))?;
2378                        for task in &page.items {
2379                            found.note(*level, &task.id, task.location.as_ref(), &task.metadata);
2380                        }
2381                        page.next
2382                    }
2383                    Level::Project => {
2384                        let page = destination
2385                            .source()
2386                            .query_projects(&ProjectQuery::default(), &request)
2387                            .await
2388                            .map_err(|error| refused(destination, error))?;
2389                        fits(page.items.len(), request.limit)
2390                            .map_err(|error| refused(destination, error))?;
2391                        for project in &page.items {
2392                            found.note(
2393                                *level,
2394                                &project.id,
2395                                project.location.as_ref(),
2396                                &project.metadata,
2397                            );
2398                        }
2399                        page.next
2400                    }
2401                    Level::Document => {
2402                        let page = destination
2403                            .source()
2404                            .query_documents(&DocumentQuery::default(), &request)
2405                            .await
2406                            .map_err(|error| refused(destination, error))?;
2407                        fits(page.items.len(), request.limit)
2408                            .map_err(|error| refused(destination, error))?;
2409                        for document in &page.items {
2410                            found.note(
2411                                *level,
2412                                &document.id,
2413                                document.location.as_ref(),
2414                                &document.metadata,
2415                            );
2416                        }
2417                        page.next
2418                    }
2419                };
2420                unrepeated(
2421                    next.as_ref(),
2422                    asked.as_ref(),
2423                    "the destination was being walked for the records a document's \
2424                     references name",
2425                )
2426                .map_err(|error| refused(destination, error))?;
2427                match next {
2428                    Some(next) => cursor = Some(next),
2429                    None => break,
2430                }
2431            }
2432        }
2433        Ok(found)
2434    }
2435
2436    /// Destination tasks filed under the copied project whose origin the source no longer
2437    /// holds.
2438    ///
2439    /// A copy never deletes, so each is left exactly as it is and reported.
2440    async fn orphans(
2441        &self,
2442        destination: &ResolvedSource,
2443        project: &GlobalId,
2444        at_destination: &NativeId,
2445        copied: &[GlobalId],
2446    ) -> Result<Vec<CopyOutcome>, EngineError> {
2447        let mut orphans = Vec::new();
2448        let mut cursor: Option<Cursor> = None;
2449        loop {
2450            let asked = cursor.clone();
2451            let request = request_for(destination, cursor);
2452            let page: Page<Task> = destination
2453                .source()
2454                .query_tasks(&TaskQuery::default(), &request)
2455                .await
2456                .map_err(|error| refused(destination, error))?;
2457            fits(page.items.len(), request.limit).map_err(|error| refused(destination, error))?;
2458            for task in &page.items {
2459                if task.project.as_ref() != Some(at_destination) {
2460                    continue;
2461                }
2462                let Some(origin) = origin_of(&task.metadata) else {
2463                    continue;
2464                };
2465                if origin.source != project.source || copied.contains(&origin) {
2466                    continue;
2467                }
2468                orphans.push(CopyOutcome {
2469                    source: origin,
2470                    action: CopyAction::Orphaned {
2471                        destination: GlobalId::new(destination.name().clone(), task.id.clone()),
2472                    },
2473                    placed: None,
2474                });
2475            }
2476            unrepeated(
2477                page.next.as_ref(),
2478                asked.as_ref(),
2479                "the destination was being read for items the copy left behind",
2480            )
2481            .map_err(|error| refused(destination, error))?;
2482            match page.next {
2483                Some(next) => cursor = Some(next),
2484                None => return Ok(orphans),
2485            }
2486        }
2487    }
2488
2489    /// Resolve and write every item planned, holding back the ones whose edges are not
2490    /// resolvable yet.
2491    ///
2492    /// An edge between two items of one copy can point at a member whose destination id
2493    /// does not exist until it has been created, so the item that points at it lands
2494    /// without that edge and is handed to `deferred`. [`Engine::repair`] finishes it once
2495    /// the *whole* request has landed — not once this call has, because the far end may
2496    /// be in another project of the same command.
2497    async fn copy_items(
2498        &self,
2499        request: &CopyRequest,
2500        mut planned: Vec<Planned>,
2501        project: Option<&Filing>,
2502        running: &mut Running,
2503        journal: &mut Journal,
2504    ) -> Result<Vec<CopyOutcome>, EngineError> {
2505        // Only a document's own content names other records, and only once every document
2506        // of this call has been read: the destination is walked once for all of them, and
2507        // the content the rest of this call lands is the rewritten one — which is what
2508        // makes a repeat copy of an already-rewritten document report `unchanged`.
2509        if planned
2510            .iter()
2511            .any(|item| item.item.level() == Level::Document)
2512        {
2513            self.rewrite_references(&mut planned, &mut running.references)
2514                .await?;
2515        }
2516
2517        for item in &planned {
2518            if let Target::Update { id, .. } = &item.target {
2519                running.counterparts.insert(
2520                    item.source.to_string(),
2521                    GlobalId::new(item.to.clone(), id.clone()),
2522                );
2523            }
2524            if let Item::Task(task) = &item.item {
2525                running.delivers_rewritten +=
2526                    members_named(&task.delivers, &item.source.source, &running.resolvable);
2527            }
2528        }
2529
2530        // Resolved once per item, and used by both passes: the repair pass writes the
2531        // same item again, and re-deriving this there could file it somewhere else.
2532        let mut filed = Vec::new();
2533        for item in &planned {
2534            filed.push(match (project, &item.item) {
2535                (_, Item::Project(_)) => None,
2536                (Some(filing), _) => filing.get(&item.to).cloned(),
2537                (None, _) => self.filed(request, item, None, running, journal).await?,
2538            });
2539        }
2540
2541        let mut outcomes = Vec::new();
2542        let mut unresolved = Vec::new();
2543        let mut priors = Vec::new();
2544        for (index, item) in planned.iter().enumerate() {
2545            let destination = self.writable(&item.to)?;
2546            let edges = mapped_edges(
2547                &item.edges,
2548                &item.source.source,
2549                &item.to,
2550                &running.resolvable,
2551                &running.counterparts,
2552            );
2553            let delivers = delivers_of(item, &item.to, &running.resolvable, &running.counterparts);
2554            if edges.iter().any(Option::is_none) || delivers.iter().any(Option::is_none) {
2555                unresolved.push(index);
2556            }
2557            let resolvable_delivers = resolved_entries(&delivers);
2558            let (outcome, prior) = self
2559                .land(
2560                    destination,
2561                    request,
2562                    item,
2563                    filed[index].clone(),
2564                    Pointing {
2565                        edges: &edges,
2566                        delivers: &resolvable_delivers,
2567                    },
2568                    journal,
2569                )
2570                .await?;
2571            if let Some(id) = outcome.destination() {
2572                running
2573                    .counterparts
2574                    .insert(item.source.to_string(), id.clone());
2575                if !request.dry_run {
2576                    running.linking.push(Linking {
2577                        item: item.source.clone(),
2578                        level: item.item.level(),
2579                        found: item.target.found(),
2580                        destination: id.clone(),
2581                        held: described(&item.item)
2582                            .1
2583                            .get(MetadataKey::COPIES_KEY)
2584                            .cloned(),
2585                    });
2586                }
2587            }
2588            if !request.dry_run
2589                && let (Item::Task(task), Some(landed)) = (&item.item, outcome.destination())
2590            {
2591                running.landed.push(LandedTask {
2592                    destination: landed.clone(),
2593                    origin: item.source.source.clone(),
2594                    delivers: task.delivers.clone(),
2595                    before: match prior.as_ref().map(|prior| &prior.item) {
2596                        Some(Item::Task(held)) => targets(&held.delivers, destination.name()),
2597                        _ => Vec::new(),
2598                    },
2599                    category: task.status.category,
2600                });
2601            }
2602            outcomes.push(outcome);
2603            priors.push(prior);
2604        }
2605
2606        if !request.dry_run {
2607            for (index, item) in planned.into_iter().enumerate() {
2608                if !unresolved.contains(&index) {
2609                    continue;
2610                }
2611                // Every item a copy that is not a dry run lands has a destination id: the
2612                // one outcome without one is a dry run that would have created, and this
2613                // block does not run for a dry run.
2614                let id = outcomes[index]
2615                    .destination()
2616                    .expect("a copy that writes lands every item it planned")
2617                    .clone();
2618                running.deferred.push(Deferred {
2619                    item,
2620                    filed: filed[index].clone(),
2621                    destination: id.native,
2622                    prior: priors[index].clone(),
2623                });
2624            }
2625        }
2626        Ok(outcomes)
2627    }
2628
2629    /// Read one item, place it, read its forward edges, and decide where it is going.
2630    ///
2631    /// A task is placed by its own repositories; a document goes with its project's home.
2632    async fn plan(
2633        &self,
2634        request: &CopyRequest,
2635        kind: Level,
2636        id: &GlobalId,
2637        running: &mut Running,
2638    ) -> Result<Planned, EngineError> {
2639        let read = self.read(kind, id).await?;
2640        let placement = match &read.item {
2641            Item::Task(task) => self.routes.place(&request.destination, &task.repositories),
2642            Item::Document(document) => {
2643                self.document_placement(request, &read.source, document, running)
2644                    .await?
2645            }
2646            Item::Project(project) => self
2647                .routes
2648                .place(&request.destination, &project.repositories),
2649        };
2650        self.aim(request, read, placement).await
2651    }
2652
2653    /// Read one item out of its own source.
2654    async fn read(&self, kind: Level, id: &GlobalId) -> Result<Read, EngineError> {
2655        let source = self.readable(&id.source)?;
2656        if kind == Level::Document {
2657            documentary(source)?;
2658        }
2659        let item = match kind {
2660            Level::Task => source
2661                .source()
2662                .get_task(&id.native)
2663                .await
2664                .map_err(|error| refused(source, error))?
2665                .map(|task| Item::Task(Box::new(task))),
2666            Level::Project => source
2667                .source()
2668                .get_project(&id.native)
2669                .await
2670                .map_err(|error| refused(source, error))?
2671                .map(|project| Item::Project(Box::new(project))),
2672            Level::Document => source
2673                .source()
2674                .get_document(&id.native)
2675                .await
2676                .map_err(|error| refused(source, error))?
2677                .map(|document| Item::Document(Box::new(document))),
2678        }
2679        .ok_or_else(|| EngineError::NoSuchItem { id: id.to_string() })?;
2680        Ok(Read {
2681            source: id.clone(),
2682            item,
2683        })
2684    }
2685
2686    /// Decide where one read item is going, once it is known which source it lands in.
2687    async fn aim(
2688        &self,
2689        request: &CopyRequest,
2690        read: Read,
2691        placement: Placement,
2692    ) -> Result<Planned, EngineError> {
2693        let Read { source: id, item } = read;
2694        let destination = self.writable(&placement.destination)?;
2695        if item.level() == Level::Document {
2696            documentary(destination)?;
2697        }
2698        // Before the destination is read, and so before it is written: a destination that
2699        // holds no priority has nowhere to put one, and every item of a copy is planned before
2700        // any of them lands. A task carrying `none` passes and writes exactly as it always did.
2701        if let Item::Task(task) = &item {
2702            holds_priority(destination, &id.to_string(), task.priority)?;
2703        }
2704        let routed = self.routes.routes(&request.destination);
2705        // A task or a document whose counterpart already sits somewhere a route could have
2706        // sent it, other than where it routes now, was moved by a repository changed after
2707        // the copy. That is a person's decision rather than a silent move, so it is refused
2708        // before anything is written. A project's home is exempt: it stays where it landed.
2709        if routed && item.level() != Level::Project {
2710            self.misrouted(request, &id, &item, &placement.destination)
2711                .await?;
2712        }
2713        let source = self.readable(&id.source)?;
2714        let edges = forward_edges(source, &id.native, item.level()).await?;
2715        let (target, held) = self.target(destination, request, &id, &item).await?;
2716        Ok(Planned {
2717            source: id,
2718            item,
2719            edges,
2720            target,
2721            held,
2722            to: placement.destination.clone(),
2723            placed: routed.then_some(placement),
2724        })
2725    }
2726
2727    /// Refuse an item whose existing counterpart, by its link or its origin, sits in a source
2728    /// its copy could reach other than `to`, the one it routes to now.
2729    async fn misrouted(
2730        &self,
2731        request: &CopyRequest,
2732        id: &GlobalId,
2733        item: &Item,
2734        to: &SourceName,
2735    ) -> Result<(), EngineError> {
2736        let metadata = described(item).1;
2737        for elsewhere in self.routes.reachable(&request.destination) {
2738            if &elsewhere == to {
2739                continue;
2740            }
2741            let there = self.writable(&elsewhere)?;
2742            let linked = match link_of(metadata, &elsewhere) {
2743                Some(link) => self
2744                    .prior(there, item.level(), &link.native)
2745                    .await?
2746                    .filter(|held| origin_of(described(&held.item).1).as_ref() == Some(id))
2747                    .map(|_| link),
2748                None => None,
2749            };
2750            let originated = match origin_of(metadata).filter(|origin| origin.source == elsewhere) {
2751                Some(origin) => self
2752                    .prior(there, item.level(), &origin.native)
2753                    .await?
2754                    .map(|_| origin),
2755                None => None,
2756            };
2757            if let Some(counterpart) = linked.or(originated) {
2758                return Err(EngineError::Misrouted {
2759                    item: id.clone(),
2760                    counterpart,
2761                    route: to.clone(),
2762                });
2763            }
2764        }
2765        Ok(())
2766    }
2767
2768    /// Where a document lands: with its project's home, wherever that is; by its own
2769    /// repositories when its project has none yet, or it is in no project.
2770    async fn document_placement(
2771        &self,
2772        request: &CopyRequest,
2773        id: &GlobalId,
2774        document: &Document,
2775        running: &mut Running,
2776    ) -> Result<Placement, EngineError> {
2777        if self.routes.routes(&request.destination)
2778            && let Some(project) = &document.project
2779        {
2780            let qualified = GlobalId::new(id.source.clone(), project.clone());
2781            if let Some(home) = self.find_home(request, &qualified, running).await? {
2782                return Ok(self.placed_at(request, &home.source));
2783            }
2784        }
2785        Ok(self
2786            .routes
2787            .place(&request.destination, &document.repositories))
2788    }
2789
2790    /// What the report says placed an item in `at`: the first route entry sending there, or
2791    /// none for the copy's own destination.
2792    fn placed_at(&self, request: &CopyRequest, at: &SourceName) -> Placement {
2793        Placement {
2794            destination: at.clone(),
2795            route: self
2796                .routes
2797                .of(&request.destination)
2798                .iter()
2799                .position(|route| route.to() == at)
2800                .map(|index| u32::try_from(index).unwrap_or(u32::MAX)),
2801        }
2802    }
2803
2804    /// Which destination item this one corresponds to, by the link the item records, the
2805    /// two origin rules and the caller's escape, and what the destination holds there.
2806    async fn target(
2807        &self,
2808        destination: &ResolvedSource,
2809        request: &CopyRequest,
2810        id: &GlobalId,
2811        item: &Item,
2812    ) -> Result<(Target, Option<Prior>), EngineError> {
2813        let (title, metadata) = described(item);
2814        // A caller asserting there is nothing to find is believed, and nothing is looked for —
2815        // unless the item itself says otherwise, by naming a counterpart at the destination.
2816        if request.create {
2817            let carrier = link_of(metadata, destination.name()).or_else(|| {
2818                origin_of(metadata).filter(|origin| &origin.source == destination.name())
2819            });
2820            if let Some(carrier) = carrier {
2821                return Err(EngineError::CreateCarried {
2822                    item: id.clone(),
2823                    carrier,
2824                });
2825            }
2826            return Ok((Target::Create, None));
2827        }
2828        // The link first, and it is believed only when the item it names still names this
2829        // one back: a person who re-pointed that item's origin has said it is not this
2830        // one's counterpart any more, so the link is ignored and rewritten to whatever the
2831        // rules below find.
2832        if let Some(link) = link_of(metadata, destination.name()) {
2833            match self.prior(destination, item.level(), &link.native).await? {
2834                Some(held) if origin_of(described(&held.item).1).as_ref() == Some(id) => {
2835                    return Ok((
2836                        Target::Update {
2837                            id: link.native,
2838                            found: Found::Link,
2839                        },
2840                        Some(held),
2841                    ));
2842                }
2843                Some(_) => {}
2844                None if request.recreate => {}
2845                None => {
2846                    return Err(EngineError::StaleLink {
2847                        item: id.to_string(),
2848                        link: link.to_string(),
2849                    });
2850                }
2851            }
2852        }
2853        if let Some(origin) = origin_of(metadata)
2854            && &origin.source == destination.name()
2855        {
2856            // The read that says the origin still names something is the read of what it
2857            // holds, so rule 1 costs one round trip rather than two.
2858            if let Some(held) = self
2859                .prior(destination, item.level(), &origin.native)
2860                .await?
2861            {
2862                return Ok((
2863                    Target::Update {
2864                        id: origin.native,
2865                        found: Found::Origin,
2866                    },
2867                    Some(held),
2868                ));
2869            }
2870            if !request.recreate {
2871                return Err(EngineError::StaleOrigin {
2872                    item: id.to_string(),
2873                    origin: origin.to_string(),
2874                });
2875            }
2876        }
2877        if let Some(found) = self
2878            .scan(destination, item.level(), &Wanted::Origin(id.to_string()))
2879            .await?
2880        {
2881            let held = self.prior(destination, item.level(), &found).await?;
2882            return Ok((
2883                Target::Update {
2884                    id: found,
2885                    found: Found::Scan,
2886                },
2887                held,
2888            ));
2889        }
2890        let wanted = match &request.match_by {
2891            Some(MatchBy::Title) => Some(Wanted::Title(title.to_owned())),
2892            Some(MatchBy::Metadata(key)) => metadata
2893                .get(key)
2894                .map(|value| Wanted::Metadata(key.clone(), value.clone())),
2895            None => None,
2896        };
2897        if let Some(wanted) = wanted
2898            && let Some(found) = self.scan(destination, item.level(), &wanted).await?
2899        {
2900            let held = self.prior(destination, item.level(), &found).await?;
2901            return Ok((
2902                Target::Update {
2903                    id: found,
2904                    found: Found::Match,
2905                },
2906                held,
2907            ));
2908        }
2909        Ok((Target::Create, None))
2910    }
2911
2912    /// Walk the destination one page at a time, looking for `wanted`.
2913    ///
2914    /// One page is held at a time and nothing is written down, which is the same bound
2915    /// every other compensation in this engine works under.
2916    async fn scan(
2917        &self,
2918        destination: &ResolvedSource,
2919        kind: Level,
2920        wanted: &Wanted,
2921    ) -> Result<Option<NativeId>, EngineError> {
2922        let mut cursor: Option<Cursor> = None;
2923        let tasks = wanted.task_query(&destination.source().capabilities());
2924        loop {
2925            let asked = cursor.clone();
2926            let request = request_for(destination, cursor);
2927            let next = match kind {
2928                Level::Task => {
2929                    let page = destination
2930                        .source()
2931                        .query_tasks(&tasks, &request)
2932                        .await
2933                        .map_err(|error| refused(destination, error))?;
2934                    fits(page.items.len(), request.limit)
2935                        .map_err(|error| refused(destination, error))?;
2936                    for task in &page.items {
2937                        if wanted.found(&task.title, &task.metadata) {
2938                            return Ok(Some(task.id.clone()));
2939                        }
2940                    }
2941                    page.next
2942                }
2943                Level::Project => {
2944                    let page = destination
2945                        .source()
2946                        .query_projects(&ProjectQuery::default(), &request)
2947                        .await
2948                        .map_err(|error| refused(destination, error))?;
2949                    fits(page.items.len(), request.limit)
2950                        .map_err(|error| refused(destination, error))?;
2951                    for project in &page.items {
2952                        if wanted.found(&project.title, &project.metadata) {
2953                            return Ok(Some(project.id.clone()));
2954                        }
2955                    }
2956                    page.next
2957                }
2958                Level::Document => {
2959                    let page = destination
2960                        .source()
2961                        .query_documents(&DocumentQuery::default(), &request)
2962                        .await
2963                        .map_err(|error| refused(destination, error))?;
2964                    fits(page.items.len(), request.limit)
2965                        .map_err(|error| refused(destination, error))?;
2966                    for document in &page.items {
2967                        if wanted.found(&document.title, &document.metadata) {
2968                            return Ok(Some(document.id.clone()));
2969                        }
2970                    }
2971                    page.next
2972                }
2973            };
2974            unrepeated(
2975                next.as_ref(),
2976                asked.as_ref(),
2977                "the destination was being scanned for the item to update",
2978            )
2979            .map_err(|error| refused(destination, error))?;
2980            match next {
2981                Some(next) => cursor = Some(next),
2982                None => return Ok(None),
2983            }
2984        }
2985    }
2986
2987    /// Write one planned item, or say what a dry run would have done.
2988    ///
2989    /// Answers with what the destination held there beforehand as well, which is what
2990    /// makes an item written twice restorable to what it was rather than to what this
2991    /// copy's first pass left.
2992    async fn land(
2993        &self,
2994        destination: &ResolvedSource,
2995        request: &CopyRequest,
2996        item: &Planned,
2997        project: Option<NativeId>,
2998        pointing: Pointing<'_>,
2999        journal: &mut Journal,
3000    ) -> Result<(CopyOutcome, Option<Prior>), EngineError> {
3001        let Pointing { edges, delivers } = pointing;
3002        let (target, found) = match &item.target {
3003            Target::Update { id, found } => (Some(id.clone()), Some(*found)),
3004            Target::Create => (None, None),
3005        };
3006        // Every link is settled once the whole copy has landed, by `Engine::link`, so none
3007        // is known here.
3008        let reached = |destination: Option<GlobalId>, changed: bool| match (found, destination) {
3009            (Some(via), Some(destination)) if changed => CopyAction::Updated {
3010                destination,
3011                via: via.via(),
3012                link: None,
3013            },
3014            (Some(via), Some(destination)) => CopyAction::Unchanged {
3015                destination,
3016                via: via.via(),
3017                link: None,
3018            },
3019            (_, destination) => CopyAction::Created {
3020                destination,
3021                via: NoCounterpart::Created,
3022                link: None,
3023            },
3024        };
3025        // The one read of the destination item, made where its target was found, used to
3026        // decide whether the write changes anything and — if the copy cannot finish — to
3027        // put that item back.
3028        let prior = item.held.clone();
3029        let edges = resolved(edges);
3030        let qualified = |native: NativeId| GlobalId::new(destination.name().clone(), native);
3031        if let Some(id) = &target
3032            && !changes(
3033                prior.as_ref(),
3034                item,
3035                id,
3036                &project,
3037                &edges,
3038                delivers,
3039                destination.name(),
3040            )
3041        {
3042            return Ok((
3043                CopyOutcome {
3044                    source: item.source.clone(),
3045                    action: reached(Some(qualified(id.clone())), false),
3046                    placed: item.placed.clone(),
3047                },
3048                prior,
3049            ));
3050        }
3051        if request.dry_run {
3052            return Ok((
3053                CopyOutcome {
3054                    source: item.source.clone(),
3055                    // Null only for a create here: nothing was created, so there is no id to
3056                    // report.
3057                    action: reached(target.map(qualified), true),
3058                    placed: item.placed.clone(),
3059                },
3060                prior,
3061            ));
3062        }
3063        let written = qualified(
3064            self.write(
3065                destination,
3066                item,
3067                target,
3068                project,
3069                &edges,
3070                delivers,
3071                prior.clone(),
3072                journal,
3073            )
3074            .await?,
3075        );
3076        Ok((
3077            CopyOutcome {
3078                source: item.source.clone(),
3079                action: reached(Some(written), true),
3080                placed: item.placed.clone(),
3081            },
3082            prior,
3083        ))
3084    }
3085
3086    /// Which destination project this item is filed under, when it is filed at all.
3087    ///
3088    /// A task copied as part of a project copy is filed under that project's counterpart,
3089    /// which the copy has just established. A task copied on its own has to find it.
3090    async fn filed(
3091        &self,
3092        request: &CopyRequest,
3093        item: &Planned,
3094        project: Option<NativeId>,
3095        running: &mut Running,
3096        journal: &mut Journal,
3097    ) -> Result<Option<NativeId>, EngineError> {
3098        match (&item.item, project) {
3099            (Item::Task(task), None) => {
3100                self.counterpart(request, item, task.project.as_ref(), running, journal)
3101                    .await
3102            }
3103            (Item::Document(document), None) => {
3104                self.counterpart(request, item, document.project.as_ref(), running, journal)
3105                    .await
3106            }
3107            (Item::Task(_) | Item::Document(_), filed) => Ok(filed),
3108            (Item::Project(_), _) => Ok(None),
3109        }
3110    }
3111
3112    /// The destination project this task's own project corresponds to, when there is one.
3113    ///
3114    /// A task copied on its own keeps its source's project id when the destination holds
3115    /// no counterpart: the field is opaque to this engine, and dropping it would lose
3116    /// what the source said.
3117    ///
3118    /// Looked for once per source project and landing source per command. A project this
3119    /// command itself copied is already known, and one an earlier task of this command was
3120    /// filed under was already looked for; walking the destination again for either would
3121    /// spend a scan per task on an answer the command holds. A project whose own link names
3122    /// its counterpart there is found by that link — see [`Engine::linked_project`] — and
3123    /// the destination is walked only when it does not.
3124    ///
3125    /// A routed item whose project's home landed in another source than it does is filed
3126    /// under that home's member project in its own source — see [`Engine::member_in`].
3127    async fn counterpart(
3128        &self,
3129        request: &CopyRequest,
3130        item: &Planned,
3131        project: Option<&NativeId>,
3132        running: &mut Running,
3133        journal: &mut Journal,
3134    ) -> Result<Option<NativeId>, EngineError> {
3135        let Some(project) = project else {
3136            return Ok(None);
3137        };
3138        let qualified = GlobalId::new(item.source.source.clone(), project.clone());
3139        let key = (qualified.to_string(), item.to.clone());
3140        if let Some(looked) = running.filings.get(&key) {
3141            return Ok(looked.clone());
3142        }
3143        let filed = match self.find_home(request, &qualified, running).await? {
3144            Some(home) if home.source == item.to => Some(home.native),
3145            Some(home) => {
3146                self.member_in(request.dry_run, &home, &item.to, running, journal)
3147                    .await?
3148            }
3149            None => Some(project.clone()),
3150        };
3151        running.filings.insert(key, filed.clone());
3152        Ok(filed)
3153    }
3154
3155    /// The counterpart a source project has in some source this copy can reach — its home —
3156    /// when it has one: the one this command landed, the one its own link names, or the one a
3157    /// walk of each reachable source finds recording it as its origin.
3158    async fn find_home(
3159        &self,
3160        request: &CopyRequest,
3161        project: &GlobalId,
3162        running: &Running,
3163    ) -> Result<Option<GlobalId>, EngineError> {
3164        if let Some(landed) = running.counterparts.get(&project.to_string()) {
3165            return Ok(Some(landed.clone()));
3166        }
3167        if let Some(linked) = self.linked_project(request, project).await? {
3168            return Ok(Some(linked));
3169        }
3170        for reachable in self.routes.reachable(&request.destination) {
3171            let there = self.writable(&reachable)?;
3172            if let Some(found) = self
3173                .scan(there, Level::Project, &Wanted::Origin(project.to_string()))
3174                .await?
3175            {
3176                return Ok(Some(GlobalId::new(reachable, found)));
3177            }
3178        }
3179        Ok(None)
3180    }
3181
3182    /// The member project `home` names in `to`, creating it when it names none there.
3183    ///
3184    /// Found through the home's `onetaskgraph.members`, read once per command. A member it
3185    /// names that `to` no longer holds is replaced rather than refused: the member exists
3186    /// only to file the home's routed tasks, and nothing a person wrote lives on it. A dry
3187    /// run creates nothing, so a member it would create files its tasks under nothing.
3188    async fn member_in(
3189        &self,
3190        dry_run: bool,
3191        home: &GlobalId,
3192        to: &SourceName,
3193        running: &mut Running,
3194        journal: &mut Journal,
3195    ) -> Result<Option<NativeId>, EngineError> {
3196        let key = home.to_string();
3197        if !running.homes.contains_key(&key) {
3198            let at = self.writable(&home.source)?;
3199            let Some(held) = at
3200                .source()
3201                .get_project(&home.native)
3202                .await
3203                .map_err(|error| refused(at, error))?
3204            else {
3205                return Ok(None);
3206            };
3207            running
3208                .homes
3209                .insert(key.clone(), Home::of(home.clone(), &held)?);
3210        }
3211        let named = running.homes[&key]
3212            .members
3213            .iter()
3214            .find(|member| &member.source == to)
3215            .cloned();
3216        let there = self.writable(to)?;
3217        if let Some(member) = named
3218            && there
3219                .source()
3220                .get_project(&member.native)
3221                .await
3222                .map_err(|error| refused(there, error))?
3223                .is_some()
3224        {
3225            return Ok(Some(member.native));
3226        }
3227        if dry_run {
3228            return Ok(None);
3229        }
3230        let held = &running.homes[&key];
3231        let member = Project {
3232            id: home.native.clone(),
3233            title: held.title.clone(),
3234            content: None,
3235            status: held.status.clone(),
3236            labels: Vec::new(),
3237            url: None,
3238            location: None,
3239            created_at: None,
3240            updated_at: None,
3241            metadata: BTreeMap::from([(
3242                MetadataKey::MEMBER_OF_KEY.to_owned(),
3243                Value::String(home.to_string()),
3244            )]),
3245            repositories: Vec::new(),
3246        };
3247        let created = there
3248            .source()
3249            .write_project(&ItemWrite {
3250                target: None,
3251                item: member,
3252                depends_on: Vec::new(),
3253            })
3254            .await
3255            .map_err(|error| refused(there, error))?;
3256        journal.record(Undo::Created {
3257            at: to.clone(),
3258            kind: Level::Project,
3259            id: created.clone(),
3260        });
3261        let entry = running
3262            .homes
3263            .get_mut(&key)
3264            .expect("the home was recorded above");
3265        entry.members.retain(|member| &member.source != to);
3266        entry
3267            .members
3268            .push(GlobalId::new(to.clone(), created.clone()));
3269        entry.grew = true;
3270        Ok(Some(created))
3271    }
3272
3273    /// Write each home's grown `onetaskgraph.members` through its own project write, once the
3274    /// whole copy has landed.
3275    ///
3276    /// The home is read as it now stands and written back with that one entry changed, and
3277    /// the read is what the journal restores when the copy cannot finish — unless the copy
3278    /// already holds an earlier entry for it, which is what it was before this copy began.
3279    async fn record_members(
3280        &self,
3281        homes: BTreeMap<String, Home>,
3282        journal: &mut Journal,
3283    ) -> Result<(), EngineError> {
3284        for home in homes.into_values().filter(|home| home.grew) {
3285            let at = self.writable(&home.id.source)?;
3286            let Some(prior) = self.prior(at, Level::Project, &home.id.native).await? else {
3287                continue;
3288            };
3289            let Item::Project(mut project) = prior.item.clone() else {
3290                continue;
3291            };
3292            project.metadata.insert(
3293                MetadataKey::MEMBERS_KEY.to_owned(),
3294                Value::Array(
3295                    home.members
3296                        .iter()
3297                        .map(|member| Value::String(member.to_string()))
3298                        .collect(),
3299                ),
3300            );
3301            let edges = prior.edges.clone();
3302            journal.record(Undo::Updated {
3303                at: home.id.source.clone(),
3304                id: home.id.native.clone(),
3305                prior,
3306            });
3307            at.source()
3308                .write_project(&ItemWrite {
3309                    target: Some(home.id.native.clone()),
3310                    item: *project,
3311                    depends_on: edges,
3312                })
3313                .await
3314                .map_err(|error| refused(at, error))?;
3315        }
3316        Ok(())
3317    }
3318
3319    /// The home project a source project's own `onetaskgraph.copies` names in a source this
3320    /// copy can reach, when the project it names there still records that source project as
3321    /// its origin.
3322    ///
3323    /// The filing lookup's half of following a link: one read of the project at its source
3324    /// and one of its counterpart by id, in place of a walk of the destination's projects.
3325    /// A link that names nothing there, or names a project somebody re-pointed, is not a
3326    /// refusal here as it is for a copy's own target — this only decides where an item is
3327    /// filed — so the walk answers instead, exactly as it did before there were links.
3328    async fn linked_project(
3329        &self,
3330        request: &CopyRequest,
3331        project: &GlobalId,
3332    ) -> Result<Option<GlobalId>, EngineError> {
3333        let source = self.readable(&project.source)?;
3334        if !source.source().capabilities().projects.is_native() {
3335            return Ok(None);
3336        }
3337        let Some(held) = source
3338            .source()
3339            .get_project(&project.native)
3340            .await
3341            .map_err(|error| refused(source, error))?
3342        else {
3343            return Ok(None);
3344        };
3345        for reachable in self.routes.reachable(&request.destination) {
3346            let Some(link) = link_of(&held.metadata, &reachable) else {
3347                continue;
3348            };
3349            let there = self.writable(&reachable)?;
3350            let named = there
3351                .source()
3352                .get_project(&link.native)
3353                .await
3354                .map_err(|error| refused(there, error))?;
3355            if named.is_some_and(|there| origin_of(&there.metadata).as_ref() == Some(project)) {
3356                return Ok(Some(link));
3357            }
3358        }
3359        Ok(None)
3360    }
3361
3362    /// What the destination holds at one id, item and forward edges together.
3363    ///
3364    /// One read for both purposes it serves — deciding whether a write changes anything,
3365    /// and putting the item back if the copy cannot finish — because a second read of the
3366    /// same item is a second round trip against a hosted destination for nothing.
3367    async fn prior(
3368        &self,
3369        destination: &ResolvedSource,
3370        kind: Level,
3371        id: &NativeId,
3372    ) -> Result<Option<Prior>, EngineError> {
3373        let held = match kind {
3374            Level::Task => destination
3375                .source()
3376                .get_task(id)
3377                .await
3378                .map_err(|error| refused(destination, error))?
3379                .map(|task| Item::Task(Box::new(task))),
3380            Level::Project => destination
3381                .source()
3382                .get_project(id)
3383                .await
3384                .map_err(|error| refused(destination, error))?
3385                .map(|project| Item::Project(Box::new(project))),
3386            Level::Document => destination
3387                .source()
3388                .get_document(id)
3389                .await
3390                .map_err(|error| refused(destination, error))?
3391                .map(|document| Item::Document(Box::new(document))),
3392        };
3393        let Some(item) = held else {
3394            return Ok(None);
3395        };
3396        let edges = forward_edges(destination, id, kind).await?;
3397        Ok(Some(Prior { item, edges }))
3398    }
3399
3400    /// Hand one item to the destination's own write interface, recording how to take it
3401    /// back.
3402    // llmlint: ignore[suppressions_justified] A write is the item, where it is going, what
3403    // it is filed under, its edges, what was there before and the journal that records how
3404    // to put it back. Each is a distinct decision made by a different part of the copy, and
3405    // grouping them would only move the argument list to a constructor.
3406    #[allow(clippy::too_many_arguments)]
3407    async fn write(
3408        &self,
3409        destination: &ResolvedSource,
3410        item: &Planned,
3411        target: Option<NativeId>,
3412        project: Option<NativeId>,
3413        edges: &[DependencyEdge],
3414        delivers: &[TaskRef],
3415        prior: Option<Prior>,
3416        journal: &mut Journal,
3417    ) -> Result<NativeId, EngineError> {
3418        let created_kind = item.item.level();
3419        let suggested = target
3420            .clone()
3421            .unwrap_or_else(|| created_id(&item.item, project.as_ref()));
3422        // Settled before the journal takes `prior`, and from that same read: what the
3423        // destination holds at the origin key is what a copy-back leaves there, and what it
3424        // holds as `delivered_by` is what the item keeps.
3425        let origin = recorded(item, prior.as_ref());
3426        let landing = outgoing(item, suggested, project, &origin, delivers, prior.as_ref());
3427        // Recorded *before* the write rather than after it. A destination's own write is
3428        // several calls — `docs/plugin-protocol.md` §4.9 — and one of them failing leaves
3429        // the ones before it applied. No source can put those back, because only this
3430        // journal holds what was there; recorded after a successful write, an update that
3431        // stopped part way was the one way a copy could end and leave the destination
3432        // altered. A restore of an item the write never reached rewrites what is already
3433        // there, which costs one mutation and is what "either complete or it never
3434        // happened" is worth.
3435        if let (Some(id), Some(prior)) = (target.clone(), prior) {
3436            journal.record(Undo::Updated {
3437                at: destination.name().clone(),
3438                id,
3439                prior,
3440            });
3441        }
3442        let landed = match landing {
3443            Item::Task(task) => destination
3444                .source()
3445                .write_task(&ItemWrite {
3446                    target: target.clone(),
3447                    item: *task,
3448                    depends_on: edges.to_vec(),
3449                })
3450                .await
3451                .map_err(|error| refused(destination, error))?,
3452            Item::Project(project) => destination
3453                .source()
3454                .write_project(&ItemWrite {
3455                    target: target.clone(),
3456                    item: *project,
3457                    depends_on: edges.to_vec(),
3458                })
3459                .await
3460                .map_err(|error| refused(destination, error))?,
3461            // No edges, and that is the contract: a document takes part in no dependency
3462            // graph, so there is nothing here for `depends_on` to carry.
3463            Item::Document(document) => destination
3464                .source()
3465                .write_document(&ItemWrite {
3466                    target: target.clone(),
3467                    item: *document,
3468                    depends_on: Vec::new(),
3469                })
3470                .await
3471                .map_err(|error| refused(destination, error))?,
3472        };
3473        // A created item can only be journalled here: its id is what the write answers
3474        // with. A create that fails leaves nothing behind — §4.9 makes taking the item
3475        // back the source's own duty, because a write that refused must not leave an item
3476        // nobody asked for.
3477        if target.is_none() {
3478            journal.record(Undo::Created {
3479                at: destination.name().clone(),
3480                kind: created_kind,
3481                id: landed.clone(),
3482            });
3483        }
3484        Ok(landed)
3485    }
3486
3487    /// A configured source that built, for reading an item out of.
3488    fn readable(&self, name: &SourceName) -> Result<&ResolvedSource, EngineError> {
3489        let name = self.known(name)?;
3490        if let Some(unavailable) = self.unavailable().find(|source| source.name() == &name) {
3491            return Err(EngineError::SourceRefused {
3492                name: name.to_string(),
3493                error: unavailable.error().clone(),
3494            });
3495        }
3496        self.ready()
3497            .find(|source| source.name() == &name)
3498            .ok_or(EngineError::NoSources)
3499    }
3500}
3501
3502/// How many tasks of a project are read at once while walking it.
3503const PROJECT_PAGE: std::num::NonZeroU32 = std::num::NonZeroU32::new(50).expect("50 is not zero");
3504
3505/// Each source's own running totals, in the order the sources were given.
3506///
3507/// A reading a source could not take is read as that source not metering: what a command
3508/// spent is a report about the work, and a failed reading must not become a failure of the
3509/// work itself. That holds when only one of a source's two readings failed as well, since
3510/// a difference needs both ends.
3511pub(super) async fn readings(sources: &[&ResolvedSource]) -> Vec<Option<Metering>> {
3512    let mut read = Vec::with_capacity(sources.len());
3513    for source in sources {
3514        read.push(source.source().metering().await.ok().flatten());
3515    }
3516    read
3517}
3518
3519/// What the sources spent between two readings of them, or `None` when none of them meters.
3520///
3521/// A source that does not meter contributes nothing and does not make the total zero: a
3522/// command whose sources all declined reports that it cannot say, not that it spent nothing.
3523/// A budget is keyed by its name and unit together, so two sources naming one budget add up
3524/// and two units of one budget stay apart.
3525///
3526/// A source's two readings are a plugin's word, so they are held to [`Metering`]'s contract
3527/// before either is believed, and a pair that breaks it is that source not metering — see
3528/// [`difference`].
3529pub(super) fn spent_between(
3530    before: &[Option<Metering>],
3531    after: &[Option<Metering>],
3532) -> Option<Spent> {
3533    let mut metered = false;
3534    let mut requests = 0_u64;
3535    let mut budgets: BTreeMap<(String, String), (u64, u64)> = BTreeMap::new();
3536    for (before, after) in before.iter().zip(after) {
3537        let (Some(before), Some(after)) = (before, after) else {
3538            continue;
3539        };
3540        let Some((sent, spent)) = difference(before, after) else {
3541            continue;
3542        };
3543        metered = true;
3544        requests = requests.saturating_add(sent);
3545        for (key, measured, modelled) in spent {
3546            let total = budgets.entry(key).or_default();
3547            total.0 = total.0.saturating_add(measured).saturating_add(modelled);
3548            total.1 = total.1.saturating_add(modelled);
3549        }
3550    }
3551    metered.then(|| Spent {
3552        requests,
3553        budgets: budgets
3554            .into_iter()
3555            .map(|((budget, unit), (amount, modelled))| BudgetSpent {
3556                budget,
3557                unit,
3558                amount,
3559                lower_bound: modelled > 0,
3560            })
3561            .collect(),
3562    })
3563}
3564
3565/// One budget's name and unit, and the measured and modelled amounts spent against it.
3566type BudgetDifference = ((String, String), u64, u64);
3567
3568/// What one source sent and spent between two of its readings, or `None` when the pair
3569/// cannot be a running total's.
3570///
3571/// A running total never falls, never drops a budget it has named, and names every budget
3572/// it keeps, once; a pair that breaks any of that is a source that reset its figures or
3573/// reported something else, and a difference taken over it would be a number that measures
3574/// nothing. Such a source is reported as not metering rather than as having spent a clamped
3575/// zero.
3576fn difference(before: &Metering, after: &Metering) -> Option<(u64, Vec<BudgetDifference>)> {
3577    let sent = after.requests.checked_sub(before.requests)?;
3578    for reading in [before, after] {
3579        let mut named = BTreeSet::new();
3580        for budget in &reading.budgets {
3581            if budget.budget.is_empty()
3582                || budget.unit.is_empty()
3583                || !named.insert((&budget.budget, &budget.unit))
3584            {
3585                return None;
3586            }
3587        }
3588    }
3589    let held = |from: &Metering, budget: &onetaskgraph_plugin_api::Metered| {
3590        from.budgets
3591            .iter()
3592            .find(|held| held.budget == budget.budget && held.unit == budget.unit)
3593            .cloned()
3594    };
3595    if before
3596        .budgets
3597        .iter()
3598        .any(|budget| held(after, budget).is_none())
3599    {
3600        return None;
3601    }
3602    let mut spent = Vec::with_capacity(after.budgets.len());
3603    for budget in &after.budgets {
3604        let earlier = held(before, budget);
3605        let measured = budget
3606            .measured
3607            .checked_sub(earlier.as_ref().map_or(0, |held| held.measured))?;
3608        let modelled = budget
3609            .modelled
3610            .checked_sub(earlier.as_ref().map_or(0, |held| held.modelled))?;
3611        spent.push((
3612            (budget.budget.clone(), budget.unit.clone()),
3613            measured,
3614            modelled,
3615        ));
3616    }
3617    Some((sent, spent))
3618}
3619
3620/// One page request against `source`, at the largest page it will serve.
3621fn request_for(source: &ResolvedSource, cursor: Option<Cursor>) -> PageRequest {
3622    PageRequest {
3623        cursor,
3624        limit: source.source().capabilities().max_page_size.max(1),
3625    }
3626}
3627
3628/// Whether writing this item would change what the destination already holds.
3629///
3630/// A free function over the state already read rather than a method that reads it again:
3631/// the same answer is wanted where the item is landed and where a repeat copy of a project
3632/// decides whether it settled, and a second read there is a second round trip for nothing.
3633fn changes(
3634    held: Option<&Prior>,
3635    item: &Planned,
3636    target: &NativeId,
3637    project: &Option<NativeId>,
3638    edges: &[DependencyEdge],
3639    delivers: &[TaskRef],
3640    destination: &SourceName,
3641) -> bool {
3642    let Some(held) = held else {
3643        return true;
3644    };
3645    let outgoing = outgoing(
3646        item,
3647        target.clone(),
3648        project.clone(),
3649        &recorded(item, Some(held)),
3650        delivers,
3651        Some(held),
3652    );
3653    !same(&held.item, &outgoing, destination) || !same_edges(&held.edges, edges)
3654}
3655
3656/// Remove one item this copy created, through the destination's own write interface.
3657async fn remove(
3658    destination: &ResolvedSource,
3659    kind: Level,
3660    id: &NativeId,
3661) -> Result<(), SourceError> {
3662    match kind {
3663        Level::Task => destination.source().delete_task(id).await,
3664        Level::Project => destination.source().delete_project(id).await,
3665        Level::Document => destination.source().delete_document(id).await,
3666    }
3667}
3668
3669/// Write one item back exactly as the destination held it before this copy.
3670async fn restore(
3671    destination: &ResolvedSource,
3672    id: &NativeId,
3673    prior: &Prior,
3674) -> Result<(), SourceError> {
3675    match &prior.item {
3676        Item::Task(task) => destination
3677            .source()
3678            .write_task(&ItemWrite {
3679                target: Some(id.clone()),
3680                item: (**task).clone(),
3681                depends_on: prior.edges.clone(),
3682            })
3683            .await
3684            .map(|_| ()),
3685        Item::Project(project) => destination
3686            .source()
3687            .write_project(&ItemWrite {
3688                target: Some(id.clone()),
3689                item: (**project).clone(),
3690                depends_on: prior.edges.clone(),
3691            })
3692            .await
3693            .map(|_| ()),
3694        Item::Document(document) => destination
3695            .source()
3696            .write_document(&ItemWrite {
3697                target: Some(id.clone()),
3698                item: (**document).clone(),
3699                depends_on: Vec::new(),
3700            })
3701            .await
3702            .map(|_| ()),
3703    }
3704}
3705
3706/// Hold `value` at `onetaskgraph.copies` on one item, through its source's narrow metadata
3707/// write — `false` when the source holds no such item.
3708async fn set_link(
3709    source: &ResolvedSource,
3710    level: Level,
3711    id: &NativeId,
3712    value: &Value,
3713) -> Result<bool, SourceError> {
3714    let key = MetadataKey::copies();
3715    Ok(match level {
3716        Level::Task => source
3717            .source()
3718            .set_task_metadata(id, &key, value)
3719            .await?
3720            .is_some(),
3721        Level::Project => source
3722            .source()
3723            .set_project_metadata(id, &key, value)
3724            .await?
3725            .is_some(),
3726        Level::Document => source
3727            .source()
3728            .set_document_metadata(id, &key, value)
3729            .await?
3730            .is_some(),
3731    })
3732}
3733
3734/// The metadata of either kind of item, to edit.
3735fn metadata_of(item: &mut Item) -> &mut BTreeMap<String, Value> {
3736    match item {
3737        Item::Task(task) => &mut task.metadata,
3738        Item::Project(project) => &mut project.metadata,
3739        Item::Document(document) => &mut document.metadata,
3740    }
3741}
3742
3743/// Refuse a document copy addressed to a source that declares it has none.
3744///
3745/// Read off the declaration rather than by asking, which is what "not asked" means: the
3746/// engine learned at the handshake that this source holds no documents, so it refuses
3747/// naming the source and its plugin instead of sending a read that would be refused there.
3748/// Applied at both ends of a copy — a source with no documents holds nothing to copy out,
3749/// and a destination with none has nowhere to put one.
3750fn documentary(source: &ResolvedSource) -> Result<(), EngineError> {
3751    if source.source().capabilities().documents.is_native() {
3752        return Ok(());
3753    }
3754    Err(EngineError::NoDocuments {
3755        name: source.name().to_string(),
3756        kind: source.kind().to_owned(),
3757    })
3758}
3759
3760/// One source failing while a copy was mid-flight.
3761fn refused(source: &ResolvedSource, error: SourceError) -> EngineError {
3762    EngineError::SourceRefused {
3763        name: source.name().to_string(),
3764        error,
3765    }
3766}
3767
3768/// Every forward edge at one item, walked to exhaustion one page at a time.
3769async fn forward_edges(
3770    source: &ResolvedSource,
3771    id: &NativeId,
3772    kind: Level,
3773) -> Result<Vec<DependencyEdge>, EngineError> {
3774    // A document has no edges to walk, and asking for them would mean asking a source for
3775    // a graph the contract says nothing may point into.
3776    if kind == Level::Document {
3777        return Ok(Vec::new());
3778    }
3779    let mut edges = Vec::new();
3780    let mut cursor: Option<Cursor> = None;
3781    loop {
3782        let asked = cursor.clone();
3783        let request = request_for(source, cursor);
3784        let page = match kind {
3785            Level::Task | Level::Document => {
3786                source
3787                    .source()
3788                    .task_dependencies(id, Direction::DependsOn, &request)
3789                    .await
3790            }
3791            Level::Project => {
3792                source
3793                    .source()
3794                    .project_dependencies(id, Direction::DependsOn, &request)
3795                    .await
3796            }
3797        }
3798        .map_err(|error| refused(source, error))?;
3799        fits(page.items.len(), request.limit).map_err(|error| refused(source, error))?;
3800        edges.extend(page.items);
3801        unrepeated(
3802            page.next.as_ref(),
3803            asked.as_ref(),
3804            "an item's dependencies were being read for a copy",
3805        )
3806        .map_err(|error| refused(source, error))?;
3807        match page.next {
3808            Some(next) => cursor = Some(next),
3809            None => return Ok(edges),
3810        }
3811    }
3812}
3813
3814/// The location string one record reports, when it reports a usable one.
3815///
3816/// Either variant's own `String`, and `None` for a record the source gave no location for
3817/// or gave an empty string for: there is nothing to look for in a document's content and
3818/// nothing to point a reader at.
3819fn located(location: Option<&Location>) -> Option<String> {
3820    let (Location::Path(held) | Location::Url(held)) = location?;
3821    (!held.is_empty()).then(|| held.clone())
3822}
3823
3824/// Record one candidate referent, when its source said where it is.
3825fn note(
3826    into: &mut Vec<Referent>,
3827    id: GlobalId,
3828    level: Level,
3829    location: Option<&Location>,
3830    metadata: &BTreeMap<String, Value>,
3831) {
3832    if let Some(location) = located(location) {
3833        into.push(Referent {
3834            id,
3835            origin: origin_of(metadata),
3836            level,
3837            location,
3838        });
3839    }
3840}
3841
3842/// What every location string this document names becomes, longest first.
3843///
3844/// Longest first because a shorter location may start where a longer one does — a
3845/// project's directory and a task's file under it — and the longer of the two is the
3846/// record that occurrence names.
3847fn table_for(referents: &[Referent], counterparts: &Counterparts) -> Vec<(String, Resolution)> {
3848    let mut table: Vec<(String, Resolution)> = Vec::new();
3849    for referent in referents {
3850        // Two referents reporting one location string: an occurrence of it cannot be
3851        // attributed to either, and a rewrite would be *confidently wrong* rather than
3852        // merely unhelpful. So neither is chosen and both occurrences are counted.
3853        if let Some(held) = table
3854            .iter_mut()
3855            .find(|(location, _)| location == &referent.location)
3856        {
3857            held.1 = Resolution::Ambiguous;
3858            continue;
3859        }
3860        table.push((referent.location.clone(), counterparts.resolve(referent)));
3861    }
3862    table.sort_by_key(|(location, _)| std::cmp::Reverse(location.len()));
3863    table
3864}
3865
3866/// Whether `content` holds `location` at least once, stopped on both sides.
3867fn holds(content: &str, location: &str) -> bool {
3868    (0..content.len()).any(|at| delimited_at(content, at, location))
3869}
3870
3871/// Whether `location` occurs at `at` **stopped on both sides** — by
3872/// [`stops_a_location`], or by the end of the content — rather than as part of a longer
3873/// location-like string.
3874///
3875/// A location string occurring inside a longer one is a different string naming a
3876/// different record: `/…/tasks/p/t.md` must not be rewritten inside `/…/tasks/p/t.md.bak`,
3877/// `https://example.invalid/1` must not be rewritten inside `https://example.invalid/12`,
3878/// and a project's location that is a directory prefix of a task's must not be rewritten
3879/// inside that task's. What deciding it this way costs is stated on [`stops_a_location`].
3880fn delimited_at(content: &str, at: usize, location: &str) -> bool {
3881    if !content.is_char_boundary(at) || !content[at..].starts_with(location) {
3882        return false;
3883    }
3884    let before = content[..at].chars().next_back();
3885    let after = content[at + location.len()..].chars().next();
3886    stops_a_location(before) && stops_a_location(after)
3887}
3888
3889/// Whether a character cannot continue a path or a link, so a location string beside one
3890/// ends there.
3891///
3892/// Stated as what *stops* a location rather than as what one may contain, because the
3893/// second list is unbounded — a path may hold very nearly any byte, and a URL more. Every
3894/// character not named here continues, which is what leaves the three cases above alone;
3895/// the end of the content counts as a stop. The set is what the artifact this exists for
3896/// really wraps a bare path in — a backtick in a table cell — plus the delimiters prose
3897/// and Markdown put next to one.
3898///
3899/// **Sentence punctuation is deliberately absent, and that is a stated cost rather than an
3900/// oversight.** `.`, `!`, `?` and `:` each equally *continue* a real location — `/…/t.md`
3901/// and `/…/t.md.bak` are two files, `…/1` and `…/1?q=2` two pages — so admitting them as
3902/// stops would rewrite one record's location into another's. The price is that a location
3903/// written bare at the end of a sentence is not recognised at all: its text is left
3904/// byte-for-byte and it is counted in neither figure, exactly as a reference to another
3905/// project's record is. That is the direction to be wrong in, because this edits the
3906/// content of somebody's document, where a confidently wrong rewrite is worse than one
3907/// that never happens.
3908fn stops_a_location(character: Option<char>) -> bool {
3909    match character {
3910        None => true,
3911        Some(character) => character.is_whitespace() || "`\"'()[]{}<>|,;".contains(character),
3912    }
3913}
3914
3915/// One document's content with every whole reference rewritten, and what that took.
3916///
3917/// A location string with no confident counterpart is left **byte-for-byte** as it was
3918/// rather than removed or guessed at, and so is every character of the content that is not
3919/// a rewritten reference. Nothing is added and nothing is reformatted.
3920fn substitute(content: &str, table: &[(String, Resolution)]) -> (String, Counted) {
3921    let mut written = String::with_capacity(content.len());
3922    let mut counts = Counted::default();
3923    let mut at = 0;
3924    while at < content.len() {
3925        if let Some((location, resolution)) = table
3926            .iter()
3927            .find(|(location, _)| delimited_at(content, at, location))
3928        {
3929            match resolution {
3930                Resolution::Rewrite(there) => {
3931                    written.push_str(there);
3932                    counts.rewritten += 1;
3933                }
3934                // Both left-alone outcomes count as unresolved on the branch that counts
3935                // them, which is what really holds `ambiguous` at or below `unresolved`.
3936                Resolution::NoCounterpart => {
3937                    written.push_str(location);
3938                    counts.unresolved += 1;
3939                }
3940                Resolution::Ambiguous => {
3941                    written.push_str(location);
3942                    counts.unresolved += 1;
3943                    counts.ambiguous += 1;
3944                }
3945            }
3946            at += location.len();
3947            continue;
3948        }
3949        let character = content[at..]
3950            .chars()
3951            .next()
3952            .expect("a character at a boundary this walk only ever lands on");
3953        written.push(character);
3954        at += character.len_utf8();
3955    }
3956    (written, counts)
3957}
3958
3959/// The entry [`Engine::rewrite_references`] records for a document it rewrote from `read`
3960/// to `written`, or `None` to carry what the document records verbatim.
3961///
3962/// `None` unless `read` is still the rendering its entry vouches for, because only then is
3963/// the one change between it and `written` this copy's own: a hand edit is never laundered.
3964fn restamped(
3965    metadata: &BTreeMap<String, Value>,
3966    read: &str,
3967    written: &str,
3968) -> Option<TemplateProvenance> {
3969    let mut provenance = TemplateProvenance::read(metadata).ok().flatten()?;
3970    if provenance.body_digest.as_str() != body_digest(read) {
3971        return None;
3972    }
3973    provenance.body_digest =
3974        Sha256Digest::parse(body_digest(written)).expect("`body_digest` spells a digest");
3975    Some(provenance)
3976}
3977
3978/// The origin one item records, when it records a usable one.
3979fn origin_of(metadata: &BTreeMap<String, Value>) -> Option<GlobalId> {
3980    metadata
3981        .get(GlobalId::ORIGIN_KEY)?
3982        .as_str()?
3983        .parse::<GlobalId>()
3984        .ok()
3985}
3986
3987/// The destination item one item's `onetaskgraph.copies` entry names at `destination`, when
3988/// it holds a usable one.
3989///
3990/// An entry that is not a qualified id, or is one naming another source than the key it is
3991/// filed under, is no link at all: the copy goes on by the rules that follow and rewrites
3992/// it to what they find.
3993fn link_of(metadata: &BTreeMap<String, Value>, destination: &SourceName) -> Option<GlobalId> {
3994    let linked = metadata
3995        .get(MetadataKey::COPIES_KEY)?
3996        .as_object()?
3997        .get(destination.as_str())?
3998        .as_str()?
3999        .parse::<GlobalId>()
4000        .ok()?;
4001    (&linked.source == destination).then_some(linked)
4002}
4003
4004/// The entries of a `onetaskgraph.copies` value that are links: a source name, and a
4005/// qualified id of that same source.
4006fn well_formed(value: Option<&Value>) -> serde_json::Map<String, Value> {
4007    let Some(Value::Object(held)) = value else {
4008        return serde_json::Map::new();
4009    };
4010    held.iter()
4011        .filter(|(destination, linked)| {
4012            linked
4013                .as_str()
4014                .and_then(|linked| linked.parse::<GlobalId>().ok())
4015                .is_some_and(|linked| linked.source.as_str() == destination.as_str())
4016        })
4017        .map(|(destination, linked)| (destination.clone(), linked.clone()))
4018        .collect()
4019}
4020
4021/// Why a value is not one a `onetaskgraph.copies` entry may hold, or nothing when it is: a
4022/// JSON object mapping each destination source name to a qualified id of that source.
4023///
4024/// What the stdio boundary holds an incoming value of that key to, so a plugin is never
4025/// handed a link it could not answer a later copy with.
4026pub(crate) fn malformed_links(value: &Value) -> Option<String> {
4027    let Value::Object(held) = value else {
4028        return Some(format!(
4029            "{} holds an object of destination source names to qualified ids, not {value}",
4030            MetadataKey::COPIES_KEY
4031        ));
4032    };
4033    let wrong = held.len() - well_formed(Some(value)).len();
4034    (wrong > 0).then(|| {
4035        format!(
4036            "{} holds an object of destination source names to qualified ids of that source, \
4037             and {wrong} of its entries is not one: {value}",
4038            MetadataKey::COPIES_KEY
4039        )
4040    })
4041}
4042
4043/// The title and metadata of either kind of item.
4044fn described(item: &Item) -> (&str, &BTreeMap<String, Value>) {
4045    match item {
4046        Item::Task(task) => (&task.title, &task.metadata),
4047        Item::Project(project) => (&project.title, &project.metadata),
4048        Item::Document(document) => (&document.title, &document.metadata),
4049    }
4050}
4051
4052/// The item as the destination should hold it.
4053///
4054/// `url`, `location`, `created_at` and `updated_at` are the destination's own and are
4055/// never written — where the *source* holds an item says nothing about where the
4056/// destination does, which is why a copied document does not arrive claiming the path or
4057/// the link its source reported. The two reserved keys this product encodes typed fields
4058/// under are removed, because those fields travel as themselves — leaving the encoding
4059/// beside them would have the destination hold one thing twice, and disagree with itself
4060/// the moment one changed.
4061///
4062/// A task's `delivers` is `delivers`, already resolved against the destination, and its
4063/// `delivered_by` is the one the destination holds — never the source's, and empty for an
4064/// item this copy creates: that list is the store's to keep, at the destination. The
4065/// `onetaskgraph.copies` link is kept on the same terms: it says where the *destination*
4066/// item was copied to, which is the destination's to hold, never the source's to send.
4067fn outgoing(
4068    item: &Planned,
4069    id: NativeId,
4070    project: Option<NativeId>,
4071    origin: &Origin,
4072    delivers: &[TaskRef],
4073    held: Option<&Prior>,
4074) -> Item {
4075    let own = held.map(|held| described(&held.item).1);
4076    let carried = |metadata: &BTreeMap<String, Value>| carried(metadata, origin, own);
4077    match &item.item {
4078        Item::Task(task) => Item::Task(Box::new(Task {
4079            id,
4080            url: None,
4081            location: None,
4082            created_at: None,
4083            updated_at: None,
4084            project,
4085            metadata: carried(&task.metadata),
4086            delivers: delivers.to_vec(),
4087            delivered_by: match held.map(|held| &held.item) {
4088                Some(Item::Task(held)) => held.delivered_by.clone(),
4089                _ => Vec::new(),
4090            },
4091            ..(**task).clone()
4092        })),
4093        Item::Project(project) => Item::Project(Box::new(Project {
4094            id,
4095            url: None,
4096            location: None,
4097            created_at: None,
4098            updated_at: None,
4099            metadata: carried(&project.metadata),
4100            ..(**project).clone()
4101        })),
4102        Item::Document(document) => Item::Document(Box::new(Document {
4103            id,
4104            url: None,
4105            location: None,
4106            created_at: None,
4107            updated_at: None,
4108            project,
4109            metadata: carried(&document.metadata),
4110            ..(**document).clone()
4111        })),
4112    }
4113}
4114
4115/// The id a created item is offered to the destination under.
4116///
4117/// A task whose id is `<its project's id>/<rest>` is offered as `<rest>` under the
4118/// destination project it is filed in, so a destination whose ids are paths files it in that
4119/// project rather than the source's. Any other id, and every update, is left as it is. The
4120/// `a_project_copied_*` journeys in `crates/onetaskgraph/tests/e2e/copy.rs` hold the Markdown
4121/// plugin's id shape to this rule.
4122fn created_id(item: &Item, filed: Option<&NativeId>) -> NativeId {
4123    if let (Item::Task(task), Some(filed)) = (item, filed)
4124        && let Some(own) = &task.project
4125        && let Some(rest) = task
4126            .id
4127            .as_str()
4128            .strip_prefix(own.as_str())
4129            .and_then(|rest| rest.strip_prefix('/'))
4130        && !rest.is_empty()
4131    {
4132        return NativeId(format!("{}/{rest}", filed.as_str()));
4133    }
4134    item.id().clone()
4135}
4136
4137/// The metadata a copy carries: the caller's own keys untouched, the origin settled, and the
4138/// destination's own store-kept keys — its `onetaskgraph.copies` link, and the
4139/// `onetaskgraph.members` or `onetaskgraph.member_of` that ties it to its member projects or
4140/// its home — kept as `own`, the destination item, holds them.
4141///
4142/// The key is removed before it is settled rather than overwritten, because the item being
4143/// copied carries an origin of its own and [`Origin::Keeps`] must not let it through. Its
4144/// link is removed for the same reason: it names where *that* item was copied to, and on
4145/// the destination it would name the destination item's own counterpart as itself. Its
4146/// member keys name projects tied to *it*, which say nothing about the destination's.
4147fn carried(
4148    metadata: &BTreeMap<String, Value>,
4149    origin: &Origin,
4150    own: Option<&BTreeMap<String, Value>>,
4151) -> BTreeMap<String, Value> {
4152    let mut carried = metadata.clone();
4153    carried.remove(Repository::METADATA_KEY);
4154    carried.remove(DependencyEdge::RECORDED_KEY);
4155    carried.remove(TaskRef::DELIVERS_KEY);
4156    carried.remove(TaskRef::DELIVERED_BY_KEY);
4157    carried.remove(GlobalId::ORIGIN_KEY);
4158    for kept in [
4159        MetadataKey::COPIES_KEY,
4160        MetadataKey::MEMBERS_KEY,
4161        MetadataKey::MEMBER_OF_KEY,
4162    ] {
4163        carried.remove(kept);
4164        if let Some(held) = own.and_then(|own| own.get(kept)) {
4165            carried.insert(kept.to_owned(), held.clone());
4166        }
4167    }
4168    let held = match origin {
4169        Origin::Records(id) => Some(Value::String(id.to_string())),
4170        Origin::Keeps(held) => held.clone(),
4171    };
4172    if let Some(held) = held {
4173        carried.insert(GlobalId::ORIGIN_KEY.to_owned(), held);
4174    }
4175    carried
4176}
4177
4178/// What one landed item records at [`GlobalId::ORIGIN_KEY`].
4179enum Origin {
4180    /// The qualified id this item was copied from, as the id type rather than as its
4181    /// spelling: the key holds a [`GlobalId`] and nothing else may be recorded there.
4182    Records(GlobalId),
4183    /// Whatever the destination already holds there — `None` when it holds nothing, which
4184    /// is written as the key being absent rather than as a null.
4185    ///
4186    /// A [`Value`] and not a [`GlobalId`], because this variant does not interpret what it
4187    /// carries: it is the destination's own metadata entry, held for the length of one
4188    /// write and put back exactly as it was read. Parsing it would turn a value a
4189    /// destination holds and this engine cannot read into a value this engine deletes,
4190    /// which is the opposite of what keeping it means.
4191    Keeps(Option<Value>),
4192}
4193
4194/// Which of the two a copy of this item does.
4195///
4196/// A copy that reached its target by rule 1 is a copy-back: the item being copied names
4197/// the destination item, so the destination is the *original* and the id being copied
4198/// belongs to the copy that came out of it. Recording that id there would overwrite the
4199/// original's own provenance — and with it the correspondence every later copy from the
4200/// source it was authored in depends on. That copy would then match nothing and create a
4201/// second item beside the one it meant to update, which is the whole failure: nothing is
4202/// reported, and whoever reads that board now has two. So a copy-back leaves the
4203/// destination's origin exactly as the destination holds it, absent included, and every
4204/// other copy records the id it was copied from.
4205fn recorded(item: &Planned, held: Option<&Prior>) -> Origin {
4206    if let Target::Update {
4207        found: Found::Origin,
4208        ..
4209    } = &item.target
4210    {
4211        return Origin::Keeps(
4212            held.and_then(|held| described(&held.item).1.get(GlobalId::ORIGIN_KEY).cloned()),
4213        );
4214    }
4215    Origin::Records(item.source.clone())
4216}
4217
4218/// Whether the destination already reads exactly as this copy would leave it.
4219///
4220/// The destination's own `url` and timestamps are excluded because a copy never writes
4221/// them, so a difference there is not one this copy would close.
4222fn same(held: &Item, outgoing: &Item, destination: &SourceName) -> bool {
4223    match (held, outgoing) {
4224        (Item::Task(held), Item::Task(outgoing)) => {
4225            // Qualified before they are compared, so `T-1` and `folder:T-1` at the destination
4226            // `folder` are the one entry they are.
4227            targets(&held.delivers, destination) == targets(&outgoing.delivers, destination)
4228                && targets(&held.delivered_by, destination)
4229                    == targets(&outgoing.delivered_by, destination)
4230                && held.title == outgoing.title
4231                && held.content == outgoing.content
4232                && held.status == outgoing.status
4233                && held.priority == outgoing.priority
4234                && held.labels == outgoing.labels
4235                && held.project == outgoing.project
4236                && held.metadata == outgoing.metadata
4237                && held.repositories == outgoing.repositories
4238        }
4239        (Item::Project(held), Item::Project(outgoing)) => {
4240            held.title == outgoing.title
4241                && held.content == outgoing.content
4242                && held.status == outgoing.status
4243                && held.labels == outgoing.labels
4244                && held.metadata == outgoing.metadata
4245                && held.repositories == outgoing.repositories
4246        }
4247        // No status, because a document has none; no edges, because it is in no graph.
4248        (Item::Document(held), Item::Document(outgoing)) => {
4249            held.title == outgoing.title
4250                && held.content == outgoing.content
4251                && held.labels == outgoing.labels
4252                && held.project == outgoing.project
4253                && held.metadata == outgoing.metadata
4254                && held.repositories == outgoing.repositories
4255        }
4256        _ => false,
4257    }
4258}
4259
4260/// Whether the destination's forward edges already say what this copy would write.
4261fn same_edges(held: &[DependencyEdge], outgoing: &[DependencyEdge]) -> bool {
4262    let ends = |edges: &[DependencyEdge]| {
4263        let mut ends: Vec<(String, ItemKind, DependencyKind)> = edges
4264            .iter()
4265            .map(|edge| (edge.to.id().to_owned(), edge.to.kind, edge.kind))
4266            .collect();
4267        ends.sort_by(|left, right| left.0.cmp(&right.0));
4268        ends
4269    };
4270    ends(held) == ends(outgoing)
4271}
4272
4273/// Each read edge as the destination should record it, or `None` when its far end is a
4274/// member of this copy whose destination id is not known yet.
4275///
4276/// `destination` is the source the near end lands in. A far end this copy landed in another
4277/// source — a routed copy's other half — is written qualified, which every source records
4278/// as a cross-source edge on the near item.
4279fn mapped_edges(
4280    edges: &[DependencyEdge],
4281    origin: &SourceName,
4282    destination: &SourceName,
4283    copied: &[GlobalId],
4284    written: &BTreeMap<String, GlobalId>,
4285) -> Vec<Option<DependencyEdge>> {
4286    edges
4287        .iter()
4288        .map(|edge| {
4289            let far = GlobalId::new(origin.clone(), NativeId(edge.to.id().to_owned()));
4290            let id = if let Some(native) = names(&edge.to, destination) {
4291                // A far end already qualified to the destination's own source is that
4292                // source's own item, so it is written the way that source names its own:
4293                // unqualified. Leaving it qualified would have the destination hold an
4294                // edge into itself written as if it left, which is the one spelling the
4295                // reserved key exists to keep for edges that really do.
4296                Some(native)
4297            } else if !edge.to.is_qualified() && copied.contains(&far) {
4298                // A member of this copy, inside one source included: a copy there lands
4299                // beside the item it was read from, so the edge names the copy rather than
4300                // the original the copy's own project does not hold.
4301                written
4302                    .get(&far.to_string())
4303                    .map(|landed| spelled_from(landed, destination))
4304            } else if edge.to.is_qualified() || origin == destination {
4305                // Already naming a source of its own, or a copy inside one source where
4306                // the far end's own id is the destination's id.
4307                Some(edge.to.id().to_owned())
4308            } else {
4309                Some(far.to_string())
4310            }?;
4311            DependencyEndpoint::new(id, edge.to.kind)
4312                .ok()
4313                .map(|to| DependencyEdge {
4314                    from: edge.from.clone(),
4315                    to,
4316                    kind: edge.kind,
4317                })
4318        })
4319        .collect()
4320}
4321
4322/// Each `delivers` entry of one planned task as the destination should hold it, or `None`
4323/// where it names a member of this copy whose destination id is not known yet. Empty for
4324/// anything but a task.
4325fn delivers_of(
4326    item: &Planned,
4327    destination: &SourceName,
4328    copied: &[GlobalId],
4329    written: &BTreeMap<String, GlobalId>,
4330) -> Vec<Option<TaskRef>> {
4331    match &item.item {
4332        Item::Task(task) => mapped_delivers(
4333            &task.delivers,
4334            &item.source.source,
4335            destination,
4336            copied,
4337            written,
4338        ),
4339        Item::Project(_) | Item::Document(_) => Vec::new(),
4340    }
4341}
4342
4343/// Each `delivers` entry as the destination should hold it, or `None` when it names a member
4344/// of this copy whose destination id is not known yet.
4345///
4346/// An entry naming a member of the copied set becomes that member's own id at the
4347/// destination — bare, because the member is the destination's, unless the id holds a colon
4348/// a bare entry would be misread by. Every other entry is carried through qualified, so a
4349/// bare one read at `origin` goes on naming a task of `origin`.
4350fn mapped_delivers(
4351    entries: &[TaskRef],
4352    origin: &SourceName,
4353    destination: &SourceName,
4354    copied: &[GlobalId],
4355    written: &BTreeMap<String, GlobalId>,
4356) -> Vec<Option<TaskRef>> {
4357    entries
4358        .iter()
4359        .map(|entry| {
4360            let qualified = entry.in_source(origin);
4361            let Ok(far) = qualified.as_str().parse::<GlobalId>() else {
4362                return Some(qualified);
4363            };
4364            if !copied.contains(&far) {
4365                return Some(qualified);
4366            }
4367            let landed = written.get(&far.to_string())?;
4368            let native = &landed.native;
4369            Some(
4370                if &landed.source != destination || native.as_str().contains(':') {
4371                    TaskRef::qualified(&landed.source, native)
4372                } else {
4373                    TaskRef::new(native.as_str())
4374                        .unwrap_or_else(|_| TaskRef::qualified(destination, native))
4375                },
4376            )
4377        })
4378        .collect()
4379}
4380
4381/// How many of one task's `delivers` entries name a member of the copied set.
4382fn members_named(entries: &[TaskRef], origin: &SourceName, copied: &[GlobalId]) -> u64 {
4383    let named = entries
4384        .iter()
4385        .filter(|entry| {
4386            entry
4387                .in_source(origin)
4388                .as_str()
4389                .parse::<GlobalId>()
4390                .is_ok_and(|far| copied.contains(&far))
4391        })
4392        .count();
4393    u64::try_from(named).unwrap_or(u64::MAX)
4394}
4395
4396/// The entries that could be resolved, which is every one of them on the second pass.
4397fn resolved_entries(entries: &[Option<TaskRef>]) -> Vec<TaskRef> {
4398    entries.iter().flatten().cloned().collect()
4399}
4400
4401/// How an item landed at `landed` is named from one landed in `near`: by its native id in
4402/// the same source, qualified from any other.
4403fn spelled_from(landed: &GlobalId, near: &SourceName) -> String {
4404    if &landed.source == near {
4405        landed.native.0.clone()
4406    } else {
4407        landed.to_string()
4408    }
4409}
4410
4411/// The native id a qualified endpoint names at `destination`, when it names one there.
4412fn names(endpoint: &DependencyEndpoint, destination: &SourceName) -> Option<String> {
4413    if !endpoint.is_qualified() {
4414        return None;
4415    }
4416    let id: GlobalId = endpoint.id().parse().ok()?;
4417    (&id.source == destination).then_some(id.native.0)
4418}
4419
4420/// Where a project copy files the tasks it carries: in each source a task lands in, the
4421/// home there or the home's member project.
4422type Filing = BTreeMap<SourceName, NativeId>;
4423
4424/// The edges that could be resolved, which is every one of them on the second pass.
4425fn resolved(edges: &[Option<DependencyEdge>]) -> Vec<DependencyEdge> {
4426    edges.iter().flatten().cloned().collect()
4427}
4428
4429/// Refuse an item of a member copy whose edge names a member that copy does not carry
4430/// and whose destination id nothing records.
4431///
4432/// `unrecorded` is empty for every other copy, which is what makes this a no-op there. An
4433/// edge already naming a source of its own, and every edge of a copy inside one source,
4434/// is written as it was read by [`mapped_edges`], so neither can need a recorded origin.
4435fn unrecorded_far_end(item: &Planned, unrecorded: &[GlobalId]) -> Result<(), EngineError> {
4436    if item.source.source == item.to {
4437        return Ok(());
4438    }
4439    for edge in &item.edges {
4440        if edge.to.is_qualified() {
4441            continue;
4442        }
4443        let far = GlobalId::new(
4444            item.source.source.clone(),
4445            NativeId(edge.to.id().to_owned()),
4446        );
4447        if unrecorded.contains(&far) {
4448            return Err(EngineError::UnrecordedMember {
4449                item: item.source.clone(),
4450                member: far,
4451                destination: item.to.clone(),
4452            });
4453        }
4454    }
4455    Ok(())
4456}