Skip to main content

onetaskgraph_core/engine/
rendered.rs

1//! Tasks and documents created from a body or a template, and regenerated in place.
2//!
3//! An item created or regenerated from a template records where it came from under the
4//! reserved `onetaskgraph.template` key ([`TemplateProvenance`]); one created from a plain
5//! body records none. The answers a regenerate needs are kept only where a source keeps them
6//! beside the item — `local-md`'s own file — and never in the item's content or metadata, so
7//! nothing of them is duplicated into an issue body. Every write here lands the content, the
8//! provenance and those answers in **one** call to the source, which the source makes one
9//! write.
10//!
11//! Regeneration is this engine's, and it never resolves a caller's layers or runs a caller's
12//! command: the template is the one given — a file, or a loader document a caller states —
13//! else the recorded `template` when that is a readable file, else the regenerate is refused
14//! naming the recorded reference.
15
16use std::collections::BTreeMap;
17use std::fmt;
18use std::path::{Path, PathBuf};
19
20use onetaskgraph_plugin_api::{
21    DependencyEdge, DependencyEndpoint, DependencyKind, Document, ItemKind, ItemWrite, Label,
22    MetadataKey, MetadataRecord, NativeId, Priority, Repository, SourceName, Status,
23    StatusCategory, Task, TaskRef,
24};
25use schemars::JsonSchema;
26use serde::{Deserialize, Serialize};
27use serde_json::Value;
28
29use super::delivery::{qualified_task, source_failed, targets};
30use super::{Delivered, Engine, EngineError, Qualified};
31use crate::GlobalId;
32use crate::resolve::ResolvedSource;
33use crate::template::{
34    Answers, RenderedTemplate, Sha256Digest, Template, TemplateError, TemplateInput,
35    TemplateProvenance,
36};
37
38/// The content a create writes: text given as it is, or a template's rendering.
39///
40/// Built only by [`Body::plain`], [`Body::rendered`] and [`Body::rendered_as`], so a rendered
41/// body always carries the [`TemplateProvenance`] its rendering proves — a rendering whose
42/// digest is not one is refused where the body is built, never later where it is written.
43#[derive(Debug, Clone, PartialEq)]
44pub struct Body(Content);
45
46#[derive(Debug, Clone, PartialEq)]
47enum Content {
48    /// The item records no provenance and no answers are kept.
49    Plain(String),
50    /// The item records `provenance`, and a source that keeps answers keeps the rendering's
51    /// resolved answers beside it.
52    Rendered {
53        rendered: RenderedTemplate,
54        provenance: TemplateProvenance,
55    },
56}
57
58impl Body {
59    /// Text given as it is.
60    #[must_use]
61    pub fn plain(text: impl Into<String>) -> Self {
62        Self(Content::Plain(text.into()))
63    }
64
65    /// A template's rendering, recorded by the reference `template` answers.
66    ///
67    /// # Errors
68    ///
69    /// As [`Body::rendered_as`].
70    pub fn rendered(
71        template: &TemplateInput,
72        rendered: RenderedTemplate,
73    ) -> Result<Self, TemplateError> {
74        Self::rendered_as(template.reference(), rendered)
75    }
76
77    /// A template's rendering, recorded by `template` — a string of a library caller's own.
78    ///
79    /// # Errors
80    ///
81    /// [`TemplateError::Malformed`] when the rendering's `digest` is not one: a
82    /// [`RenderedTemplate`] assembled by hand rather than answered by a render.
83    pub fn rendered_as(
84        template: impl Into<String>,
85        rendered: RenderedTemplate,
86    ) -> Result<Self, TemplateError> {
87        let provenance = TemplateProvenance::of(template, &rendered)?;
88        Ok(Self(Content::Rendered {
89            rendered,
90            provenance,
91        }))
92    }
93
94    /// What a create writes of this body beside `metadata`.
95    fn parts(&self, metadata: &BTreeMap<MetadataKey, Value>) -> Parts<'_> {
96        let mut carried: BTreeMap<String, Value> = metadata
97            .iter()
98            .map(|(key, value)| (key.as_str().to_owned(), value.clone()))
99            .collect();
100        match &self.0 {
101            Content::Plain(content) => Parts {
102                content: content.clone(),
103                metadata: carried,
104                answers: None,
105            },
106            Content::Rendered {
107                rendered,
108                provenance,
109            } => {
110                carried.insert(TemplateProvenance::KEY.to_owned(), provenance.to_value());
111                Parts {
112                    content: rendered.body.clone(),
113                    metadata: carried,
114                    answers: Some(&rendered.answers),
115                }
116            }
117        }
118    }
119}
120
121/// What a create writes: the content, the metadata the item carries — the caller's keys and,
122/// for a rendering, its provenance — and the answers a source that keeps them keeps beside it.
123struct Parts<'a> {
124    content: String,
125    metadata: BTreeMap<String, Value>,
126    answers: Option<&'a BTreeMap<String, Value>>,
127}
128
129/// One task to create in one source.
130#[derive(Debug, Clone, PartialEq)]
131pub struct TaskCreate {
132    /// The configured source to create it in.
133    pub source: SourceName,
134    /// The project it is filed under, by that source's own id.
135    pub project: NativeId,
136    /// Its title.
137    pub title: String,
138    /// Its content.
139    pub body: Body,
140    /// Its status category; `todo` when none is given.
141    pub status: Option<StatusCategory>,
142    /// The names of its labels.
143    pub labels: Vec<String>,
144    /// The repositories its work changes.
145    pub repositories: Vec<Repository>,
146    /// The tasks it depends on — its own source's written without their source, any other's
147    /// qualified.
148    pub depends_on: Vec<GlobalId>,
149    /// The tasks it delivers. Each is kept in step with it once it lands, as every write of a
150    /// task that delivers keeps them.
151    pub delivers: Vec<GlobalId>,
152    /// The caller's own metadata keys — never one this product reserves, which a
153    /// [`MetadataKey`] cannot name.
154    pub metadata: BTreeMap<MetadataKey, Value>,
155}
156
157/// One project document to create, or replace, in one source.
158#[derive(Debug, Clone, PartialEq)]
159pub struct DocumentCreate {
160    /// The configured source to write it in.
161    pub source: SourceName,
162    /// The project it is filed under, by that source's own id.
163    pub project: NativeId,
164    /// Its title.
165    pub title: String,
166    /// The id to write it under: a document the source already holds by this id is
167    /// replaced, and otherwise one is created under it where the source lets a caller name
168    /// what it creates. `None` creates one under an id derived from the title.
169    pub id: Option<NativeId>,
170    /// Its content.
171    pub body: Body,
172    /// The names of its labels.
173    pub labels: Vec<String>,
174    /// The repositories it concerns.
175    pub repositories: Vec<Repository>,
176    /// The caller's own metadata keys.
177    pub metadata: BTreeMap<MetadataKey, Value>,
178}
179
180/// What creating a task came to: the task as its source reads it back, and every task it
181/// delivers, kept in step with it.
182#[derive(Debug, Clone, PartialEq)]
183pub struct TaskCreated {
184    /// The task, under its qualified id.
185    pub task: Qualified<Task>,
186    /// One entry per task it delivers; empty when it delivers none.
187    pub delivered: Vec<Delivered>,
188}
189
190/// Which kind of item a regenerate or an answers read names.
191#[derive(Debug, Clone, Copy, PartialEq, Eq)]
192pub enum RenderedRecord {
193    /// A task.
194    Task,
195    /// A project document.
196    Document,
197}
198
199impl fmt::Display for RenderedRecord {
200    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
201        formatter.write_str(self.noun())
202    }
203}
204
205impl RenderedRecord {
206    fn noun(self) -> &'static str {
207        match self {
208            Self::Task => "task",
209            Self::Document => "document",
210        }
211    }
212
213    fn no_such(self, id: &GlobalId) -> EngineError {
214        match self {
215            Self::Task => EngineError::NoSuchTask { id: id.to_string() },
216            Self::Document => EngineError::NoSuchDocument { id: id.to_string() },
217        }
218    }
219}
220
221/// Which template a regenerate renders.
222///
223/// One of two, rather than an optional template beside a search path: a search path means
224/// something only for the recorded file, and a given template carries its own.
225#[derive(Debug, Clone, PartialEq)]
226pub enum RenderTemplate {
227    /// The template given: a file with the directories its chain resolves over, or a loader
228    /// document.
229    Given(TemplateInput),
230    /// The `template` the item records, when that is a readable file, its chain resolved over
231    /// `search_path`. A recorded reference that is not a file is refused, never resolved.
232    Recorded {
233        /// The directories the recorded file's chain resolves over.
234        search_path: Vec<PathBuf>,
235    },
236}
237
238impl Default for RenderTemplate {
239    fn default() -> Self {
240        Self::Recorded {
241            search_path: Vec::new(),
242        }
243    }
244}
245
246/// What `task render` and `document render` are asked.
247#[derive(Debug, Clone, PartialEq, Default)]
248pub struct RenderRequest {
249    /// The template to render.
250    pub template: RenderTemplate,
251    /// The answers laid over the base: the stored ones when they are in step with the item's
252    /// provenance, none otherwise. An unset name drops its answer.
253    pub answers: Answers,
254    /// Read and render everything, and write nothing.
255    pub dry_run: bool,
256}
257
258/// What `task render` and `document render` answer with.
259#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
260pub struct Regenerated {
261    /// The item regenerated.
262    pub id: GlobalId,
263    /// The chain digest it rendered with.
264    pub digest: Sha256Digest,
265    /// The SHA-256 of the rendered content: what its provenance now records.
266    pub body_digest: Sha256Digest,
267    /// Whether its content, its provenance or its stored answers differ from what it held —
268    /// what a write changed, or for a dry run what one would change.
269    pub changed: bool,
270    /// The rendered content.
271    pub body: String,
272}
273
274/// What `task answers` and `document answers` answer with: the resolved answers an item was
275/// last rendered from — defaults applied, `null` for an optional variable given neither — by
276/// variable name, exactly as its source keeps them.
277#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
278#[serde(transparent)]
279pub struct TemplateAnswers(pub BTreeMap<String, Value>);
280
281impl TemplateAnswers {
282    /// The answers as a YAML mapping — the document an answers file holds, so what this
283    /// prints can be handed back with `--answers`.
284    ///
285    /// # Errors
286    ///
287    /// Why the answers could not be written as YAML.
288    pub fn to_yaml(&self) -> Result<String, String> {
289        serde_norway::to_string(&self.0).map_err(|error| error.to_string())
290    }
291}
292
293/// Why a regenerate did not start from the answers stored beside the item.
294#[derive(Debug, Clone, PartialEq, Eq)]
295pub enum UnusedAnswers {
296    /// The item records no template provenance, so nothing says which answers are its.
297    NoProvenance,
298    /// Its source keeps no answers beside it — it keeps none at all, or none for this item.
299    NoneStored {
300        /// The configured name of its source.
301        source: String,
302    },
303    /// The answers stored beside it do not hash to the `answers_digest` its provenance
304    /// records.
305    OutOfStep,
306    /// Its `onetaskgraph.template` entry is not one this product writes, so nothing trusted says
307    /// which answers are its.
308    MalformedProvenance {
309        /// What is wrong with the entry.
310        problem: String,
311    },
312}
313
314impl fmt::Display for UnusedAnswers {
315    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
316        match self {
317            Self::NoProvenance => formatter.write_str(
318                "it records no template provenance, so no stored answers are known to be its",
319            ),
320            Self::NoneStored { source } => write!(
321                formatter,
322                "source {source} holds no stored answers for it — only a source that keeps an \
323                 authoring file, such as local-md, stores them"
324            ),
325            Self::OutOfStep => formatter.write_str(
326                "the answers stored beside it do not hash to the answers_digest its provenance \
327                 records, so they changed after it was rendered and are not trusted",
328            ),
329            Self::MalformedProvenance { problem } => write!(
330                formatter,
331                "{problem}, so nothing trusted says which stored answers are its"
332            ),
333        }
334    }
335}
336
337/// Everything a regenerate reads before it renders: the item's own content and provenance,
338/// the template, and the answers a render starts from.
339///
340/// [`Engine::regeneration`] answers one and [`Engine::regenerate`] renders and writes it — the
341/// two halves apart so a caller that asks for answers, as the command line does when
342/// interactive, can ask between them. [`Engine::render_task`] and
343/// [`Engine::render_document`] are the two together.
344#[derive(Debug, Clone)]
345pub struct Regeneration {
346    id: GlobalId,
347    record: RenderedRecord,
348    template: Template,
349    reference: String,
350    base: Answers,
351    stored: Stored,
352    /// Whether its source keeps answers beside it, so that answers it does not hold are a
353    /// difference a write repairs rather than the source's nature.
354    keeps_answers: bool,
355    content: String,
356    provenance: Option<TemplateProvenance>,
357}
358
359/// The answers stored beside an item, and whether a render starts from them — one or the
360/// other, never both: answers that are the base are in step with the provenance, and answers
361/// that are not carry why.
362#[derive(Debug, Clone)]
363enum Stored {
364    /// In step with the item's provenance, so a render starts from them.
365    Trusted(BTreeMap<String, Value>),
366    /// Not a base, for `reason`. What is held, if anything, is still what a render's own
367    /// answers are compared with to say whether the item changed.
368    Unused {
369        reason: UnusedAnswers,
370        held: Option<BTreeMap<String, Value>>,
371    },
372}
373
374impl Stored {
375    /// Whatever answers the item holds, trusted or not.
376    fn held(&self) -> Option<&BTreeMap<String, Value>> {
377        match self {
378            Self::Trusted(answers) => Some(answers),
379            Self::Unused { held, .. } => held.as_ref(),
380        }
381    }
382}
383
384impl Regeneration {
385    /// The item being regenerated.
386    #[must_use]
387    pub fn id(&self) -> &GlobalId {
388        &self.id
389    }
390
391    /// The template it renders.
392    #[must_use]
393    pub fn template(&self) -> &Template {
394        &self.template
395    }
396
397    /// The answers a render starts from, before any new ones are laid over them.
398    #[must_use]
399    pub fn base(&self) -> &Answers {
400        &self.base
401    }
402
403    /// Every variable the stored answers settle when they are the base — an optional one
404    /// they left `null` included, which a render leaves unanswered again — and none when
405    /// they are not.
406    pub fn settled(&self) -> impl Iterator<Item = &str> {
407        let trusted = match &self.stored {
408            Stored::Trusted(answers) => Some(answers),
409            Stored::Unused { .. } => None,
410        };
411        trusted
412            .into_iter()
413            .flat_map(BTreeMap::keys)
414            .map(String::as_str)
415    }
416
417    /// Why the stored answers were not the base, when they were not.
418    #[must_use]
419    pub fn unused(&self) -> Option<&UnusedAnswers> {
420        match &self.stored {
421            Stored::Trusted(_) => None,
422            Stored::Unused { reason, .. } => Some(reason),
423        }
424    }
425
426    /// Render from the base with `answers` laid over it.
427    ///
428    /// A stored answer to a variable the template no longer declares is dropped rather than
429    /// refused: it was this product's to keep, not the caller's to answer for.
430    ///
431    /// # Errors
432    ///
433    /// [`EngineError::MissingAnswers`] naming every required variable left unanswered and
434    /// why the stored answers were not used; [`EngineError::Template`] for any other refusal
435    /// of the answers or of the template.
436    pub fn render(&self, answers: &Answers) -> Result<RenderedTemplate, EngineError> {
437        let mut base = self.base.clone();
438        loop {
439            match self.template.render(&base.overlay(answers)) {
440                Err(TemplateError::UnknownAnswer { names })
441                    if names.iter().all(|name| {
442                        base.names().any(|held| held == name)
443                            && !answers.names().any(|given| given == name)
444                    }) =>
445                {
446                    for name in names {
447                        base.unset(name);
448                    }
449                }
450                Err(TemplateError::MissingRequired { names }) => {
451                    return Err(EngineError::MissingAnswers {
452                        id: self.id.to_string(),
453                        names,
454                        reason: self.unused().map_or_else(
455                            || {
456                                "the stored answers were used and do not answer them: an \
457                                 answer unset falls back to its default, and these have none"
458                                    .to_owned()
459                            },
460                            |unused| format!("the stored answers were not used because {unused}"),
461                        ),
462                    });
463                }
464                Err(error) => return Err(EngineError::Template { error }),
465                Ok(rendered) => return Ok(rendered),
466            }
467        }
468    }
469}
470
471impl Engine {
472    /// Create one task, from a plain body or a template's rendering, and keep every task it
473    /// delivers in step with it.
474    ///
475    /// The content, the provenance and — where the source keeps them — the answers land in
476    /// one write. A delivered task that cannot be kept in step is not an error: it is
477    /// reported `failed` beside the task that was created.
478    ///
479    /// # Errors
480    ///
481    /// [`EngineError::UnknownSource`] and [`EngineError::SourceUnavailable`] for a source that
482    /// cannot be reached, [`EngineError::NotCreatable`] for one with no write side — neither is
483    /// written — and [`EngineError::SourceFailed`] when the source refuses the task.
484    pub async fn create_task(&self, request: &TaskCreate) -> Result<TaskCreated, EngineError> {
485        let source = self.creatable(&request.source, MetadataRecord::Task)?;
486        let Parts {
487            content,
488            metadata,
489            answers,
490        } = request.body.parts(&request.metadata);
491        let category = request.status.unwrap_or(StatusCategory::Todo);
492        let near = &request.source;
493        let id = NativeId::from(slug(&request.title, "task").as_str());
494        let depends_on = request
495            .depends_on
496            .iter()
497            .map(|far| DependencyEdge {
498                from: DependencyEndpoint::from_native(id.clone(), ItemKind::Task),
499                to: endpoint(far, near),
500                kind: DependencyKind::Blocks,
501            })
502            .collect();
503        let write = ItemWrite {
504            target: None,
505            item: Task {
506                id,
507                key: None,
508                title: request.title.clone(),
509                content: Some(content),
510                status: Status {
511                    category,
512                    name: category_word(category),
513                },
514                priority: Priority::None,
515                labels: labels(&request.labels),
516                project: Some(request.project.clone()),
517                url: None,
518                location: None,
519                created_at: None,
520                updated_at: None,
521                metadata,
522                repositories: request.repositories.clone(),
523                delivers: request
524                    .delivers
525                    .iter()
526                    .map(|task| TaskRef::qualified(&task.source, &task.native))
527                    .collect(),
528                delivered_by: Vec::new(),
529            },
530            depends_on,
531        };
532        let written = match answers {
533            Some(answers) => source.source().write_task_rendered(&write, answers).await,
534            None => source.source().write_task(&write).await,
535        }
536        .map_err(|error| source_failed(source, error))?;
537        let id = GlobalId::new(near.clone(), written);
538        let task = source
539            .source()
540            .get_task(&id.native)
541            .await
542            .map_err(|error| source_failed(source, error))?
543            .ok_or_else(|| EngineError::NoSuchTask { id: id.to_string() })?;
544        let delivers = targets(&task.delivers, near);
545        let delivered = self
546            .deliver(&id, task.status.category, &delivers, &[])
547            .await;
548        Ok(TaskCreated {
549            task: qualified_task(id, task),
550            delivered,
551        })
552    }
553
554    /// Create one project document, or replace the one the source holds under
555    /// [`DocumentCreate::id`], from a plain body or a template's rendering.
556    ///
557    /// # Errors
558    ///
559    /// As [`create_task`](Self::create_task), and [`EngineError::NoDocuments`] for a source
560    /// declaring it has none, which is not asked.
561    pub async fn create_document(
562        &self,
563        request: &DocumentCreate,
564    ) -> Result<Qualified<Document>, EngineError> {
565        let source = self.creatable(&request.source, MetadataRecord::Document)?;
566        documentary(source)?;
567        let target = match &request.id {
568            Some(id) => source
569                .source()
570                .get_document(id)
571                .await
572                .map_err(|error| source_failed(source, error))?
573                .map(|held| held.id),
574            None => None,
575        };
576        let Parts {
577            content,
578            metadata,
579            answers,
580        } = request.body.parts(&request.metadata);
581        let write = ItemWrite {
582            target,
583            item: Document {
584                id: request
585                    .id
586                    .clone()
587                    .unwrap_or_else(|| NativeId::from(slug(&request.title, "document").as_str())),
588                title: request.title.clone(),
589                content: Some(content),
590                project: Some(request.project.clone()),
591                labels: labels(&request.labels),
592                url: None,
593                location: None,
594                created_at: None,
595                updated_at: None,
596                metadata,
597                repositories: request.repositories.clone(),
598            },
599            depends_on: Vec::new(),
600        };
601        let written = match answers {
602            Some(answers) => {
603                source
604                    .source()
605                    .write_document_rendered(&write, answers)
606                    .await
607            }
608            None => source.source().write_document(&write).await,
609        }
610        .map_err(|error| source_failed(source, error))?;
611        let id = GlobalId::new(request.source.clone(), written);
612        let document = source
613            .source()
614            .get_document(&id.native)
615            .await
616            .map_err(|error| source_failed(source, error))?
617            .ok_or_else(|| EngineError::NoSuchDocument { id: id.to_string() })?;
618        Ok(Qualified { id, item: document })
619    }
620
621    /// The answers the task or document `id` was last rendered from, as its source keeps them.
622    ///
623    /// # Errors
624    ///
625    /// [`EngineError::NoSuchTask`] or [`EngineError::NoSuchDocument`] when the item is not
626    /// there, [`EngineError::NoStoredAnswers`] naming it when none are stored for it — which
627    /// is every item of a source that keeps none — and [`EngineError::SourceFailed`] when the
628    /// source cannot answer.
629    pub async fn template_answers(
630        &self,
631        record: RenderedRecord,
632        id: &GlobalId,
633    ) -> Result<TemplateAnswers, EngineError> {
634        let source = self.built(&id.source)?;
635        if record == RenderedRecord::Document {
636            documentary(source)?;
637        }
638        self.read_item(source, record, id).await?;
639        self.stored(source, record, id)
640            .await?
641            .map(TemplateAnswers)
642            .ok_or_else(|| EngineError::NoStoredAnswers {
643                record,
644                id: id.to_string(),
645                reason: UnusedAnswers::NoneStored {
646                    source: id.source.to_string(),
647                }
648                .to_string(),
649            })
650    }
651
652    /// Read everything a regenerate of the task or document `id` needs, before it renders.
653    ///
654    /// # Errors
655    ///
656    /// [`EngineError::NoSuchTask`] or [`EngineError::NoSuchDocument`] when the item is not
657    /// there; [`EngineError::NoTemplate`] when no template is given and the item records
658    /// none; [`EngineError::MalformedProvenance`] when no template is given and the entry it
659    /// records is not one this product writes; [`EngineError::TemplateNotAFile`] when no template is given and the one it
660    /// records is not a readable file, which only a loader document can then stand in for;
661    /// [`EngineError::Template`] when the template cannot be loaded; and the refusals of a
662    /// source that cannot be reached or cannot answer.
663    pub async fn regeneration(
664        &self,
665        record: RenderedRecord,
666        id: &GlobalId,
667        request: &RenderRequest,
668    ) -> Result<Regeneration, EngineError> {
669        let source = self.built(&id.source)?;
670        if record == RenderedRecord::Document {
671            documentary(source)?;
672        }
673        let (content, metadata) = self.read_item(source, record, id).await?;
674        let read = TemplateProvenance::read(&metadata);
675        let template = match (&request.template, &read) {
676            (RenderTemplate::Given(given), _) => given.clone(),
677            // An entry this product did not write names nothing it can trust: with no template
678            // given there is nothing to render, and the refusal says why.
679            (RenderTemplate::Recorded { .. }, Err(problem)) => {
680                return Err(EngineError::MalformedProvenance {
681                    record,
682                    id: id.to_string(),
683                    problem: problem.clone(),
684                });
685            }
686            (RenderTemplate::Recorded { search_path }, Ok(Some(recorded)))
687                if Path::new(&recorded.template).is_file() =>
688            {
689                TemplateInput::File {
690                    path: PathBuf::from(&recorded.template),
691                    search_path: search_path.clone(),
692                }
693            }
694            (RenderTemplate::Recorded { .. }, Ok(Some(recorded))) => {
695                return Err(EngineError::TemplateNotAFile {
696                    record,
697                    id: id.to_string(),
698                    reference: recorded.template.clone(),
699                });
700            }
701            (RenderTemplate::Recorded { .. }, Ok(None)) => {
702                return Err(EngineError::NoTemplate {
703                    record,
704                    id: id.to_string(),
705                });
706            }
707        };
708        let reference = template.reference();
709        let template = template
710            .load()
711            .map_err(|error| EngineError::Template { error })?;
712        let held = self.stored(source, record, id).await?;
713        let unused = |reason| Stored::Unused {
714            reason,
715            held: held.clone(),
716        };
717        let stored = match (&read, &held) {
718            (Err(problem), _) => unused(UnusedAnswers::MalformedProvenance {
719                problem: problem.clone(),
720            }),
721            (Ok(None), _) => unused(UnusedAnswers::NoProvenance),
722            (Ok(Some(_)), None) => unused(UnusedAnswers::NoneStored {
723                source: id.source.to_string(),
724            }),
725            (Ok(Some(recorded)), Some(answers))
726                if crate::template::answers_digest(answers) != recorded.answers_digest.as_str() =>
727            {
728                unused(UnusedAnswers::OutOfStep)
729            }
730            (Ok(Some(_)), Some(answers)) => Stored::Trusted(answers.clone()),
731        };
732        let mut base = Answers::new();
733        if let Stored::Trusted(answers) = &stored {
734            // An optional variable given nothing resolved to `null`; left unanswered, it
735            // resolves to that again.
736            for (name, value) in answers.iter().filter(|(_, value)| !value.is_null()) {
737                base.set(name.clone(), value.clone());
738            }
739        }
740        Ok(Regeneration {
741            id: id.clone(),
742            record,
743            template,
744            reference,
745            base,
746            stored,
747            keeps_answers: source.source().keeps_template_answers(),
748            content,
749            provenance: read.ok().flatten(),
750        })
751    }
752
753    /// Render `regeneration` with `answers` laid over its base, and write the content, the
754    /// provenance and the answers in one write — and nothing else about the item — when any of
755    /// them differs from what it holds and this is not a dry run.
756    ///
757    /// # Errors
758    ///
759    /// Every refusal of [`Regeneration::render`]; [`EngineError::RenderingNotWritable`] for a
760    /// source with no write side, which is not asked; and [`EngineError::SourceFailed`] when
761    /// the source refuses the write.
762    pub async fn regenerate(
763        &self,
764        regeneration: &Regeneration,
765        answers: &Answers,
766        dry_run: bool,
767    ) -> Result<Regenerated, EngineError> {
768        let rendered = regeneration.render(answers)?;
769        let provenance = TemplateProvenance::of(regeneration.reference.clone(), &rendered)
770            .map_err(|error| EngineError::Template { error })?;
771        let changed = regeneration.content != rendered.body
772            || regeneration.provenance.as_ref() != Some(&provenance)
773            // Answers a source keeps and this item does not hold — a block deleted by hand —
774            // are a difference the write repairs; a source keeping none holds none by nature.
775            || regeneration
776                .stored
777                .held()
778                .map_or(regeneration.keeps_answers, |stored| {
779                    *stored != rendered.answers
780                });
781        let id = &regeneration.id;
782        if changed && !dry_run {
783            let source = self.built(&id.source)?;
784            if !source.source().writes().is_supported() {
785                return Err(EngineError::RenderingNotWritable {
786                    name: source.name().to_string(),
787                    kind: source.kind().to_owned(),
788                    record: regeneration.record,
789                });
790            }
791            let value = provenance.to_value();
792            match regeneration.record {
793                RenderedRecord::Task => {
794                    source
795                        .source()
796                        .set_task_rendering(&id.native, &rendered.body, &value, &rendered.answers)
797                        .await
798                }
799                RenderedRecord::Document => {
800                    source
801                        .source()
802                        .set_document_rendering(
803                            &id.native,
804                            &rendered.body,
805                            &value,
806                            &rendered.answers,
807                        )
808                        .await
809                }
810            }
811            .map_err(|error| source_failed(source, error))?
812            .ok_or_else(|| regeneration.record.no_such(id))?;
813        }
814        Ok(Regenerated {
815            id: id.clone(),
816            digest: provenance.digest,
817            body_digest: provenance.body_digest,
818            changed,
819            body: rendered.body,
820        })
821    }
822
823    /// Regenerate one task in place: [`regeneration`](Self::regeneration) then
824    /// [`regenerate`](Self::regenerate), never asking for an answer.
825    ///
826    /// # Errors
827    ///
828    /// Every refusal of either half.
829    pub async fn render_task(
830        &self,
831        id: &GlobalId,
832        request: &RenderRequest,
833    ) -> Result<Regenerated, EngineError> {
834        let regeneration = self.regeneration(RenderedRecord::Task, id, request).await?;
835        self.regenerate(&regeneration, &request.answers, request.dry_run)
836            .await
837    }
838
839    /// Regenerate one project document in place, on the terms of
840    /// [`render_task`](Self::render_task).
841    ///
842    /// # Errors
843    ///
844    /// As [`render_task`](Self::render_task).
845    pub async fn render_document(
846        &self,
847        id: &GlobalId,
848        request: &RenderRequest,
849    ) -> Result<Regenerated, EngineError> {
850        let regeneration = self
851            .regeneration(RenderedRecord::Document, id, request)
852            .await?;
853        self.regenerate(&regeneration, &request.answers, request.dry_run)
854            .await
855    }
856
857    /// The built source called `name`, when it can be written through.
858    fn creatable(
859        &self,
860        name: &SourceName,
861        record: MetadataRecord,
862    ) -> Result<&ResolvedSource, EngineError> {
863        let source = self.built(name)?;
864        if source.source().writes().is_supported() {
865            return Ok(source);
866        }
867        Err(EngineError::NotCreatable {
868            name: source.name().to_string(),
869            kind: source.kind().to_owned(),
870            record,
871        })
872    }
873
874    /// The content and the metadata of the item `id` names.
875    async fn read_item(
876        &self,
877        source: &ResolvedSource,
878        record: RenderedRecord,
879        id: &GlobalId,
880    ) -> Result<(String, BTreeMap<String, Value>), EngineError> {
881        let read = match record {
882            RenderedRecord::Task => source
883                .source()
884                .get_task(&id.native)
885                .await
886                .map(|task| task.map(|task| (task.content, task.metadata))),
887            RenderedRecord::Document => source
888                .source()
889                .get_document(&id.native)
890                .await
891                .map(|document| document.map(|document| (document.content, document.metadata))),
892        }
893        .map_err(|error| source_failed(source, error))?;
894        let (content, metadata) = read.ok_or_else(|| record.no_such(id))?;
895        Ok((content.unwrap_or_default(), metadata))
896    }
897
898    /// The answers the item `id` names has stored beside it, when it has any.
899    async fn stored(
900        &self,
901        source: &ResolvedSource,
902        record: RenderedRecord,
903        id: &GlobalId,
904    ) -> Result<Option<BTreeMap<String, Value>>, EngineError> {
905        match record {
906            RenderedRecord::Task => source.source().task_template_answers(&id.native).await,
907            RenderedRecord::Document => source.source().document_template_answers(&id.native).await,
908        }
909        .map_err(|error| source_failed(source, error))
910    }
911}
912
913/// Refuse a source declaring it has no documents, before it is asked anything.
914fn documentary(source: &ResolvedSource) -> Result<(), EngineError> {
915    if source.source().capabilities().documents.is_native() {
916        return Ok(());
917    }
918    Err(EngineError::NoDocuments {
919        name: source.name().to_string(),
920        kind: source.kind().to_owned(),
921    })
922}
923
924/// A dependency's far end as the near source writes it: its own item by its own id, another
925/// source's qualified.
926fn endpoint(far: &GlobalId, near: &SourceName) -> DependencyEndpoint {
927    if &far.source == near {
928        return DependencyEndpoint::from_native(far.native.clone(), ItemKind::Task);
929    }
930    DependencyEndpoint::new(far.to_string(), ItemKind::Task)
931        .unwrap_or_else(|_| DependencyEndpoint::from_native(far.native.clone(), ItemKind::Task))
932}
933
934/// A create names each label by name alone, so its id is that name too: the id is what a
935/// folder of Markdown writes, and the name is what a source that matches labels reads.
936fn labels(names: &[String]) -> Vec<Label> {
937    names
938        .iter()
939        .map(|name| Label {
940            id: NativeId::from(name.as_str()),
941            name: name.clone(),
942            color: None,
943        })
944        .collect()
945}
946
947/// A category as this product spells it — the word every source's default mapping reads back
948/// as that category.
949fn category_word(category: StatusCategory) -> String {
950    serde_json::to_value(category)
951        .ok()
952        .and_then(|word| word.as_str().map(str::to_owned))
953        .unwrap_or_else(|| "todo".to_owned())
954}
955
956/// The id a created item is suggested under: its title in lower case, every run of anything
957/// but an ASCII letter or digit one dash. A source is free to file it under another.
958fn slug(title: &str, fallback: &str) -> String {
959    let mut slug = String::with_capacity(title.len());
960    for character in title.chars() {
961        if character.is_ascii_alphanumeric() {
962            slug.push(character.to_ascii_lowercase());
963        } else if !slug.is_empty() && !slug.ends_with('-') {
964            slug.push('-');
965        }
966    }
967    let slug = slug.trim_end_matches('-');
968    if slug.is_empty() {
969        fallback.to_owned()
970    } else {
971        slug.to_owned()
972    }
973}