use std::fmt;
use crate::ast::{
BoundValue, Command, KipValue, KmlStatement, KqlQuery, MetaCommand, MutationClause,
MutationValue, Scalar, StructuralEdge, UpdateAction,
};
use crate::error::{KipError, KipErrorCode};
pub const STANCES: &[&str] = crate::types::Stance::NAMES;
pub const ASSERTION_MODES: &[&str] = crate::types::AssertionMode::NAMES;
pub const ASSERTION_LIFECYCLE: &[&str] = crate::types::AssertionStatus::NAMES;
pub const EVIDENCE_LIFECYCLE: &[&str] = &["active", "corrected"];
pub const EVIDENCE_ROLES: &[&str] = &["support", "challenge", "context"];
pub const ACTIVITY_STATUS: &[&str] = &["pending", "running", "completed", "failed", "cancelled"];
pub const ACTIVITY_TERMINAL: &[&str] = &["completed", "failed", "cancelled"];
pub const TRANSITION_STATES: &[&str] = crate::ast::transition_state::ALL;
pub const BELIEF_STATUSES: &[&str] = crate::types::BeliefStatus::NAMES;
pub const SEARCH_MODES: &[&str] = crate::request::SearchMode::NAMES;
pub const CORE_ELEMENT_KINDS: &[&str] = &[
"Concept",
"Proposition",
"Assertion",
"Evidence",
"Activity",
];
pub const CORE_STRUCTURAL_FIELDS: &[(&str, &str)] = &[
("evidence", "Assertion"),
("context", "Assertion"),
("source", "Evidence"),
("generated_by", "Evidence"),
("inputs", "Activity"),
("outputs", "Activity"),
("associated_actors", "Activity"),
];
pub fn core_structural_owner(name: &str) -> Option<&'static str> {
CORE_STRUCTURAL_FIELDS
.iter()
.find(|(field, _)| *field == name)
.map(|(_, owner)| *owner)
}
pub const PRIMER_MODES: &[&str] = &["compact", "full"];
pub const EXPLANATION_LEVELS: &[&str] = &["none", "summary", "ledger"];
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Severity {
Error,
Warning,
}
impl fmt::Display for Severity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Severity::Error => f.write_str("error"),
Severity::Warning => f.write_str("warning"),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Diagnostic {
pub severity: Severity,
pub code: KipErrorCode,
pub message: String,
}
impl Diagnostic {
fn error(code: KipErrorCode, message: impl Into<String>) -> Self {
Self {
severity: Severity::Error,
code,
message: message.into(),
}
}
fn warning(code: KipErrorCode, message: impl Into<String>) -> Self {
Self {
severity: Severity::Warning,
code,
message: message.into(),
}
}
}
impl fmt::Display for Diagnostic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.severity, self.message)
}
}
impl From<Diagnostic> for KipError {
fn from(diagnostic: Diagnostic) -> Self {
KipError::new(diagnostic.code, diagnostic.message)
}
}
pub fn analyze(command: &Command) -> Vec<Diagnostic> {
let mut out = Vec::new();
match command {
Command::Kql(query) => analyze_kql(query, &mut out),
Command::Kml(statement) => analyze_kml(statement, &mut out),
Command::Meta(meta) => analyze_meta(meta, &mut out),
}
out
}
pub(crate) fn check_kql(query: &KqlQuery) -> Result<(), KipError> {
let mut out = Vec::new();
analyze_kql(query, &mut out);
first_error(out)
}
pub(crate) fn check_kml(statement: &KmlStatement) -> Result<(), KipError> {
let mut out = Vec::new();
analyze_kml(statement, &mut out);
first_error(out)
}
pub(crate) fn check_meta(meta: &MetaCommand) -> Result<(), KipError> {
let mut out = Vec::new();
analyze_meta(meta, &mut out);
first_error(out)
}
fn first_error(diagnostics: Vec<Diagnostic>) -> Result<(), KipError> {
match diagnostics
.into_iter()
.find(|d| d.severity == Severity::Error)
{
Some(diagnostic) => Err(diagnostic.into()),
None => Ok(()),
}
}
fn literal_str(value: &MutationValue) -> Option<&str> {
match value {
MutationValue::Value(KipValue::String(text)) => Some(text),
_ => None,
}
}
fn literal_f64(value: &MutationValue) -> Option<f64> {
match value {
MutationValue::Value(KipValue::Number(number)) => number.as_f64(),
_ => None,
}
}
fn scalar_str(scalar: &Scalar) -> Option<&str> {
match scalar {
Scalar::Literal(KipValue::String(text)) => Some(text),
_ => None,
}
}
fn bound_str(value: &BoundValue) -> Option<&str> {
match value {
BoundValue::Value(KipValue::String(text)) => Some(text),
_ => None,
}
}
fn check_enum(written: Option<&str>, allowed: &[&str], label: &str, out: &mut Vec<Diagnostic>) {
let Some(written) = written else { return };
if allowed.contains(&written) {
return;
}
out.push(Diagnostic::error(
KipErrorCode::ConstraintViolation,
format!(
"{label} must be one of {}, found {written:?}",
allowed.join(" | ")
),
));
}
fn check_unit_interval(value: Option<f64>, label: &str, out: &mut Vec<Diagnostic>) {
let Some(value) = value else { return };
if (0.0..=1.0).contains(&value) {
return;
}
out.push(Diagnostic::error(
KipErrorCode::ConstraintViolation,
format!("{label} must be within [0, 1], found {value}"),
));
}
fn analyze_kml(statement: &KmlStatement, out: &mut Vec<Diagnostic>) {
for clause in &statement.clauses {
analyze_clause(clause, out);
}
}
fn analyze_clause(clause: &MutationClause, out: &mut Vec<Diagnostic>) {
match clause {
MutationClause::CreateAssertion(record) => {
if let Some(fields) = &record.set_fields {
analyze_assignments(fields, out);
analyze_assertion_shape(fields, record.set_structural.as_deref(), out);
}
analyze_structural(record.set_structural.as_deref(), out);
for facet in &record.set_facets {
analyze_assignments(&facet.values, out);
}
}
MutationClause::CreateEvidence(record) | MutationClause::CreateActivity(record) => {
if let Some(fields) = &record.set_fields {
analyze_assignments(fields, out);
}
analyze_structural(record.set_structural.as_deref(), out);
for facet in &record.set_facets {
analyze_assignments(&facet.values, out);
}
}
MutationClause::CreateConcept(concept) => {
for fields in [&concept.set_fields, &concept.set_attributes]
.into_iter()
.flatten()
{
analyze_assignments(fields, out);
}
analyze_structural(concept.set_structural.as_deref(), out);
for facet in &concept.set_facets {
analyze_assignments(&facet.values, out);
}
}
MutationClause::UpsertConcept(concept) => {
for fields in [&concept.set_fields, &concept.set_attributes]
.into_iter()
.flatten()
{
analyze_assignments(fields, out);
}
analyze_structural(concept.set_structural.as_deref(), out);
for facet in &concept.set_facets {
analyze_assignments(&facet.values, out);
}
}
MutationClause::Update(update) => {
for action in &update.actions {
match action {
UpdateAction::SetFields(a) | UpdateAction::SetAttributes(a) => {
analyze_assignments(a, out)
}
UpdateAction::SetFacet(facet) => analyze_assignments(&facet.values, out),
UpdateAction::SetStructural(edges) => analyze_structural(Some(edges), out),
_ => {}
}
}
warn_unbounded(
"UPDATE",
update.where_clauses.is_some(),
update.limit.is_some(),
out,
);
}
MutationClause::Transition(transition) => {
check_enum(
transition.state(),
TRANSITION_STATES,
"TRANSITION ... TO",
out,
);
if let Some(fields) = &transition.set_fields {
analyze_assignments(fields, out);
}
analyze_structural(transition.set_structural.as_deref(), out);
warn_unbounded(
"TRANSITION",
transition.where_clauses.is_some(),
transition.limit.is_some(),
out,
);
}
MutationClause::SetRetention(retention) => {
analyze_assignments(&retention.values, out);
warn_unbounded(
"SET RETENTION",
retention.where_clauses.is_some(),
retention.limit.is_some(),
out,
);
}
MutationClause::Purge(purge) => warn_unbounded(
"PURGE",
purge.where_clauses.is_some(),
purge.limit.is_some(),
out,
),
MutationClause::PurgePayload(purge) => warn_unbounded(
"PURGE PAYLOAD",
purge.where_clauses.is_some(),
purge.limit.is_some(),
out,
),
MutationClause::EnsureProposition(_) | MutationClause::MergeConcept(_) => {}
}
}
fn analyze_assignments(assignments: &crate::ast::Assignments, out: &mut Vec<Diagnostic>) {
for (field, value) in assignments {
match field.as_str() {
"stance" => check_enum(literal_str(value), STANCES, "stance", out),
"mode" => check_enum(literal_str(value), ASSERTION_MODES, "mode", out),
"confidence" => check_unit_interval(literal_f64(value), "confidence", out),
_ => {}
}
}
}
fn analyze_structural(edges: Option<&[StructuralEdge]>, out: &mut Vec<Diagnostic>) {
for edge in edges.into_iter().flatten() {
let crate::ast::SymbolRef::Name(field) = &edge.field else {
continue;
};
if field != "evidence" {
continue;
}
let Some(options) = &edge.options else {
continue;
};
if let Some(role) = options.get("role") {
check_enum(
bound_str(role),
EVIDENCE_ROLES,
"an Evidence citation role",
out,
);
}
}
}
fn analyze_assertion_shape(
fields: &crate::ast::Assignments,
structural: Option<&[StructuralEdge]>,
out: &mut Vec<Diagnostic>,
) {
let observed = fields
.iter()
.any(|(name, value)| name == "mode" && literal_str(value) == Some("observed"));
if !observed {
return;
}
let cites_evidence = structural.into_iter().flatten().any(
|edge| matches!(&edge.field, crate::ast::SymbolRef::Name(field) if field == "evidence"),
);
if !cites_evidence {
out.push(Diagnostic::warning(
KipErrorCode::ConstraintViolation,
"mode: \"observed\" without evidence: an observation normally cites the artifact it \
was observed from",
));
}
}
fn warn_unbounded(statement: &str, has_where: bool, has_limit: bool, out: &mut Vec<Diagnostic>) {
if has_where && !has_limit {
out.push(Diagnostic::warning(
KipErrorCode::ResultLimitExceeded,
format!(
"{statement} selects by pattern without a LIMIT: the match set is unbounded, and \
an over-broad one cannot be undone"
),
));
}
}
fn analyze_kql(query: &KqlQuery, out: &mut Vec<Diagnostic>) {
if let Some(epistemic) = &query.epistemic
&& let Some(explanation) = epistemic.get("explanation")
{
check_enum(
bound_str(explanation),
EXPLANATION_LEVELS,
"WITH EPISTEMIC explanation",
out,
);
}
if query.limit.is_none() {
out.push(Diagnostic::warning(
KipErrorCode::ResultLimitExceeded,
"FIND without a LIMIT: an unbounded recall returns whatever the Space happens to hold",
));
}
}
fn analyze_meta(meta: &MetaCommand, out: &mut Vec<Diagnostic>) {
match meta {
MetaCommand::Search(search) => check_enum(
search.mode.as_ref().and_then(scalar_str),
SEARCH_MODES,
"SEARCH MODE",
out,
),
MetaCommand::Describe(crate::ast::DescribeTarget::Primer { mode }) => check_enum(
mode.as_ref().and_then(scalar_str),
PRIMER_MODES,
"DESCRIBE PRIMER MODE",
out,
),
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::{parse_kip, parse_kml};
#[test]
fn core_registry_values_are_checked_wherever_they_are_written() {
for input in [
r#"ASSERT (:a, "p", :b) { by: :me, mode: "guessed" }"#,
r#"ASSERT (:a, "p", :b) { by: :me, mode: "stated", stance: "maybe" }"#,
r#"CREATE ASSERTION ?a { SET FIELDS { stance: "nope" } }"#,
r#"UPDATE :c SET FIELDS { mode: "wat" }"#,
r#"CREATE ASSERTION ?a { SET STRUCTURAL { ("evidence", :e) { role: "bogus" } } }"#,
r#"RETRACT ASSERTION :a EXPECT STATE "banana""#,
r#"SUPERSEDE ASSERTION :a BY :b EXPECT STATE "banana""#,
r#"SEARCH CONCEPT "x" MODE "fuzzy""#,
r#"DESCRIBE PRIMER MODE "verbose""#,
] {
assert!(
parse_kip(input).is_err(),
"a Core registry violation must not parse: {input}"
);
}
}
#[test]
fn the_unit_interval_is_enforced_where_the_protocol_fixes_it() {
for input in [
r#"ASSERT (:a, "p", :b) { by: :me, mode: "stated", confidence: 5 }"#,
r#"ASSERT (:a, "p", :b) { by: :me, mode: "stated", confidence: -0.5 }"#,
r#"CREATE ASSERTION ?a { SET FIELDS { confidence: 1.5 } }"#,
] {
assert!(parse_kip(input).is_err(), "out of [0,1]: {input}");
}
for input in [
r#"ASSERT (:a, "p", :b) { by: :me, mode: "stated", confidence: 0 }"#,
r#"ASSERT (:a, "p", :b) { by: :me, mode: "stated", confidence: 1 }"#,
r#"ASSERT (:a, "p", :b) { by: :me, mode: "stated", confidence: 0.5 }"#,
] {
assert!(parse_kip(input).is_ok(), "inside [0,1]: {input}");
}
}
#[test]
fn search_threshold_execution_validation_belongs_to_the_engine() {
for input in [
r#"SEARCH CONCEPT "x" THRESHOLD 0.5"#,
r#"SEARCH CONCEPT "x" THRESHOLD 1000"#,
r#"SEARCH CONCEPT "x" THRESHOLD -3"#,
] {
assert!(parse_kip(input).is_ok(), "{input}");
}
}
#[test]
fn a_parameter_is_never_second_guessed() {
for input in [
r#"ASSERT (:a, "p", :b) { by: :me, mode: :mode, stance: :stance, confidence: :c }"#,
r#"SEARCH CONCEPT "x" MODE :mode THRESHOLD :threshold"#,
r#"DESCRIBE PRIMER MODE :mode"#,
r#"TRANSITION :a TO :state"#,
] {
assert!(parse_kip(input).is_ok(), "a parameter must pass: {input}");
}
}
#[test]
fn package_defined_signals_are_left_to_the_engine() {
assert!(parse_kip(r#"UPDATE :c SET FACET "MnemonicState" { salience: 42 }"#).is_ok());
assert!(parse_kip(r#"UPDATE :c SET FACET "Skill" { utility: 42 }"#).is_ok());
}
#[test]
fn a_transition_names_a_state_from_the_registry() {
for state in TRANSITION_STATES {
let by = if crate::ast::transition_state::WITH_BY.contains(state) {
" BY :b"
} else {
""
};
assert!(
parse_kip(&format!(r#"TRANSITION :a TO "{state}"{by}"#)).is_ok(),
"{state}"
);
}
let err = parse_kip(r#"TRANSITION :a TO "succeeded""#).expect_err("not a state");
assert_eq!(err.code, KipErrorCode::ConstraintViolation);
assert!(parse_kip(r#"TRANSITION :a TO "active""#).is_err());
assert!(parse_kip(r#"TRANSITION :a TO "quarantined""#).is_err());
}
#[test]
fn unbounded_pattern_mutations_warn_but_still_parse() {
let statement = parse_kml(r#"PURGE ?x WHERE { ?x {type: "T"} } CONFIRM "PURGE""#)
.expect("a legal command");
let mut diagnostics = Vec::new();
analyze_kml(&statement, &mut diagnostics);
assert!(
diagnostics
.iter()
.any(|d| d.severity == Severity::Warning && d.message.contains("LIMIT")),
"an unbounded PURGE must warn: {diagnostics:?}"
);
let bounded = parse_kml(r#"PURGE :x CONFIRM "PURGE""#).expect("a legal command");
let mut none = Vec::new();
analyze_kml(&bounded, &mut none);
assert!(none.is_empty(), "a targeted PURGE must not warn: {none:?}");
let payload =
parse_kml(r#"PURGE PAYLOAD ?x WHERE { ?x EVIDENCE {evidence_class: "document"} } CONFIRM "PURGE""#)
.expect("a legal command");
let mut diagnostics = Vec::new();
analyze_kml(&payload, &mut diagnostics);
assert!(
diagnostics
.iter()
.any(|d| d.severity == Severity::Warning && d.message.contains("LIMIT")),
"an unbounded PURGE PAYLOAD must warn: {diagnostics:?}"
);
}
#[test]
fn an_observation_without_evidence_is_a_warning_not_a_rejection() {
let command =
parse_kip(r#"ASSERT (:a, "p", :b) { by: :me, mode: "observed" }"#).expect("legal");
let diagnostics = analyze(&command);
assert!(diagnostics.iter().all(|d| d.severity == Severity::Warning));
assert!(diagnostics.iter().any(|d| d.message.contains("observed")));
let cited =
parse_kip(r#"ASSERT (:a, "p", :b) { by: :me, mode: "observed", evidence: :e }"#)
.expect("legal");
assert!(analyze(&cited).is_empty());
}
#[test]
fn the_explanation_level_comes_from_the_registry() {
assert!(
parse_kip(r#"FIND(?x) WHERE { ?x {a: 1} } WITH EPISTEMIC { explanation: "verbose" }"#)
.is_err()
);
assert!(
parse_kip(
r#"FIND(?x) WHERE { ?x {a: 1} } WITH EPISTEMIC { explanation: "ledger" } LIMIT 5"#
)
.is_ok()
);
}
#[test]
fn diagnostics_carry_the_registry_code_they_would_be_reported_under() {
let err = parse_kip(r#"ASSERT (:a, "p", :b) { by: :me, mode: "guessed" }"#)
.expect_err("rejected");
assert_eq!(err.code, KipErrorCode::ConstraintViolation);
assert!(err.message.contains("observed | stated"), "{}", err.message);
}
}