corpora-core 0.1.0

Core domain types, immutable graph, and event bus for the corpora docs validator.
Documentation
//! The schema (schema-v0) as types: illegal states unrepresentable where cheap.
//! Parsing produces a [`Record`]; a kind-specific [`Facet`] carries the rest.

#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Id(pub String); // "D2", "A1", "F5"

#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct FindingId(pub String); // "r9", "t6", "am-3"

#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct DocPath(pub String);

/// ISO-8601 `YYYY-MM-DD`. Stored as text in v0; structured parsing is Phase 2.
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Date(pub String);

/// A VCS revision (git sha / ref). Resolution is behind `RevisionOracle` in the engine.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Rev(pub String);

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Kind {
    Decision,
    Axiom,
    Invariant,
    Architecture,
    Current,
    Roadmap,
    Milestone,
    Evidence,
    ReviewLog,
    Evolution,
    Handoff,
    Explainer,
    Index,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Lifecycle {
    Draft,
    Current,
    Superseded,
    Historical,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Authority {
    Normative,
    Axiomatic,
    Descriptive,
    Prospective,
    Evidence,
    Historical,
    Operational,
    Explanatory,
    Navigational,
}

/// Adoption state — the decide-timeline. A fork is just `Open`.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Status {
    Open,
    Proposed,
    Accepted,
    Superseded,
    Deprecated,
    Rejected,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Impl {
    Absent,
    Scaffold,
    Partial,
    Implemented,
    Verified,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Door {
    Reversible,
    OneWay,
}

/// Kind-specific payload. Parsing must produce exactly one, validated against `kind`.
#[derive(Clone, Debug)]
pub enum Facet {
    Decision(DecisionFacet),
    /// Assumed external fact (A1–A4): cited, never preserved by code.
    Axiom,
    /// architecture / invariant — canon impl relaxed to *may-carry* (schema-v0 A7).
    Canon {
        implementation: Option<Impl>,
        code_revision: Option<Rev>,
    },
    Current {
        implementation: Impl,
        code_revision: Rev,
        source_revision: Option<Rev>,
    },
    /// roadmap / milestone.
    Plan {
        implementation: Impl,
        code_revision: Rev,
    },
    Evidence {
        measured: Option<(Impl, Rev)>,
        source_revision: Option<Rev>,
    },
    /// review-log / evolution / handoff / explainer / index.
    Narrative,
}

#[derive(Clone, Debug)]
pub struct DecisionFacet {
    pub status: Status,
    pub date: Date,
    /// Present ⇒ held/deferred/done are distinguishable (schema-v0 §3).
    pub implementation: Option<Impl>,
    /// `Some` iff `status == Open`.
    pub fork: Option<Fork>,
    /// roadmap/milestone links — a deferred decision is a *scheduled* one.
    pub realized_by: Vec<Id>,
}

#[derive(Clone, Debug)]
pub struct Fork {
    pub lean: String,
    pub decide_when: String,
    pub door: Door,
}

impl Facet {
    /// The pinned code revision this record carries, if any — what Gate B verifies.
    pub fn code_revision(&self) -> Option<&Rev> {
        match self {
            Facet::Current { code_revision, .. } | Facet::Plan { code_revision, .. } => {
                Some(code_revision)
            }
            Facet::Canon { code_revision, .. } => code_revision.as_ref(),
            Facet::Evidence { measured, .. } => measured.as_ref().map(|(_, rev)| rev),
            _ => None,
        }
    }
}

/// Typed edges between records.
#[derive(Clone, Debug, Default)]
pub struct Edges {
    pub depends_on: Vec<Id>,
    pub supersedes: Vec<Id>,
    pub related: Vec<Id>,
    pub supports: Vec<Id>,
    pub driven_by: Vec<FindingId>,
}

/// Extracted markdown body signals (Phase 2 parser fills these from the prose).
#[derive(Clone, Debug, Default)]
pub struct Body {
    /// Prose occurrences of ids with no typed edge — the E3 *warning* path. Excludes the
    /// record's own id/aka and anything already cited via a structured edge.
    pub bare_mentions: Vec<Id>,
    /// Review-log finding anchors (`<a id="r9">`), in document order.
    pub findings: Vec<Finding>,
    /// `§`-prefixed section references (e.g. `9.6`), for the positional-ref rule.
    pub section_refs: Vec<String>,
    /// Raw link destinations found in the body.
    pub links: Vec<String>,
    /// Ids named by body links — a typed citation (see [`crate::Relation::Link`]), so a
    /// link to a superseded atom is an E3 error like any other structured edge.
    pub link_refs: Vec<Id>,
}

#[derive(Clone, Debug)]
pub struct Finding {
    pub id: FindingId,
    /// Findings carry their own inline status vocabulary (open/refuted/…); text in v0.
    pub status: String,
}

#[derive(Clone, Debug)]
pub struct Record {
    pub id: Option<Id>,
    pub path: DocPath,
    pub kind: Kind,
    pub lifecycle: Lifecycle,
    pub authority: Authority,
    pub last_reviewed: Date,
    /// id-aliases: `id="D2", aka=["F2"]` — resolves the F↔D identity (schema-v0 §5).
    pub aka: Vec<Id>,
    pub edges: Edges,
    pub facet: Facet,
    pub body: Body,
}

impl Record {
    /// Construct a record with empty edges/body and a blank `last_reviewed`.
    /// A convenience for tests and embedding — the real builder is the Phase 2 parser.
    pub fn minimal(
        id: Option<Id>,
        path: DocPath,
        kind: Kind,
        lifecycle: Lifecycle,
        authority: Authority,
        facet: Facet,
    ) -> Self {
        Record {
            id,
            path,
            kind,
            lifecycle,
            authority,
            last_reviewed: Date(String::new()),
            aka: Vec::new(),
            edges: Edges::default(),
            facet,
            body: Body::default(),
        }
    }
}