use crate::cache::Cache;
use crate::change::{Departed, Prior};
use crate::context::{Context, Date};
use crate::instance::{Input, Instance, Outcome};
use headwater_census::census::{Census, Outcome as Classification};
use headwater_census::resolve::Step;
use headwater_doc::Body;
use headwater_graph::{Direction, Edge, Graph, Target, Trouble};
use headwater_yaml::{Mapping, Span};
const NO_DOCUMENT: &str = "the row carries no document to read";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Grain {
Document,
Edge,
Neighbourhood { depth: u8 },
Corpus,
Taxonomy,
}
impl Grain {
pub fn name(self) -> &'static str {
match self {
Grain::Document => "document",
Grain::Edge => "edge",
Grain::Neighbourhood { .. } => "neighbourhood",
Grain::Corpus => "corpus",
Grain::Taxonomy => "taxonomy",
}
}
pub fn routes(self) -> bool {
match self {
Grain::Document | Grain::Edge | Grain::Neighbourhood { .. } => true,
Grain::Corpus | Grain::Taxonomy => false,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Scope {
grain: Grain,
needs_body: bool,
needs_phase_a: bool,
needs_clock: bool,
needs_prior: bool,
needs_claims: bool,
}
impl Scope {
pub(crate) const fn document(
needs_body: bool,
needs_phase_a: bool,
needs_clock: bool,
needs_prior: bool,
) -> Self {
Scope {
grain: Grain::Document,
needs_body,
needs_phase_a,
needs_clock,
needs_prior,
needs_claims: false,
}
}
pub(crate) const fn edge(needs_clock: bool) -> Self {
Scope {
grain: Grain::Edge,
needs_body: false,
needs_phase_a: false,
needs_clock,
needs_prior: false,
needs_claims: false,
}
}
pub(crate) const fn neighbourhood(needs_clock: bool) -> Self {
Scope {
grain: Grain::Neighbourhood { depth: 1 },
needs_body: false,
needs_phase_a: false,
needs_clock,
needs_prior: false,
needs_claims: false,
}
}
pub(crate) const fn corpus(needs_phase_a: bool, needs_prior: bool, needs_claims: bool) -> Self {
Scope {
grain: Grain::Corpus,
needs_body: false,
needs_phase_a,
needs_clock: false,
needs_prior,
needs_claims,
}
}
pub(crate) const fn taxonomy() -> Self {
Scope {
grain: Grain::Taxonomy,
needs_body: false,
needs_phase_a: false,
needs_clock: false,
needs_prior: false,
needs_claims: false,
}
}
pub fn grain(&self) -> Grain {
self.grain
}
pub fn needs_body(&self) -> bool {
self.needs_body
}
pub fn needs_phase_a(&self) -> bool {
self.needs_phase_a
}
pub fn needs_clock(&self) -> bool {
self.needs_clock
}
pub fn needs_prior(&self) -> bool {
self.needs_prior
}
pub fn needs_claims(&self) -> bool {
self.needs_claims
}
pub fn render(&self) -> String {
let carries = match (self.grain, self.needs_body) {
(Grain::Document, false) => "one document and its front matter",
(Grain::Document, true) => "one document, its front matter and its body",
(Grain::Edge, _) => "one relation instance and both endpoints",
(Grain::Neighbourhood { .. }, _) => {
"one document and the documents one relation away from it"
}
(Grain::Corpus, _) => "every row of the census",
(Grain::Taxonomy, _) => "the resolved taxonomy, and no document",
};
let phase_a = match (self.needs_phase_a, self.grain) {
(false, _) => "",
(true, Grain::Corpus) => {
", and what phase A could not make of each document's identity"
}
(true, _) => ", and what phase A could not make of it",
};
let clock = match self.needs_clock {
true => ", and the injected clock",
false => "",
};
let prior = match (self.needs_prior, self.grain) {
(false, _) => "",
(true, Grain::Corpus) => {
", and the version of each path the change named that no row of this corpus holds"
}
(true, _) => ", and the version of it that stood before the change",
};
let claims = match self.needs_claims {
true => ", and the identifier claim store",
false => "",
};
let barrier = match self.grain {
Grain::Corpus => ", and it is a barrier",
_ => "",
};
format!(
"{} scope, {carries}{phase_a}{clock}{prior}{claims}{barrier}",
self.grain.name()
)
}
}
pub type ExportTargets = &'static [&'static str];
pub trait DocumentCheck {
const RULE: &'static str;
const VERSION: u32;
const EXPORTABLE_AS: ExportTargets = &[];
const NEEDS_BODY: bool = false;
const NEEDS_PHASE_A: bool = false;
const NEEDS_CLOCK: bool = false;
const NEEDS_PRIOR: bool = false;
fn instantiates(&self, kind: &str) -> bool {
let _ = kind;
true
}
fn evaluate(&self, view: &DocumentView<'_>) -> Outcome;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EdgeUnit {
Pair,
Entry,
}
pub trait EdgeCheck {
const RULE: &'static str;
const VERSION: u32;
const EXPORTABLE_AS: ExportTargets = &[];
const NEEDS_CLOCK: bool = false;
const UNIT: EdgeUnit = EdgeUnit::Pair;
fn instantiates(&self, relation: &str) -> bool {
let _ = relation;
true
}
fn evaluate(&self, view: &EdgeView<'_>) -> Outcome;
}
pub trait CorpusCheck {
const RULE: &'static str;
const VERSION: u32;
const EXPORTABLE_AS: ExportTargets = &[];
const NEEDS_PHASE_A: bool = false;
const NEEDS_PRIOR: bool = false;
const NEEDS_CLAIMS: bool = false;
const NEEDS_LINKS: bool = false;
const NEEDS_ANCHORS: bool = false;
fn evaluate(&self, view: &CorpusView<'_>) -> Outcome;
}
pub trait NeighbourhoodCheck {
const RULE: &'static str;
const VERSION: u32;
const EXPORTABLE_AS: ExportTargets = &[];
const NEEDS_CLOCK: bool = false;
fn instantiates(&self, kind: &str) -> bool {
let _ = kind;
true
}
fn evaluate(&self, view: &NeighbourhoodView<'_>) -> Outcome;
}
pub fn document_scope<C: DocumentCheck>() -> Scope {
Scope::document(
C::NEEDS_BODY,
C::NEEDS_PHASE_A,
C::NEEDS_CLOCK,
C::NEEDS_PRIOR,
)
}
pub fn edge_scope<C: EdgeCheck>() -> Scope {
Scope::edge(C::NEEDS_CLOCK)
}
pub fn neighbourhood_scope<C: NeighbourhoodCheck>() -> Scope {
Scope::neighbourhood(C::NEEDS_CLOCK)
}
pub fn corpus_scope<C: CorpusCheck>() -> Scope {
Scope::corpus(C::NEEDS_PHASE_A, C::NEEDS_PRIOR, C::NEEDS_CLAIMS)
}
pub fn document_version<C: DocumentCheck>() -> u32 {
C::VERSION
}
pub fn edge_version<C: EdgeCheck>() -> u32 {
C::VERSION
}
pub fn neighbourhood_version<C: NeighbourhoodCheck>() -> u32 {
C::VERSION
}
pub fn corpus_version<C: CorpusCheck>() -> u32 {
C::VERSION
}
pub fn document_exports<C: DocumentCheck>() -> ExportTargets {
C::EXPORTABLE_AS
}
pub fn edge_exports<C: EdgeCheck>() -> ExportTargets {
C::EXPORTABLE_AS
}
pub fn neighbourhood_exports<C: NeighbourhoodCheck>() -> ExportTargets {
C::EXPORTABLE_AS
}
pub fn corpus_exports<C: CorpusCheck>() -> ExportTargets {
C::EXPORTABLE_AS
}
fn clock_for(scope: Scope, ctx: &Context) -> Option<Date> {
match scope.needs_clock() {
true => Some(ctx.now()),
false => None,
}
}
enum PriorFor<'a> {
NotDeclared,
Bound(Prior<'a>),
Skip(String),
}
pub const CHANGE_SCOPED_ONLY: &str = "change-scoped-only";
fn prior_for<'a>(scope: Scope, ctx: &'a Context, path: &str) -> PriorFor<'a> {
if !scope.needs_prior() {
return PriorFor::NotDeclared;
}
let Some(change) = ctx.change() else {
return PriorFor::Skip(format!(
"{CHANGE_SCOPED_ONLY}: the prior version is available only in change-scoped \
evaluation, and this run carries no change"
));
};
match change.prior_of(path) {
Ok(prior) => PriorFor::Bound(prior),
Err(why) => PriorFor::Skip(why.to_string()),
}
}
pub struct DocumentView<'a> {
path: &'a str,
digest: Option<&'a str>,
kind: &'a str,
placed_on: Option<&'a str>,
facets: &'a Mapping,
body: Option<&'a Body>,
phase_a: Option<Trouble<'a>>,
clock: Option<Date>,
prior: Option<Prior<'a>>,
}
impl<'a> DocumentView<'a> {
pub fn path(&self) -> &'a str {
self.path
}
pub fn kind(&self) -> &'a str {
self.kind
}
pub fn placed_on(&self) -> Option<&'a str> {
self.placed_on
}
pub fn facets(&self) -> &'a Mapping {
self.facets
}
pub fn body(&self) -> Option<&'a Body> {
self.body
}
pub fn phase_a(&self) -> Option<&Trouble<'a>> {
self.phase_a.as_ref()
}
pub fn now(&self) -> Option<Date> {
self.clock
}
pub fn prior(&self) -> Option<Prior<'a>> {
self.prior
}
pub fn reads(&self) -> Vec<Input> {
vec![Input::new(self.path, self.digest)]
}
}
pub struct EdgeView<'a> {
relation: &'a str,
declared: Option<&'a Edge>,
inverse: Option<&'a Edge>,
ends: Option<(EdgeEnd<'a>, EdgeEnd<'a>)>,
clock: Option<Date>,
reads: Vec<Input>,
resolution: String,
}
#[derive(Clone, Copy, Debug)]
pub struct EdgeEnd<'a> {
pub id: &'a str,
pub path: &'a str,
pub kind: &'a str,
facets: Option<&'a Mapping>,
}
impl<'a> EdgeEnd<'a> {
pub fn facets(&self) -> Option<&'a Mapping> {
self.facets
}
}
impl<'a> EdgeView<'a> {
fn over(
halves: &[&'a Edge],
census: &'a Census,
digests: &Digests,
clock: Option<Date>,
) -> Option<Self> {
let declared = halves
.iter()
.copied()
.find(|edge| edge.direction == Direction::AsDeclared);
let inverse = halves
.iter()
.copied()
.find(|edge| edge.direction == Direction::Inverse);
let anchor = declared.or(inverse)?;
let mut reads = vec![digests.input(&anchor.source.path)];
if let Target::Document { path, .. } = &anchor.target {
if !reads.iter().any(|input| &input.path == path) {
reads.push(digests.input(path));
}
}
let ends = match &anchor.target {
Target::Document { id, path, kind } => {
let writer = EdgeEnd {
id: &anchor.source.id,
path: &anchor.source.path,
kind: &anchor.source.kind,
facets: facets_of(census, &anchor.source.path),
};
let other = EdgeEnd {
id,
path,
kind,
facets: facets_of(census, path),
};
Some(match anchor.direction {
Direction::AsDeclared => (writer, other),
Direction::Inverse => (other, writer),
})
}
_ => None,
};
Some(EdgeView {
relation: anchor.declared.as_str(),
declared,
inverse,
ends,
clock,
reads,
resolution: anchor.target.resolution(),
})
}
fn resolution(&self) -> &str {
&self.resolution
}
pub fn relation(&self) -> &'a str {
self.relation
}
pub fn declared_half(&self) -> Option<&'a Edge> {
self.declared
}
pub fn inverse_half(&self) -> Option<&'a Edge> {
self.inverse
}
pub fn ends(&self) -> Option<(EdgeEnd<'a>, EdgeEnd<'a>)> {
self.ends
}
pub fn now(&self) -> Option<Date> {
self.clock
}
pub fn reads(&self) -> &[Input] {
&self.reads
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum End {
Source,
Target,
}
#[derive(Clone, Copy, Debug)]
pub struct Neighbour<'a> {
pub relation: &'a str,
pub end: End,
pub path: &'a str,
pub kind: &'a str,
pub id: &'a str,
pub span: Span,
}
pub struct NeighbourhoodView<'a> {
path: &'a str,
kind: &'a str,
facets: &'a Mapping,
neighbours: Vec<Neighbour<'a>>,
clock: Option<Date>,
reads: Vec<Input>,
}
impl<'a> NeighbourhoodView<'a> {
pub fn path(&self) -> &'a str {
self.path
}
pub fn kind(&self) -> &'a str {
self.kind
}
pub fn facets(&self) -> &'a Mapping {
self.facets
}
pub fn neighbours(&self) -> &[Neighbour<'a>] {
&self.neighbours
}
pub fn now(&self) -> Option<Date> {
self.clock
}
pub fn reads(&self) -> &[Input] {
&self.reads
}
}
pub struct CorpusView<'a> {
identity: Option<&'a [headwater_graph::index::Reported]>,
departed: &'a [Departed<'a>],
claims: Option<&'a crate::claim::Claims>,
links: Option<&'a [headwater_graph::links::Link]>,
anchors: Option<&'a crate::fragment::Anchors>,
reads: Vec<Input>,
}
impl<'a> CorpusView<'a> {
pub fn identity(&self) -> Option<&'a [headwater_graph::index::Reported]> {
self.identity
}
pub fn departed(&self) -> &[Departed<'a>] {
self.departed
}
pub fn claims(&self) -> Option<&'a crate::claim::Claims> {
self.claims
}
pub fn links(&self) -> Option<&'a [headwater_graph::links::Link]> {
self.links
}
pub fn anchors(&self) -> Option<&'a crate::fragment::Anchors> {
self.anchors
}
pub fn reads(&self) -> &[Input] {
&self.reads
}
#[cfg(test)]
pub(crate) fn only_links(links: Option<&'a [headwater_graph::links::Link]>) -> Self {
CorpusView {
identity: None,
departed: &[],
claims: None,
links,
anchors: None,
reads: Vec::new(),
}
}
#[cfg(test)]
pub(crate) fn only_links_and_anchors(
links: Option<&'a [headwater_graph::links::Link]>,
anchors: Option<&'a crate::fragment::Anchors>,
) -> Self {
CorpusView {
identity: None,
departed: &[],
claims: None,
links,
anchors,
reads: Vec::new(),
}
}
}
pub struct Digests {
by_path: Vec<(String, Option<String>)>,
}
impl Digests {
pub fn of(census: &Census) -> Self {
Digests {
by_path: census
.rows
.iter()
.map(|row| (row.path.clone(), row.digest.clone()))
.collect(),
}
}
pub fn input(&self, path: &str) -> Input {
let digest = self
.by_path
.binary_search_by(|(known, _)| known.as_str().cmp(path))
.ok()
.and_then(|index| self.by_path[index].1.as_deref());
Input::new(path, digest)
}
}
fn facets_of<'a>(census: &'a Census, path: &str) -> Option<&'a Mapping> {
census
.rows
.binary_search_by(|row| row.path.as_str().cmp(path))
.ok()
.and_then(|index| census.rows[index].document.as_ref())
.map(|document| &document.facets)
}
pub fn over_documents<C: DocumentCheck>(
check: &C,
census: &Census,
graph: &Graph,
ctx: &Context,
cache: &mut Cache,
) -> Vec<Instance> {
let scope = document_scope::<C>();
let clock = clock_for(scope, ctx);
let mut instances = Vec::new();
for row in &census.rows {
let Classification::Typed { kind, derivation } = &row.outcome else {
continue;
};
if !check.instantiates(kind) {
continue;
}
let prior = match prior_for(scope, ctx, &row.path) {
PriorFor::NotDeclared => None,
PriorFor::Bound(prior) => Some(prior),
PriorFor::Skip(why) => {
cache.undecided();
instances.push(Instance::skipped(
C::RULE,
Grain::Document,
vec![Input::new(&row.path, row.digest.as_deref())],
&why,
));
continue;
}
};
let Some(document) = &row.document else {
cache.undecided();
instances.push(Instance::skipped(
C::RULE,
Grain::Document,
vec![Input::new(&row.path, row.digest.as_deref())],
NO_DOCUMENT,
));
continue;
};
let view = DocumentView {
path: &row.path,
digest: row.digest.as_deref(),
kind,
placed_on: derivation.steps.iter().find_map(|step| match step {
Step::PlacementCarriesTheKind { shelf, .. } => Some(shelf.as_str()),
_ => None,
}),
facets: &document.facets,
body: match C::NEEDS_BODY {
true => Some(&document.body),
false => None,
},
phase_a: match C::NEEDS_PHASE_A {
true => Some(graph.about(&row.path)),
false => None,
},
clock,
prior,
};
let reads = view.reads();
let outcome = cache.outcome(
C::RULE,
C::VERSION,
scope,
&row.path,
&reads,
clock,
prior,
None,
|| check.evaluate(&view),
);
instances.push(Instance::of(C::RULE, Grain::Document, reads, outcome));
}
instances
}
pub fn over_edges<C: EdgeCheck>(
check: &C,
census: &Census,
graph: &Graph,
digests: &Digests,
ctx: &Context,
cache: &mut Cache,
) -> Vec<Instance> {
let scope = edge_scope::<C>();
let clock = clock_for(scope, ctx);
let mut pairs: Vec<(String, Vec<&Edge>)> = Vec::new();
for edge in &graph.edges {
let Some(key) = (match C::UNIT {
EdgeUnit::Pair => edge
.declared_triple()
.and_then(
|(source, relation, target)| match check.instantiates(&relation) {
true => Some(format!("{source}\u{1f}{relation}\u{1f}{target}")),
false => None,
},
),
EdgeUnit::Entry => match check.instantiates(&edge.declared) {
true => {
let (source, name, target) = edge.triple();
Some(format!("{source}\u{1f}{name}\u{1f}{target}"))
}
false => None,
},
}) else {
continue;
};
match pairs.iter_mut().find(|(known, _)| known == &key) {
Some((_, halves)) => halves.push(edge),
None => pairs.push((key, vec![edge])),
}
}
let mut instances = Vec::with_capacity(pairs.len());
for (triple, halves) in &pairs {
let Some(view) = EdgeView::over(halves, census, digests, clock) else {
continue;
};
let reads = view.reads().to_vec();
let outcome = cache.outcome(
C::RULE,
C::VERSION,
scope,
triple,
&reads,
clock,
None,
Some(view.resolution()),
|| check.evaluate(&view),
);
instances.push(Instance::of(C::RULE, Grain::Edge, reads, outcome));
}
instances
}
pub fn over_neighbourhoods<C: NeighbourhoodCheck>(
check: &C,
census: &Census,
graph: &Graph,
digests: &Digests,
ctx: &Context,
cache: &mut Cache,
) -> Vec<Instance> {
let scope = neighbourhood_scope::<C>();
let clock = clock_for(scope, ctx);
let adjacency = Adjacency::of(graph);
let mut instances = Vec::new();
for row in &census.rows {
let Classification::Typed { kind, .. } = &row.outcome else {
continue;
};
if !check.instantiates(kind) {
continue;
}
let Some(document) = &row.document else {
cache.undecided();
instances.push(Instance::skipped(
C::RULE,
Grain::Neighbourhood { depth: 1 },
vec![Input::new(&row.path, row.digest.as_deref())],
NO_DOCUMENT,
));
continue;
};
let neighbours = adjacency.of_path(&row.path);
let mut reads = vec![Input::new(&row.path, row.digest.as_deref())];
for neighbour in &neighbours {
if !reads.iter().any(|input| input.path == neighbour.path) {
reads.push(digests.input(neighbour.path));
}
}
let view = NeighbourhoodView {
path: &row.path,
kind,
facets: &document.facets,
neighbours,
clock,
reads: reads.clone(),
};
let outcome = cache.outcome(
C::RULE,
C::VERSION,
scope,
&row.path,
&reads,
clock,
None,
None,
|| check.evaluate(&view),
);
instances.push(Instance::of(
C::RULE,
Grain::Neighbourhood { depth: 1 },
reads,
outcome,
));
}
instances
}
const CORPUS: &str = "the corpus";
pub fn over_corpus<C: CorpusCheck>(
check: &C,
census: &Census,
graph: &Graph,
claims: &crate::claim::Claims,
ctx: &Context,
cache: &mut Cache,
) -> Vec<Instance> {
let scope = corpus_scope::<C>();
let mut reads: Vec<Input> = census
.rows
.iter()
.filter(|row| row.document.is_some())
.map(|row| Input::new(&row.path, row.digest.as_deref()))
.collect();
let departed = match (C::NEEDS_PRIOR, ctx.change()) {
(false, _) => Vec::new(),
(true, Some(change)) => change.departed(),
(true, None) => {
cache.undecided();
return vec![Instance::skipped(
C::RULE,
Grain::Corpus,
reads,
format!(
"{CHANGE_SCOPED_ONLY}: the prior version is available only in change-scoped \
evaluation, and this run carries no change"
),
)];
}
};
reads.extend(
departed
.iter()
.map(|entry| Input::new(entry.path, Some(entry.digest))),
);
if C::NEEDS_CLAIMS {
let digest = claims.digest();
reads.push(Input::new(crate::claim::STORE, Some(digest.as_str())));
}
let anchors = match C::NEEDS_ANCHORS {
true => Some(crate::fragment::Anchors::of(census)),
false => None,
};
let view = CorpusView {
identity: match C::NEEDS_PHASE_A {
true => Some(&graph.index.defects),
false => None,
},
departed: &departed,
claims: match C::NEEDS_CLAIMS {
true => Some(claims),
false => None,
},
links: match C::NEEDS_LINKS {
true => Some(&graph.links),
false => None,
},
anchors: anchors.as_ref(),
reads: reads.clone(),
};
let outcome = cache.outcome(
C::RULE,
C::VERSION,
scope,
CORPUS,
&reads,
None,
None,
None,
|| check.evaluate(&view),
);
vec![Instance::of(C::RULE, Grain::Corpus, reads, outcome)]
}
struct Adjacency<'a> {
by_path: Vec<(&'a str, Vec<Neighbour<'a>>)>,
}
impl<'a> Adjacency<'a> {
fn of(graph: &'a Graph) -> Self {
let mut adjacency = Adjacency {
by_path: Vec::new(),
};
for edge in &graph.edges {
let Target::Document { id, path, kind } = &edge.target else {
continue;
};
let (near, far) = match edge.direction {
Direction::AsDeclared => (End::Source, End::Target),
Direction::Inverse => (End::Target, End::Source),
};
adjacency.push(
&edge.source.path,
Neighbour {
relation: &edge.declared,
end: near,
path,
kind,
id,
span: edge.span,
},
);
adjacency.push(
path,
Neighbour {
relation: &edge.declared,
end: far,
path: &edge.source.path,
kind: &edge.source.kind,
id: &edge.source.id,
span: edge.span,
},
);
}
adjacency
}
fn push(&mut self, path: &'a str, neighbour: Neighbour<'a>) {
let entry = match self.by_path.iter_mut().find(|(known, _)| *known == path) {
Some(entry) => entry,
None => {
self.by_path.push((path, Vec::new()));
self.by_path.last_mut().expect("just pushed")
}
};
if !entry.1.iter().any(|known| {
known.relation == neighbour.relation
&& known.end == neighbour.end
&& known.path == neighbour.path
}) {
entry.1.push(neighbour);
}
}
fn of_path(&self, path: &str) -> Vec<Neighbour<'a>> {
self.by_path
.iter()
.find(|(known, _)| *known == path)
.map(|(_, neighbours)| neighbours.clone())
.unwrap_or_default()
}
}