use crate::wiki::links::{EvidenceRef, InlineRef};
use provenance_core::{
MessageRole, NodeType, RequirementStatus, ResolutionInputType, ResolutionStatus, RuleSeverity,
RuleStatus, SourceType, ThreadStatus,
};
pub use provenance_store::cache::GapKind;
use serde::Serialize;
mod discovery;
pub use discovery::{
DecisionIndexPage, DomainGroup, DomainIndexPage, DomainState, HomepageDomain,
OpenQuestionNotice, SearchEntry, SearchIndexPage, UnfinishedPage, HOMEPAGE_DOMAIN_ROW_CAP,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PageKind {
ScopeIndex,
DomainIndex,
SearchIndex,
DecisionIndex,
Unfinished,
Requirement,
Resolution,
Rule,
Source,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum RecordKind {
Requirement,
Resolution,
Rule,
Source,
}
impl From<RecordKind> for PageKind {
fn from(kind: RecordKind) -> Self {
match kind {
RecordKind::Requirement => Self::Requirement,
RecordKind::Resolution => Self::Resolution,
RecordKind::Rule => Self::Rule,
RecordKind::Source => Self::Source,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct PageId {
pub kind: RecordKind,
pub record_id: String,
}
impl PageId {
pub fn new(kind: RecordKind, record_id: impl Into<String>) -> Self {
Self {
kind,
record_id: record_id.into(),
}
}
pub fn route(&self) -> String {
crate::wiki::routes::WikiRoute::Record(self).path()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct PageLink {
pub target: PageId,
pub title: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct GapNotice {
pub kind: GapKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub subject: Option<PageLink>,
#[serde(skip_serializing_if = "Option::is_none")]
pub related: Option<PageLink>,
pub detail: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct OrphanRecord {
pub link: PageLink,
pub reason: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct OrphanReport {
pub sources: Vec<OrphanRecord>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
pub struct CorpusCounts {
pub sources: usize,
pub requirements: usize,
pub resolutions: usize,
pub rules: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ScopeIndexPage {
pub scope: String,
pub title: String,
pub counts: CorpusCounts,
pub search_coverage: String,
pub search_example: Option<String>,
pub domains: Vec<HomepageDomain>,
pub authored_domain_count: usize,
pub unfinished_count: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct LineageEntry {
pub link: PageLink,
pub is_current: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct InputCitation {
pub input_type: ResolutionInputType,
pub summary: String,
pub reference: EvidenceRef,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct SourceCitation {
pub link: PageLink,
pub source_type: SourceType,
#[serde(skip_serializing_if = "Option::is_none")]
pub clause: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reference: Option<EvidenceRef>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct DecisionSection {
pub link: PageLink,
pub status: ResolutionStatus,
pub position: String,
pub rationale: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enforcement: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub confidence: Option<f64>,
pub inputs: Vec<InputCitation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub made_by: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub approved_by: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub approved_at: Option<i64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct RuleCard {
pub link: PageLink,
pub statement: String,
pub status: RuleStatus,
pub severity: RuleSeverity,
pub evidence: Vec<EvidenceRef>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct FieldNote {
pub message_id: String,
pub role: MessageRole,
pub created_at: i64,
pub body: String,
pub refs: Vec<InlineRef>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct EvidenceThread {
pub thread_id: String,
pub parent_type: NodeType,
pub parent_id: String,
pub status: ThreadStatus,
pub messages: Vec<FieldNote>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct RequirementPage {
pub id: PageId,
pub title: String,
pub status: RequirementStatus,
pub statement: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fog: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub domain_id: Option<String>,
pub domain_has_anchor: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub back_link: Option<PageLink>,
pub lineage: Vec<LineageEntry>,
pub decisions: Vec<DecisionSection>,
pub produced_rules: Vec<RuleCard>,
pub children: Vec<PageLink>,
pub siblings: Vec<PageLink>,
pub supersedes: Vec<PageLink>,
pub depends_on: Vec<PageLink>,
#[serde(skip_serializing_if = "Option::is_none")]
pub superseded_by: Option<PageLink>,
pub sources: Vec<SourceCitation>,
pub gaps: Vec<GapNotice>,
pub threads: Vec<EvidenceThread>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct ResolutionPage {
pub id: PageId,
pub title: String,
pub status: ResolutionStatus,
pub position: String,
pub rationale: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enforcement: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub confidence: Option<f64>,
pub inputs: Vec<InputCitation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub made_by: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub approved_by: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub approved_at: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub review_on: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub superseded_by: Option<PageLink>,
pub resolves: Vec<PageLink>,
pub spawned: Vec<PageLink>,
pub produced_rules: Vec<RuleCard>,
pub gaps: Vec<GapNotice>,
pub threads: Vec<EvidenceThread>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ImplementationBinding {
pub symbol: Option<String>,
pub location: EvidenceRef,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct CodeScan {
#[serde(skip_serializing_if = "Option::is_none")]
pub commit: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct VerificationSite {
pub method: String,
pub symbol: Option<String>,
pub location: EvidenceRef,
pub outside_implementation_module: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct RulePage {
pub id: PageId,
pub title: String,
pub statement: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub status: RuleStatus,
pub severity: RuleSeverity,
#[serde(skip_serializing_if = "Option::is_none")]
pub code_scan: Option<CodeScan>,
pub implementations: Vec<ImplementationBinding>,
pub verifications: Vec<VerificationSite>,
pub produced_by: Vec<PageLink>,
pub requirements: Vec<PageLink>,
pub sources: Vec<PageLink>,
pub gaps: Vec<GapNotice>,
pub threads: Vec<EvidenceThread>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct SourcePage {
pub id: PageId,
pub title: String,
pub source_type: SourceType,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reference: Option<EvidenceRef>,
#[serde(skip_serializing_if = "Option::is_none")]
pub commit_pin: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub effective_date: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub review_date: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub superseded_by: Option<PageLink>,
pub referenced_requirements: Vec<PageLink>,
pub gaps: Vec<GapNotice>,
pub threads: Vec<EvidenceThread>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct WikiCorpus {
pub scope: String,
pub index: ScopeIndexPage,
pub domains: DomainIndexPage,
pub search: SearchIndexPage,
pub decisions: DecisionIndexPage,
pub unfinished: UnfinishedPage,
pub requirements: Vec<RequirementPage>,
pub resolutions: Vec<ResolutionPage>,
pub rules: Vec<RulePage>,
pub sources: Vec<SourcePage>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn singleton_pages_are_not_record_identities() {
let singletons = [
PageKind::ScopeIndex,
PageKind::DomainIndex,
PageKind::SearchIndex,
PageKind::DecisionIndex,
PageKind::Unfinished,
];
assert_eq!(singletons.len(), 5);
}
#[test]
fn page_id_routes_records_under_their_kind() {
assert_eq!(
PageId::new(RecordKind::Requirement, "req_split").route(),
"/requirements/req_split/"
);
assert_eq!(
PageId::new(RecordKind::Resolution, "res_split").route(),
"/resolutions/res_split/"
);
assert_eq!(
PageId::new(RecordKind::Rule, "rule_sah_inv_001").route(),
"/rules/rule_sah_inv_001/"
);
assert_eq!(
PageId::new(RecordKind::Source, "source_schads").route(),
"/sources/source_schads/"
);
}
}