Skip to main content

prov_graph/graph/
mod.rs

1//! Plain text → walkable graph — the crate's read core.
2//!
3//! Underneath everything else sits the **census**
4//! ([`census`](Graph::census)): one traversal that
5//! yields every forward link reachable from a root — frontmatter relation
6//! edges *and* body `[[…]]` wikilinks alike — each tagged with where it is
7//! written ([`LinkSite`]) and how it resolves ([`Resolution`]), plus the
8//! [`StructuralFact`]s the same pass raises from traversal state (a document
9//! that would not load, a broken single-parent invariant, and so on). Because
10//! it is read straight from the documents, the census is *ground truth*:
11//! `prov`'s `validate`'s findings, the
12//! [`backlinks`](Graph::backlinks) map, and
13//! reachability ([`reachable_files`](Graph::reachable_files),
14//! [`reachable_documents`](Graph::reachable_documents))
15//! are all views over it, and any stored index heals *toward* the census,
16//! never the reverse.
17//!
18//! Alongside it sits [`tree`]'s materialized [`Node`] walk — the same edges,
19//! but a spanning-only DFS that renders a `contents`/`part_of` outline rather
20//! than a flat link census. See `tree`'s module doc for why it stays a
21//! second walker instead of a view over the census.
22//!
23//! This is the plain-text-workspace promise (crate root docs) made concrete:
24//! follow the links declared in a document's own metadata and body, and the
25//! structure unfolds without a side channel — no cache to trust instead of the
26//! documents themselves. `validate`'s findings and `mutate`'s inbound-rename
27//! maintenance are both built on what is censused here; nothing above this
28//! module re-derives an edge from anywhere but a document's own bytes.
29//!
30//! Housed here: the read primitive ([`load`]) every pass shares, link
31//! resolution ([`resolve`], [`Target`]) built on top of it, the census types
32//! with the spanning-tree walker that fills them in, and the [`tree`] walker.
33//! They stay `impl`ed on [`Graph`] rather
34//! than a graph type of its own, but they no longer *require* it: every
35//! function here is bounded on [`ReadStorage`](crate::fs::ReadStorage) and
36//! [`IdIndex`](crate::index::IdIndex) — the read halves of the two ports — and
37//! on nothing else. That is a compiler-checked statement, not a convention: the
38//! read core cannot write a byte or change a registration, because the traits
39//! it is generic over have no method that could.
40//!
41//! Those two splits exist for a consumer that does not exist yet: a language
42//! server, a renderer, a browser viewer — anything that must traverse a
43//! workspace without the authority to change it, and without linking the
44//! machinery that would. Narrowing the bounds is the step that proves such a
45//! consumer is *possible*; extracting a `prov-graph` crate is the step that
46//! makes it *cheap*, and follows from here as a file move rather than a
47//! redesign.
48//!
49//! `graph` is also the crate's sole *surface* onto
50//! [`ReadStorage`](crate::fs::ReadStorage): every other module reaches the
51//! filesystem for reads through a `Workspace` method housed here rather than
52//! calling `self.fs()` directly. Most of that surface is [`load`] — clamped
53//! against root escape and served from the read-scope memo — but a handful of
54//! call sites (existence checks, a directory listing, a raw byte read for
55//! something that is not a document) never wanted the clamp or the memo; those
56//! go through [`probe`]'s raw primitives instead.
57//!
58//! **What this module does not depend on.** `graph` imports the mechanism
59//! layers below it — [`crate::document`], [`crate::link`], [`crate::title`],
60//! [`crate::identity`], and the generic [`crate::index::IdIndex`] — but
61//! never a *policy* module (`crate::config`, `crate::validate`,
62//! `crate::about`). The census walk raises [`StructuralFact`]s rather than
63//! `prov`'s `Finding`s for exactly this reason: `Finding`
64//! is `validate`'s vocabulary, and a walker that constructed one directly
65//! would pull that policy layer's whole enum (and its config-, fixity-, and
66//! vocabulary-flavored variants) down into the read core. `validate::check`
67//! derives each `Finding` from a `StructuralFact` or a [`CensusEntry`]'s
68//! [`Resolution`] one for one — the walk already knows exactly what
69//! happened; `validate` only names it.
70//! [`resolve_link_with`](Graph::resolve_link_with)'s
71//! only reach beyond a bare path/id resolver is [`crate::title::TitleIndex`], which
72//! is itself a derived cache with no policy of its own (DESIGN §5) — the same
73//! dependency [`census`] already carries. That is a stable seam, not a design
74//! gap the coupling papers over, so no `Resolve` trait was introduced here: it
75//! would exist only to abstract a single already-generic parameter
76//! (`Ix: IdIndex`) and a self-contained cache type, and would cost a layer
77//! of indirection for no dependency this module does not already own.
78//!
79//! [`crate::peer`] is not that trait arriving late. It abstracts a dependency
80//! this module genuinely does not have — a map from a workspace *name* to a
81//! location, which only a host holds — and nothing here consumes it: no method
82//! below takes a resolver and [`Graph`] grows no third parameter for one.
83//! Following a foreign reference is a step a caller takes after resolution
84//! returns [`Target::Foreign`], so a traversal that never follows one is
85//! unchanged by the port existing.
86
87pub mod census;
88pub mod load;
89pub mod manifest;
90pub mod probe;
91pub mod resolve;
92pub mod scan;
93pub mod shadow;
94pub mod tree;
95
96pub use census::{Backlink, CensusEntry, LinkSite, Resolution, StructuralFact, inbound, invert};
97pub use census::{Walk, reachable_set};
98pub use resolve::Target;
99pub use shadow::sidecar_candidates;
100pub use tree::{Node, NodeKind, TreeOptions};
101
102use std::path::{Path, PathBuf};
103use std::sync::{Arc, Mutex};
104
105use crate::identity::IdStorage;
106use crate::memo::{ReadMemo, ReadScope};
107use crate::relation::RelationSet;
108
109/// The settings a *read* of a workspace depends on — the whole of what
110/// traversal needs to be told about the workspace it is traversing.
111///
112/// Three fields out of the ten a full workspace is configured with, and the cut
113/// is not arbitrary: these are the only ones that change what a link *resolves
114/// to*. [`relations`](Self::relations) says which metadata fields are edges at
115/// all; [`workspace_id`](Self::workspace_id) is what lets a foreign
116/// `id:<ws>/<id>` reference be recognized as pointing back here rather than
117/// away; [`id_storage`](Self::id_storage) says whether a document's own
118/// frontmatter is a place an id can be found. The other seven — link style,
119/// reference style, embed format and style, fixity, history — govern how prov
120/// *writes*, and a reader that never writes has no use for any of them.
121#[derive(Debug, Clone)]
122pub struct ReadSettings {
123    /// The relation vocabulary: which metadata fields are links, and which one
124    /// (if any) is the spanning relation the tree walk follows.
125    pub relations: RelationSet,
126    /// What this workspace calls itself — the qualifier a cross-workspace
127    /// reference names it by. Empty means anonymous, so no `id:<ws>/<id>`
128    /// reference can ever be recognized as pointing back here.
129    pub workspace_id: String,
130    /// Where a document's stable id is persisted, and so where resolution may
131    /// look for one.
132    pub id_storage: IdStorage,
133}
134
135impl Default for ReadSettings {
136    fn default() -> Self {
137        Self {
138            relations: RelationSet::diaryx(),
139            workspace_id: String::new(),
140            id_storage: IdStorage::default(),
141        }
142    }
143}
144
145/// A readable workspace: a root, a filesystem to read it through, an id index
146/// to resolve `id:` references against, and the [`ReadSettings`] that say how
147/// its links are spelled.
148///
149/// This is the whole of what traversal needs, and — because `FS` is only ever
150/// bounded by [`ReadStorage`](crate::fs::ReadStorage) and `Ix` by
151/// [`IdIndex`](crate::index::IdIndex) — the whole of what it *can* do. There is
152/// no method here that changes a byte on disk or a registration in the index,
153/// and no way to add one without changing a trait bound in this crate.
154///
155/// `prov`'s `Workspace` owns one of these and forwards every read to it, adding
156/// the identity policy, the change/journal machinery, and the config layer on
157/// top. A consumer that only needs to *see* the workspace — a language server,
158/// a renderer, a viewer — can hold a `Graph` directly and link none of that.
159#[derive(Debug)]
160pub struct Graph<FS, Ix> {
161    fs: FS,
162    root: PathBuf,
163    index: Ix,
164    settings: ReadSettings,
165    /// What the current operation has already read — empty unless a
166    /// [`read_scope`](Graph::read_scope) is open. See [`crate::memo`].
167    ///
168    /// Interior mutability because the passes that benefit take `&self`: a
169    /// census is a read-only operation, and making it `&mut` to let it remember
170    /// what it read would be the tail wagging the dog.
171    memo: Arc<Mutex<ReadMemo>>,
172}
173
174impl<FS, Ix> Graph<FS, Ix> {
175    /// A graph over `fs`, rooted at `root`, resolving ids through `index`.
176    pub fn new(fs: FS, root: impl Into<PathBuf>, index: Ix, settings: ReadSettings) -> Self {
177        Self {
178            fs,
179            root: root.into(),
180            index,
181            settings,
182            memo: Arc::new(Mutex::new(ReadMemo::default())),
183        }
184    }
185
186    /// The underlying filesystem.
187    pub fn fs(&self) -> &FS {
188        &self.fs
189    }
190
191    /// The workspace root every path here is relative to.
192    pub fn root(&self) -> &Path {
193        &self.root
194    }
195
196    /// The absolute path of a workspace-relative one — `root` joined to `rel`.
197    ///
198    /// The two path forms are deliberately kept apart: everything this crate
199    /// returns ([`Node::path`], [`Target::Path`], a [`CensusEntry`]'s source) is
200    /// workspace-relative and root-independent, so a graph can be re-rooted to a
201    /// different directory without touching a single stored path. `fs_path` is
202    /// the one place that independence is given up, for the caller that actually
203    /// needs to open the file.
204    pub fn fs_path(&self, rel: impl AsRef<Path>) -> PathBuf {
205        self.root.join(rel)
206    }
207
208    /// The id index `id:` references resolve through.
209    pub fn index(&self) -> &Ix {
210        &self.index
211    }
212
213    /// The id index, mutably — for an owner that also writes to it.
214    pub fn index_mut(&mut self) -> &mut Ix {
215        &mut self.index
216    }
217
218    /// The settings this graph reads by.
219    pub fn settings(&self) -> &ReadSettings {
220        &self.settings
221    }
222
223    /// The relation vocabulary — which metadata fields are links.
224    pub fn relations(&self) -> &RelationSet {
225        &self.settings.relations
226    }
227
228    /// What this workspace calls itself; empty means anonymous.
229    pub fn workspace_id(&self) -> &str {
230        &self.settings.workspace_id
231    }
232
233    /// Where a document's stable id is persisted.
234    pub fn id_storage(&self) -> IdStorage {
235        self.settings.id_storage
236    }
237
238    /// Open a read scope: within it, a document read twice is parsed once. See
239    /// [`crate::memo`].
240    pub fn read_scope(&self) -> ReadScope {
241        ReadScope::open(&self.memo)
242    }
243
244    /// The memo itself, locked — for an owner that must forget what it wrote.
245    pub fn memo_lock(&self) -> std::sync::MutexGuard<'_, ReadMemo> {
246        crate::memo::lock(&self.memo)
247    }
248
249    pub(crate) fn memo_hit(&self, path: &Path) -> Option<(String, crate::document::Document)> {
250        self.memo.lock().unwrap().get(path)
251    }
252
253    pub(crate) fn memo_remember(&self, path: &Path, text: &str, doc: &crate::document::Document) {
254        self.memo.lock().unwrap().remember(path, text, doc);
255    }
256}
257
258impl<FS: Clone, Ix: Clone> Clone for Graph<FS, Ix> {
259    fn clone(&self) -> Self {
260        Self::new(
261            self.fs.clone(),
262            self.root.clone(),
263            self.index.clone(),
264            self.settings.clone(),
265        )
266    }
267}