use crate::error::{Result, YantrikDbError};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Source {
User,
Inference,
Document,
System,
}
impl Source {
pub fn parse(s: &str) -> Result<Self> {
match s.trim().to_ascii_lowercase().as_str() {
"user" => Ok(Self::User),
"inference" => Ok(Self::Inference),
"document" => Ok(Self::Document),
"system" => Ok(Self::System),
other => Err(YantrikDbError::ProvenanceInconsistent {
path: "source",
reason: format!(
"unknown source '{other}' (expected user|inference|document|system)"
),
}),
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::User => "user",
Self::Inference => "inference",
Self::Document => "document",
Self::System => "system",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConfidenceBasis {
Observation,
Asserted,
Confirmation,
Verification,
Inference,
Assumption,
Learned { model: String },
}
impl ConfidenceBasis {
pub fn parse(s: &str) -> Result<Self> {
let t = s.trim();
match t.to_ascii_lowercase().as_str() {
"observation" => return Ok(Self::Observation),
"asserted" => return Ok(Self::Asserted),
"confirmation" => return Ok(Self::Confirmation),
"verification" => return Ok(Self::Verification),
"inference" => return Ok(Self::Inference),
"assumption" => return Ok(Self::Assumption),
_ => {}
}
if let Some(model) = parse_learned(t) {
return Ok(Self::Learned { model });
}
Err(YantrikDbError::ProvenanceInconsistent {
path: "confidence_basis",
reason: format!(
"unknown confidence_basis '{t}' (expected observation|asserted|confirmation|\
verification|inference|assumption|learned(<model>))"
),
})
}
pub fn to_canonical(&self) -> String {
match self {
Self::Observation => "observation".into(),
Self::Asserted => "asserted".into(),
Self::Confirmation => "confirmation".into(),
Self::Verification => "verification".into(),
Self::Inference => "inference".into(),
Self::Assumption => "assumption".into(),
Self::Learned { model } => format!("learned({model})"),
}
}
}
fn parse_learned(s: &str) -> Option<String> {
let body = s.strip_suffix(')')?;
if body.len() < 8 || !body.is_char_boundary(8) {
return None;
}
let (prefix, inner) = body.split_at(8); if !prefix.eq_ignore_ascii_case("learned(") {
return None;
}
if inner.is_empty() || inner.len() > 64 {
return None;
}
if !inner
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
{
return None;
}
Some(inner.to_string())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ClaimKind {
Fact,
Observation,
Inference,
Unspecified,
Other(String),
}
impl ClaimKind {
pub fn parse(kind: Option<&str>) -> Self {
match kind {
None => Self::Unspecified,
Some(k) => {
let t = k.trim();
if t.is_empty() {
return Self::Unspecified;
}
match t.to_ascii_lowercase().as_str() {
"fact" => Self::Fact,
"observation" => Self::Observation,
"inference" => Self::Inference,
_ => Self::Other(t.to_string()),
}
}
}
}
fn is_authoritative(&self) -> bool {
matches!(self, Self::Fact | Self::Observation)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GateMode {
Off,
Warn,
Enforce,
}
#[must_use = "a Flagged verdict must be ticked post-commit via note_flagged_write_committed"]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GateVerdict {
Clean,
Flagged,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WriteAdmission {
Origin,
Admitted,
}
impl GateMode {
pub fn parse(s: &str) -> Result<Self> {
match s.trim().to_ascii_lowercase().as_str() {
"enforce" => Ok(Self::Enforce),
"warn" => Ok(Self::Warn),
"off" => Ok(Self::Off),
other => Err(YantrikDbError::ProvenanceInconsistent {
path: "provenance_gate_mode",
reason: format!(
"malformed gate mode '{other}' (expected off|warn|enforce); refusing to \
silently disable the provenance gate"
),
}),
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::Enforce => "enforce",
Self::Warn => "warn",
Self::Off => "off",
}
}
pub fn as_u8(self) -> u8 {
match self {
Self::Off => 0,
Self::Warn => 1,
Self::Enforce => 2,
}
}
pub fn from_u8(v: u8) -> Self {
match v {
2 => Self::Enforce,
1 => Self::Warn,
_ => Self::Off,
}
}
}
pub fn check_provenance_consistency_opt(
source: Source,
basis: Option<&ConfidenceBasis>,
kind: &ClaimKind,
override_kind: bool,
) -> Result<()> {
if source == Source::Inference {
if basis == Some(&ConfidenceBasis::Observation) {
return Err(YantrikDbError::ProvenanceInconsistent {
path: "source/confidence_basis",
reason: "source=inference cannot have confidence_basis=observation \
(you did not observe an inference)"
.into(),
});
}
if kind.is_authoritative()
&& !override_kind
&& !matches!(
basis,
Some(ConfidenceBasis::Confirmation | ConfidenceBasis::Verification)
)
{
let kind_str = match kind {
ClaimKind::Fact => "fact",
_ => "observation",
};
return Err(YantrikDbError::ProvenanceInconsistent {
path: "source/kind",
reason: format!(
"source=inference cannot claim kind={kind_str} without confidence_basis in \
{{confirmation, verification}} or an explicit override_kind"
),
});
}
}
Ok(())
}
pub fn check_provenance_consistency(
source: Source,
basis: &ConfidenceBasis,
kind: &ClaimKind,
override_kind: bool,
) -> Result<()> {
check_provenance_consistency_opt(source, Some(basis), kind, override_kind)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn source_parse_closed_vocab() {
assert_eq!(Source::parse("user").unwrap(), Source::User);
assert_eq!(Source::parse(" Inference ").unwrap(), Source::Inference);
assert_eq!(Source::parse("DOCUMENT").unwrap(), Source::Document);
assert!(Source::parse("inferece").is_err());
assert!(Source::parse("").is_err());
}
#[test]
fn basis_parse_incl_asserted_and_learned() {
assert_eq!(
ConfidenceBasis::parse("asserted").unwrap(),
ConfidenceBasis::Asserted
);
assert_eq!(
ConfidenceBasis::parse(" Learned(bge-base-en-v1.5) ").unwrap(),
ConfidenceBasis::Learned {
model: "bge-base-en-v1.5".into()
}
);
assert!(ConfidenceBasis::parse("learned()").is_err()); assert!(ConfidenceBasis::parse("learned(a b)").is_err()); assert!(ConfidenceBasis::parse("nonsense").is_err());
assert_eq!(
ConfidenceBasis::parse("learned(m1)")
.unwrap()
.to_canonical(),
"learned(m1)"
);
}
#[test]
fn kind_open_vocab_and_unspecified() {
assert_eq!(ClaimKind::parse(Some("fact")), ClaimKind::Fact);
assert_eq!(
ClaimKind::parse(Some(" Observation ")),
ClaimKind::Observation
);
assert_eq!(
ClaimKind::parse(Some("procedure")),
ClaimKind::Other("procedure".into())
);
assert_eq!(
ClaimKind::parse(Some("fact.")),
ClaimKind::Other("fact.".into())
);
assert_eq!(ClaimKind::parse(None), ClaimKind::Unspecified);
assert_eq!(ClaimKind::parse(Some("")), ClaimKind::Unspecified);
assert_eq!(ClaimKind::parse(Some(" ")), ClaimKind::Unspecified);
}
#[test]
fn matrix_refuses_inference_observation_basis() {
let e = check_provenance_consistency(
Source::Inference,
&ConfidenceBasis::Observation,
&ClaimKind::Inference,
false,
);
assert!(e.is_err(), "inference + observation basis must refuse");
}
#[test]
fn matrix_refuses_inference_claiming_fact() {
let e = check_provenance_consistency(
Source::Inference,
&ConfidenceBasis::Asserted,
&ClaimKind::Fact,
false,
);
assert!(
e.is_err(),
"inference claiming fact (no confirm/override) refused"
);
}
#[test]
fn matrix_confirmation_allowance() {
assert!(check_provenance_consistency(
Source::Inference,
&ConfidenceBasis::Confirmation,
&ClaimKind::Fact,
false,
)
.is_ok());
assert!(check_provenance_consistency(
Source::Inference,
&ConfidenceBasis::Verification,
&ClaimKind::Observation,
false,
)
.is_ok());
}
#[test]
fn matrix_override_allowance() {
assert!(check_provenance_consistency(
Source::Inference,
&ConfidenceBasis::Asserted,
&ClaimKind::Fact,
true, )
.is_ok());
}
#[test]
fn matrix_allows_consistent_records() {
assert!(check_provenance_consistency(
Source::User,
&ConfidenceBasis::Asserted,
&ClaimKind::Fact,
false,
)
.is_ok());
assert!(check_provenance_consistency(
Source::Inference,
&ConfidenceBasis::Inference,
&ClaimKind::Inference,
false,
)
.is_ok());
assert!(check_provenance_consistency(
Source::Inference,
&ConfidenceBasis::Asserted,
&ClaimKind::Other("hypothesis".into()),
false,
)
.is_ok());
assert!(check_provenance_consistency(
Source::Inference,
&ConfidenceBasis::Asserted,
&ClaimKind::Unspecified,
false,
)
.is_ok());
}
}