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