RS-3 1: the edge list moves to its owner (docs.rs citations)
docs.rs::citations and CitationRole are the one edge list: the fifteen roles, one per referring leaf field. Admission's A7 references() is a map over them, carrying the Missing classification by role and the hand-off source-side filter, with the same refusals in the same order. kind_of_id and outcome_references leave admission; the CitesResolvedDocument check reads outcome_citations. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
e770fb9f21
commit
511afa2f44
3 changed files with 209 additions and 75 deletions
|
|
@ -37,7 +37,7 @@ use epiphany_pipeline::{
|
|||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::docs::{Docs, Staged};
|
||||
use crate::docs::{CitationRole, Docs, Staged, citations, kind_of_id, outcome_citations};
|
||||
use crate::mind::{HuginnMindEpoch, Mind, unavailable};
|
||||
use crate::receipt::{self, CommitOutcome, PipelineProvenance};
|
||||
use crate::refusal::MindRefusal;
|
||||
|
|
@ -170,13 +170,6 @@ impl<S: MindStore> Mind<S> {
|
|||
}
|
||||
}
|
||||
|
||||
/// The kind segment of a full id, for the one field typed `Short` whose
|
||||
/// referent may be of any kind.
|
||||
fn kind_of_id(id: &str) -> Option<PipelineKind> {
|
||||
let name = id.split(':').nth(1)?;
|
||||
PipelineKind::ALL.iter().copied().find(|kind| kind.name() == name)
|
||||
}
|
||||
|
||||
/// A3 through the leaf, then A5.
|
||||
fn stage(envelope: CultCacheEnvelope, mind: &Slug) -> Result<Staged, MindRefusal> {
|
||||
validate_pipeline_write_envelope(&envelope)?;
|
||||
|
|
@ -212,74 +205,35 @@ struct Reference {
|
|||
missing: Missing,
|
||||
}
|
||||
|
||||
fn reference(kind: PipelineKind, id: &str) -> Reference {
|
||||
Reference { kind, id: id.to_string(), missing: Missing::Reference }
|
||||
}
|
||||
|
||||
fn referenced(pipeline_ref: &PipelineRef) -> Reference {
|
||||
reference(pipeline_ref.kind, &pipeline_ref.id.0)
|
||||
}
|
||||
|
||||
fn outcome_references(outcome: &ResolutionOutcome) -> Vec<Reference> {
|
||||
match outcome {
|
||||
ResolutionOutcome::Superseded { by } => by
|
||||
.iter()
|
||||
.map(|supersessor| Reference { kind: supersessor.kind, id: supersessor.id.0.clone(), missing: Missing::Supersessor })
|
||||
.collect(),
|
||||
ResolutionOutcome::Answered { by } => vec![referenced(by)],
|
||||
ResolutionOutcome::Fixed { by: Some(by), .. } => vec![referenced(by)],
|
||||
ResolutionOutcome::Deferred { to } => vec![referenced(to)],
|
||||
ResolutionOutcome::Fixed { by: None, .. } | ResolutionOutcome::Recorded { .. } | ResolutionOutcome::Withdrawn { .. } => {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A7's field list: every `PipelineRef` and every full-id `Short` field.
|
||||
/// `Fixed.commit` and `ForeignRef` are syntax only. A hand-off's documents
|
||||
/// are cited on the source side only; the receiving side imports them
|
||||
/// afterwards (Cut 12).
|
||||
/// A7's field list is `docs::citations`: every `PipelineRef` and every
|
||||
/// full-id `Short` field, each carrying the refusal its absence earns. A
|
||||
/// hand-off's documents are cited on the source side only, and each must read
|
||||
/// as an id of a kind; the receiving side imports them afterwards (Cut 12).
|
||||
fn references(staged: &Staged, mind: &Slug) -> Result<Vec<Reference>, MindRefusal> {
|
||||
use PipelineDocument as D;
|
||||
use PipelineKind as K;
|
||||
let mut refs = Vec::new();
|
||||
match &staged.document {
|
||||
D::Question(question) => refs.extend(question.raised_in.as_ref().map(referenced)),
|
||||
D::Ruling(ruling) => refs.extend(ruling.answers.as_ref().map(|question| reference(K::Question, &question.0))),
|
||||
D::CutSpec(spec) => {
|
||||
refs.extend(spec.rulings.iter().map(|ruling| reference(K::Ruling, &ruling.0)));
|
||||
refs.extend(spec.questions.iter().map(|question| reference(K::Question, &question.0)));
|
||||
if let PipelineDocument::HandOff(hand_off) = &staged.document {
|
||||
if hand_off.from_instance != *mind {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
D::CutReport(report) => {
|
||||
refs.push(Reference {
|
||||
kind: K::CutSpec,
|
||||
id: report.cut_spec.0.clone(),
|
||||
missing: Missing::SpecOfReport(staged.key.clone()),
|
||||
});
|
||||
refs.extend(report.forks.iter().map(|question| reference(K::Question, &question.0)));
|
||||
}
|
||||
D::Verdict(verdict) => {
|
||||
refs.push(reference(K::CutReport, &verdict.cut_report.0));
|
||||
refs.extend(verdict.claims.iter().flat_map(|claim| claim.findings.iter()).map(|finding| reference(K::Finding, &finding.0)));
|
||||
}
|
||||
D::Finding(finding) => refs.push(reference(K::Verdict, &finding.verdict.0)),
|
||||
D::FollowUp(follow_up) => refs.push(referenced(&follow_up.source)),
|
||||
D::Resolution(resolution) => {
|
||||
refs.push(referenced(&resolution.subject));
|
||||
refs.extend(outcome_references(&resolution.outcome));
|
||||
}
|
||||
D::HandOff(hand_off) if hand_off.from_instance == *mind => {
|
||||
for document in &hand_off.documents {
|
||||
let kind = kind_of_id(&document.0).ok_or_else(|| PipelineRefusal::InvalidFormat {
|
||||
field: "hand_off.documents".into(),
|
||||
value: document.0.clone(),
|
||||
})?;
|
||||
refs.push(reference(kind, &document.0));
|
||||
if let Some(document) = hand_off.documents.iter().find(|document| kind_of_id(&document.0).is_none()) {
|
||||
return Err(PipelineRefusal::InvalidFormat {
|
||||
field: "hand_off.documents".into(),
|
||||
value: document.0.clone(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
D::Campaign(_) | D::Target(_) | D::Instance(_) | D::Stewardship(_) | D::HandOff(_) => {}
|
||||
}
|
||||
Ok(refs)
|
||||
Ok(citations(&staged.document)
|
||||
.into_iter()
|
||||
.map(|(role, target)| Reference {
|
||||
kind: target.kind,
|
||||
id: target.id.0,
|
||||
missing: match role {
|
||||
CitationRole::CutSpec => Missing::SpecOfReport(staged.key.clone()),
|
||||
CitationRole::SupersededBy => Missing::Supersessor,
|
||||
_ => Missing::Reference,
|
||||
},
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// A7: every referent is in the batch or the image; the image ones are the
|
||||
|
|
@ -652,9 +606,9 @@ fn resolution_rule(docs: &Docs, resolution: &PipelineResolution) -> Result<(), M
|
|||
later: later.into(),
|
||||
});
|
||||
}
|
||||
for referent in outcome_references(&resolution.outcome) {
|
||||
if !docs.in_force(referent.kind, &referent.id) {
|
||||
return Err(MindRefusal::CitesResolvedDocument { kind: referent.kind, id: referent.id });
|
||||
for (_, referent) in outcome_citations(&resolution.outcome) {
|
||||
if !docs.in_force(referent.kind, &referent.id.0) {
|
||||
return Err(MindRefusal::CitesResolvedDocument { kind: referent.kind, id: referent.id.0 });
|
||||
}
|
||||
}
|
||||
match &resolution.outcome {
|
||||
|
|
|
|||
|
|
@ -8,8 +8,10 @@
|
|||
use cultcache_rs::CultCacheEnvelope;
|
||||
use epiphany_pipeline::{
|
||||
OrgRepo, PipelineDocument, PipelineKind, PipelineRef, PipelineResolution, PipelineStewardship, ResolutionOutcome,
|
||||
Slug,
|
||||
Short, Slug,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::refusal::MindRefusal;
|
||||
|
||||
|
|
@ -262,3 +264,180 @@ fn later_than(base: &PipelineDocument, other: &PipelineDocument) -> bool {
|
|||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// The referring field of a citation edge, one per leaf field that names
|
||||
/// another document. The wire spelling is the field's own name, so a client
|
||||
/// reads an edge's role as the field it came from.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CitationRole {
|
||||
RaisedIn,
|
||||
Answers,
|
||||
Rulings,
|
||||
Questions,
|
||||
CutSpec,
|
||||
Forks,
|
||||
CutReport,
|
||||
Findings,
|
||||
Verdict,
|
||||
Source,
|
||||
Subject,
|
||||
SupersededBy,
|
||||
ResolvedBy,
|
||||
DeferredTo,
|
||||
Documents,
|
||||
}
|
||||
|
||||
impl CitationRole {
|
||||
pub const ALL: [CitationRole; 15] = [
|
||||
Self::RaisedIn,
|
||||
Self::Answers,
|
||||
Self::Rulings,
|
||||
Self::Questions,
|
||||
Self::CutSpec,
|
||||
Self::Forks,
|
||||
Self::CutReport,
|
||||
Self::Findings,
|
||||
Self::Verdict,
|
||||
Self::Source,
|
||||
Self::Subject,
|
||||
Self::SupersededBy,
|
||||
Self::ResolvedBy,
|
||||
Self::DeferredTo,
|
||||
Self::Documents,
|
||||
];
|
||||
|
||||
/// The wire spelling, equal to the referring field's name.
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::RaisedIn => "raised_in",
|
||||
Self::Answers => "answers",
|
||||
Self::Rulings => "rulings",
|
||||
Self::Questions => "questions",
|
||||
Self::CutSpec => "cut_spec",
|
||||
Self::Forks => "forks",
|
||||
Self::CutReport => "cut_report",
|
||||
Self::Findings => "findings",
|
||||
Self::Verdict => "verdict",
|
||||
Self::Source => "source",
|
||||
Self::Subject => "subject",
|
||||
Self::SupersededBy => "superseded_by",
|
||||
Self::ResolvedBy => "resolved_by",
|
||||
Self::DeferredTo => "deferred_to",
|
||||
Self::Documents => "documents",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_name(name: &str) -> Option<Self> {
|
||||
Self::ALL.into_iter().find(|role| role.name() == name)
|
||||
}
|
||||
|
||||
/// The kind whose documents carry this field.
|
||||
pub fn carrier(self) -> PipelineKind {
|
||||
use PipelineKind as K;
|
||||
match self {
|
||||
Self::RaisedIn => K::Question,
|
||||
Self::Answers => K::Ruling,
|
||||
Self::Rulings | Self::Questions => K::CutSpec,
|
||||
Self::CutSpec | Self::Forks => K::CutReport,
|
||||
Self::CutReport | Self::Findings => K::Verdict,
|
||||
Self::Verdict => K::Finding,
|
||||
Self::Source => K::FollowUp,
|
||||
Self::Subject | Self::SupersededBy | Self::ResolvedBy | Self::DeferredTo => K::Resolution,
|
||||
Self::Documents => K::HandOff,
|
||||
}
|
||||
}
|
||||
|
||||
/// The kinds a referent of this field may be: the one kind the field is
|
||||
/// typed to, or every kind for the fields typed `PipelineRef` or a full
|
||||
/// id of any kind. "Any" is spelled as all thirteen, never as none.
|
||||
pub fn target_kinds(self) -> Vec<PipelineKind> {
|
||||
use PipelineKind as K;
|
||||
match self {
|
||||
Self::Answers | Self::Questions | Self::Forks => vec![K::Question],
|
||||
Self::Rulings => vec![K::Ruling],
|
||||
Self::CutSpec => vec![K::CutSpec],
|
||||
Self::CutReport => vec![K::CutReport],
|
||||
Self::Findings => vec![K::Finding],
|
||||
Self::Verdict => vec![K::Verdict],
|
||||
Self::RaisedIn
|
||||
| Self::Source
|
||||
| Self::Subject
|
||||
| Self::SupersededBy
|
||||
| Self::ResolvedBy
|
||||
| Self::DeferredTo
|
||||
| Self::Documents => PipelineKind::ALL.to_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The kind segment of a full id, for the fields typed `Short` whose referent
|
||||
/// may be of any kind.
|
||||
pub(crate) fn kind_of_id(id: &str) -> Option<PipelineKind> {
|
||||
let name = id.split(':').nth(1)?;
|
||||
PipelineKind::ALL.iter().copied().find(|kind| kind.name() == name)
|
||||
}
|
||||
|
||||
fn cited(kind: PipelineKind, id: &Short) -> PipelineRef {
|
||||
PipelineRef { kind, id: id.clone() }
|
||||
}
|
||||
|
||||
/// What a resolution's outcome names, by the field that names it.
|
||||
pub(crate) fn outcome_citations(outcome: &ResolutionOutcome) -> Vec<(CitationRole, PipelineRef)> {
|
||||
match outcome {
|
||||
ResolutionOutcome::Superseded { by } => by.iter().map(|by| (CitationRole::SupersededBy, by.clone())).collect(),
|
||||
ResolutionOutcome::Answered { by } => vec![(CitationRole::ResolvedBy, by.clone())],
|
||||
ResolutionOutcome::Fixed { by: Some(by), .. } => vec![(CitationRole::ResolvedBy, by.clone())],
|
||||
ResolutionOutcome::Deferred { to } => vec![(CitationRole::DeferredTo, to.clone())],
|
||||
ResolutionOutcome::Fixed { by: None, .. } | ResolutionOutcome::Recorded { .. } | ResolutionOutcome::Withdrawn { .. } => {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Every citation edge a document carries, derived from its leaf fields each
|
||||
/// time it is asked and never stored: the one edge list. Admission's A7 reads
|
||||
/// it to require the referents, and the read side's rows read it as their
|
||||
/// references. `Fixed.commit`, `ForeignRef` precedents and the labels
|
||||
/// (`depends_on`, `invariants`, `promise`) are not edges. A hand-off's
|
||||
/// `documents` entry whose kind segment does not read cites nothing here;
|
||||
/// admission refuses such an entry on the source side before it asks.
|
||||
pub(crate) fn citations(document: &PipelineDocument) -> Vec<(CitationRole, PipelineRef)> {
|
||||
use CitationRole as R;
|
||||
use PipelineDocument as D;
|
||||
use PipelineKind as K;
|
||||
let mut edges = Vec::new();
|
||||
match document {
|
||||
D::Question(question) => edges.extend(question.raised_in.iter().map(|to| (R::RaisedIn, to.clone()))),
|
||||
D::Ruling(ruling) => edges.extend(ruling.answers.iter().map(|to| (R::Answers, cited(K::Question, to)))),
|
||||
D::CutSpec(spec) => {
|
||||
edges.extend(spec.rulings.iter().map(|to| (R::Rulings, cited(K::Ruling, to))));
|
||||
edges.extend(spec.questions.iter().map(|to| (R::Questions, cited(K::Question, to))));
|
||||
}
|
||||
D::CutReport(report) => {
|
||||
edges.push((R::CutSpec, cited(K::CutSpec, &report.cut_spec)));
|
||||
edges.extend(report.forks.iter().map(|to| (R::Forks, cited(K::Question, to))));
|
||||
}
|
||||
D::Verdict(verdict) => {
|
||||
edges.push((R::CutReport, cited(K::CutReport, &verdict.cut_report)));
|
||||
edges.extend(
|
||||
verdict
|
||||
.claims
|
||||
.iter()
|
||||
.flat_map(|claim| claim.findings.iter())
|
||||
.map(|to| (R::Findings, cited(K::Finding, to))),
|
||||
);
|
||||
}
|
||||
D::Finding(finding) => edges.push((R::Verdict, cited(K::Verdict, &finding.verdict))),
|
||||
D::FollowUp(follow_up) => edges.push((R::Source, follow_up.source.clone())),
|
||||
D::Resolution(resolution) => {
|
||||
edges.push((R::Subject, resolution.subject.clone()));
|
||||
edges.extend(outcome_citations(&resolution.outcome));
|
||||
}
|
||||
D::HandOff(hand_off) => edges.extend(
|
||||
hand_off.documents.iter().filter_map(|to| kind_of_id(&to.0).map(|kind| (R::Documents, cited(kind, to)))),
|
||||
),
|
||||
D::Campaign(_) | D::Target(_) | D::Instance(_) | D::Stewardship(_) => {}
|
||||
}
|
||||
edges
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ pub mod wire;
|
|||
#[cfg(test)]
|
||||
pub(crate) mod fixtures;
|
||||
|
||||
pub use docs::CitationRole;
|
||||
pub use admission::{BATCH_MAX, PipelineAdmissionBatch, PipelineAdmissionOutcome};
|
||||
pub use mind::{HuginnMindEpoch, Mind};
|
||||
pub use query::{
|
||||
|
|
|
|||
Loading…
Reference in a new issue