use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Write as _;
use crate::engine::stable_id;
use crate::relative_meta_logic::{
RelativeEvidence, SourceTier, Stance, StatementAssessment, TruthValue, ASSUMED_TRUE_PRIOR,
};
use crate::substitution::{SubstitutionGraph, SubstitutionLink};
const MAX_RECALCULATION_PASSES_PER_STATEMENT: usize = 4;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Dependency {
pub on: String,
pub stance: Stance,
}
impl Dependency {
#[must_use]
pub fn supports(on: impl Into<String>) -> Self {
Self {
on: on.into(),
stance: Stance::Supports,
}
}
#[must_use]
pub fn contradicts(on: impl Into<String>) -> Self {
Self {
on: on.into(),
stance: Stance::Contradicts,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Statement {
pub id: String,
pub text: String,
pub prior: TruthValue,
pub evidence: Vec<RelativeEvidence>,
pub dependencies: Vec<Dependency>,
pub truth: TruthValue,
}
impl Statement {
#[must_use]
pub fn new(text: impl Into<String>) -> Self {
let text = text.into();
let id = stable_id("world_statement", &text);
let prior = TruthValue::new(ASSUMED_TRUE_PRIOR);
let truth = StatementAssessment::assess(&text, prior, &[]).posterior;
Self {
id,
text,
prior,
evidence: Vec::new(),
dependencies: Vec::new(),
truth,
}
}
#[must_use]
pub fn with_evidence(mut self, evidence: RelativeEvidence) -> Self {
self.evidence.push(evidence);
self
}
#[must_use]
pub fn with_dependency(mut self, dependency: Dependency) -> Self {
self.dependencies.push(dependency);
self
}
#[must_use]
pub fn with_prior(mut self, prior: impl Into<TruthValue>) -> Self {
self.prior = prior.into();
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Action {
pub name: String,
pub remove: Vec<SubstitutionLink>,
pub add: Vec<SubstitutionLink>,
}
impl Action {
#[must_use]
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
remove: Vec::new(),
add: Vec::new(),
}
}
#[must_use]
pub fn adding(mut self, from: impl Into<String>, to: impl Into<String>) -> Self {
self.add.push(SubstitutionLink::new(from, to));
self
}
#[must_use]
pub fn removing(mut self, from: impl Into<String>, to: impl Into<String>) -> Self {
self.remove.push(SubstitutionLink::new(from, to));
self
}
#[must_use]
pub fn id(&self) -> String {
let mut canonical = format!("name:{};", self.name);
for link in &self.remove {
let _ = write!(canonical, "remove:{};", link.pattern_text());
}
for link in &self.add {
let _ = write!(canonical, "add:{};", link.pattern_text());
}
stable_id("world_action", &canonical)
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Context {
pub id: String,
links: SubstitutionGraph,
statements: BTreeMap<String, Statement>,
}
impl Context {
#[must_use]
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
links: SubstitutionGraph::new(),
statements: BTreeMap::new(),
}
}
pub fn assert_link(&mut self, from: &str, to: &str) -> bool {
self.links.insert_link(from, to)
}
pub fn retract_link(&mut self, from: &str, to: &str) -> bool {
self.links.remove_link(from, to)
}
#[must_use]
pub fn holds(&self, from: &str, to: &str) -> bool {
self.links.contains_link(from, to)
}
#[must_use]
pub fn links(&self) -> Vec<SubstitutionLink> {
self.links.links()
}
#[must_use]
pub const fn statements(&self) -> &BTreeMap<String, Statement> {
&self.statements
}
#[must_use]
pub fn statement(&self, id: &str) -> Option<&Statement> {
self.statements.get(id)
}
pub fn add_statement(&mut self, statement: Statement) -> String {
let id = statement.id.clone();
self.statements.insert(id.clone(), statement);
let _ = self.recalculate();
id
}
pub fn recalculate(&mut self) -> RecalculationReport {
let ids: Vec<String> = self.statements.keys().cloned().collect();
let before: BTreeMap<String, TruthValue> = ids
.iter()
.map(|id| (id.clone(), self.statements[id].truth))
.collect();
let limit = ids
.len()
.saturating_mul(MAX_RECALCULATION_PASSES_PER_STATEMENT)
.max(MAX_RECALCULATION_PASSES_PER_STATEMENT);
let mut iterations = 0;
let mut converged = false;
while iterations < limit {
iterations += 1;
let snapshot: BTreeMap<String, TruthValue> = ids
.iter()
.map(|id| (id.clone(), self.statements[id].truth))
.collect();
let mut changed = false;
for id in &ids {
let recomputed = self.assess_with_dependencies(id, &snapshot);
if recomputed != self.statements[id].truth {
if let Some(statement) = self.statements.get_mut(id) {
statement.truth = recomputed;
}
changed = true;
}
}
if !changed {
converged = true;
break;
}
}
self.sync_statement_links();
let updated = ids
.iter()
.filter_map(|id| {
let after = self.statements[id].truth;
let previous = before.get(id).copied().unwrap_or(after);
(after != previous).then(|| StatementChange {
statement_id: id.clone(),
text: self.statements[id].text.clone(),
before: previous,
after,
})
})
.collect();
RecalculationReport {
iterations,
converged,
updated,
}
}
fn assess_with_dependencies(
&self,
id: &str,
snapshot: &BTreeMap<String, TruthValue>,
) -> TruthValue {
let statement = &self.statements[id];
let mut evidence = statement.evidence.clone();
for dependency in &statement.dependencies {
if let Some(value) = snapshot.get(&dependency.on) {
evidence.push(RelativeEvidence::new(
format!("statement:{}", dependency.on),
SourceTier::OriginalFirstParty,
dependency.stance,
*value,
));
}
}
StatementAssessment::assess(&statement.text, statement.prior, &evidence).posterior
}
fn sync_statement_links(&mut self) {
for link in self.links.links() {
if is_statement_layer_link(&link) {
self.links.remove_link(&link.from, &link.to);
}
}
for statement in self.statements.values() {
self.links.insert_link(&statement.id, "world:statement");
self.links
.insert_link(&statement.id, &format!("truth:{}", statement.truth));
for dependency in &statement.dependencies {
self.links.insert_link(
&statement.id,
&format!("{}:{}", dependency.stance.slug(), dependency.on),
);
}
}
}
pub fn apply_action(&mut self, action: &Action) -> RecalculationReport {
for link in &action.remove {
self.links.remove_link(&link.from, &link.to);
}
for link in &action.add {
self.links.insert_link(&link.from, &link.to);
}
self.recalculate()
}
#[must_use]
pub fn predict(&self, action: &Action) -> Prediction {
let mut probe = self.clone();
probe.apply_action(action);
let difference = self.difference(&probe);
let statement_changes = probe.statement_changes_against(self);
Prediction {
action_id: action.id(),
action_name: action.name.clone(),
added: difference.to_add,
removed: difference.to_remove,
statement_changes,
result: probe,
}
}
#[must_use]
pub fn difference(&self, target: &Self) -> ContextDiff {
let current: BTreeSet<SubstitutionLink> = self.state_links();
let goal: BTreeSet<SubstitutionLink> = target.state_links();
let to_add: Vec<SubstitutionLink> = goal.difference(¤t).cloned().collect();
let to_remove: Vec<SubstitutionLink> = current.difference(&goal).cloned().collect();
let mut conflicting = Vec::new();
for removed in &to_remove {
for added in &to_add {
if removed.from == added.from && removed.to != added.to {
conflicting.push(LinkConflict {
current: removed.clone(),
target: added.clone(),
});
}
}
}
ContextDiff {
to_add,
to_remove,
conflicting,
}
}
pub fn merge_from(&mut self, other: &Self) -> RecalculationReport {
for link in other.state_links() {
self.links.insert_link(&link.from, &link.to);
}
for (id, statement) in &other.statements {
self.statements.insert(id.clone(), statement.clone());
}
self.recalculate()
}
#[must_use]
pub fn split_off(&self, child_id: impl Into<String>, statement_ids: &[String]) -> Self {
let mut child = Self::new(child_id);
let selected: BTreeSet<&String> = statement_ids
.iter()
.filter(|id| self.statements.contains_key(*id))
.collect();
for id in &selected {
if let Some(statement) = self.statements.get(*id) {
child.statements.insert((*id).clone(), statement.clone());
}
}
for link in self.state_links() {
if selected.contains(&link.from) || selected.contains(&link.to) {
child.links.insert_link(&link.from, &link.to);
}
}
let _ = child.recalculate();
child
}
#[must_use]
pub fn links_notation(&self) -> String {
self.links.links_notation()
}
fn state_links(&self) -> BTreeSet<SubstitutionLink> {
self.links
.links()
.into_iter()
.filter(|link| !is_statement_layer_link(link))
.collect()
}
fn statement_changes_against(&self, baseline: &Self) -> Vec<StatementChange> {
self.statements
.values()
.filter_map(|statement| {
let before = baseline
.statements
.get(&statement.id)
.map_or(statement.truth, |previous| previous.truth);
(before != statement.truth).then(|| StatementChange {
statement_id: statement.id.clone(),
text: statement.text.clone(),
before,
after: statement.truth,
})
})
.collect()
}
}
fn is_statement_layer_link(link: &SubstitutionLink) -> bool {
link.to == "world:statement"
|| link.to.starts_with("truth:")
|| link.to.starts_with("supports:")
|| link.to.starts_with("contradicts:")
|| link.to.starts_with("neutral:")
}
#[derive(Debug, Clone, PartialEq)]
pub struct RecalculationReport {
pub iterations: usize,
pub converged: bool,
pub updated: Vec<StatementChange>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct StatementChange {
pub statement_id: String,
pub text: String,
pub before: TruthValue,
pub after: TruthValue,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LinkConflict {
pub current: SubstitutionLink,
pub target: SubstitutionLink,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ContextDiff {
pub to_add: Vec<SubstitutionLink>,
pub to_remove: Vec<SubstitutionLink>,
pub conflicting: Vec<LinkConflict>,
}
impl ContextDiff {
#[must_use]
pub const fn is_empty(&self) -> bool {
self.to_add.is_empty() && self.to_remove.is_empty() && self.conflicting.is_empty()
}
#[must_use]
pub fn links_notation(&self) -> String {
let mut out = String::from("context_diff\n");
for link in &self.to_add {
let _ = writeln!(out, " to_add \"{}\"", link.pattern_text());
}
for link in &self.to_remove {
let _ = writeln!(out, " to_remove \"{}\"", link.pattern_text());
}
for conflict in &self.conflicting {
let _ = writeln!(
out,
" conflict \"{} vs {}\"",
conflict.current.pattern_text(),
conflict.target.pattern_text()
);
}
out.trim_end().to_owned()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Prediction {
pub action_id: String,
pub action_name: String,
pub added: Vec<SubstitutionLink>,
pub removed: Vec<SubstitutionLink>,
pub statement_changes: Vec<StatementChange>,
pub result: Context,
}
impl Prediction {
#[must_use]
pub const fn is_noop(&self) -> bool {
self.added.is_empty() && self.removed.is_empty() && self.statement_changes.is_empty()
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct WorldModel {
pub current: Context,
pub target: Context,
pub general: Context,
}
impl WorldModel {
#[must_use]
pub fn new() -> Self {
Self {
current: Context::new("current"),
target: Context::new("target"),
general: Context::new("general"),
}
}
#[must_use]
pub fn difference(&self) -> ContextDiff {
self.current.difference(&self.target)
}
#[must_use]
pub fn predict(&self, action: &Action) -> Prediction {
self.current.predict(action)
}
#[must_use]
pub fn target_reached(&self) -> bool {
self.difference().is_empty()
}
pub fn commit_current_to_general(&mut self) -> RecalculationReport {
self.general.merge_from(&self.current)
}
}