mirror of
https://gitea.ingwaz.work/Ingwaz/openbrain-mcp.git
synced 2026-06-15 22:07:08 +00:00
merge: resolve conflict with main — combine PLN and ECAN in truth/mod.rs
This commit is contained in:
24
.env.example
24
.env.example
@@ -42,3 +42,27 @@ OPENBRAIN__AUTH__ENABLED=false
|
|||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
RUST_LOG=info,openbrain_mcp=debug
|
RUST_LOG=info,openbrain_mcp=debug
|
||||||
|
|
||||||
|
# Truth Engine (optional)
|
||||||
|
# Enable the background truth scoring worker. When enabled, stored memories
|
||||||
|
# are continuously evaluated for truthfulness using PLN deduction, ECAN
|
||||||
|
# attention economy, and cross-referencing against related memories.
|
||||||
|
OPENBRAIN__TRUTH__ENABLED=false
|
||||||
|
# Seconds between scoring cycles (default: 300 = 5 minutes)
|
||||||
|
OPENBRAIN__TRUTH__SCORING_INTERVAL_SECONDS=300
|
||||||
|
# Number of memories to score per cycle (default: 50)
|
||||||
|
OPENBRAIN__TRUTH__BATCH_SIZE=50
|
||||||
|
# Seconds before a scored memory is re-evaluated (default: 86400 = 24 hours)
|
||||||
|
OPENBRAIN__TRUTH__RESCORE_AFTER_SECONDS=86400
|
||||||
|
# PLN base confidence for deduction chains (0.0–1.0, default: 0.85)
|
||||||
|
OPENBRAIN__TRUTH__PLN_BASE_CONFIDENCE=0.85
|
||||||
|
# ECAN STI decay rate per cycle (0.0–1.0, default: 0.95)
|
||||||
|
OPENBRAIN__TRUTH__ECAN_DECAY_RATE=0.95
|
||||||
|
# ECAN attention spread factor (default: 0.05)
|
||||||
|
OPENBRAIN__TRUTH__ECAN_SPREAD_FACTOR=0.05
|
||||||
|
# Similarity threshold for contradiction detection (0.0–1.0, default: 0.85)
|
||||||
|
OPENBRAIN__TRUTH__CONTRADICTION_THRESHOLD=0.85
|
||||||
|
# Truth value threshold for "verified" categorization (0.0–1.0, default: 0.8)
|
||||||
|
OPENBRAIN__TRUTH__VERIFICATION_THRESHOLD=0.8
|
||||||
|
# Max related memories to cross-reference per scoring (default: 10)
|
||||||
|
OPENBRAIN__TRUTH__CROSS_REF_LIMIT=10
|
||||||
|
|||||||
34
migrations/V5__truth_scoring.sql
Normal file
34
migrations/V5__truth_scoring.sql
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
-- V5: Add truth scoring columns for the Truth Engine integration
|
||||||
|
-- Part of: https://gitea.ingwaz.work/Ingwaz/openbrain-mcp/issues/31
|
||||||
|
--
|
||||||
|
-- All columns are nullable — existing memories start unscored and
|
||||||
|
-- get populated by the background truth scoring worker.
|
||||||
|
--
|
||||||
|
-- Rollback (manual):
|
||||||
|
-- ALTER TABLE memories
|
||||||
|
-- DROP COLUMN IF EXISTS truth_value,
|
||||||
|
-- DROP COLUMN IF EXISTS truth_confidence,
|
||||||
|
-- DROP COLUMN IF EXISTS truth_category,
|
||||||
|
-- DROP COLUMN IF EXISTS truth_evaluated_at,
|
||||||
|
-- DROP COLUMN IF EXISTS ecan_sti,
|
||||||
|
-- DROP COLUMN IF EXISTS ecan_lti;
|
||||||
|
-- DROP INDEX IF EXISTS idx_memories_truth_category;
|
||||||
|
-- DROP INDEX IF EXISTS idx_memories_truth_unevaluated;
|
||||||
|
|
||||||
|
ALTER TABLE memories
|
||||||
|
ADD COLUMN truth_value REAL,
|
||||||
|
ADD COLUMN truth_confidence REAL,
|
||||||
|
ADD COLUMN truth_category TEXT,
|
||||||
|
ADD COLUMN truth_evaluated_at TIMESTAMPTZ,
|
||||||
|
ADD COLUMN ecan_sti REAL,
|
||||||
|
ADD COLUMN ecan_lti REAL;
|
||||||
|
|
||||||
|
-- Partial index: quickly find memories by truth category
|
||||||
|
CREATE INDEX idx_memories_truth_category
|
||||||
|
ON memories (truth_category)
|
||||||
|
WHERE truth_category IS NOT NULL;
|
||||||
|
|
||||||
|
-- Partial index: efficiently find unscored memories for the background worker
|
||||||
|
CREATE INDEX idx_memories_truth_unevaluated
|
||||||
|
ON memories (created_at)
|
||||||
|
WHERE truth_evaluated_at IS NULL;
|
||||||
115
src/config.rs
115
src/config.rs
@@ -15,6 +15,7 @@ pub struct Config {
|
|||||||
pub dedup: DedupConfig,
|
pub dedup: DedupConfig,
|
||||||
pub ttl: TtlConfig,
|
pub ttl: TtlConfig,
|
||||||
pub auth: AuthConfig,
|
pub auth: AuthConfig,
|
||||||
|
pub truth: TruthConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Server configuration
|
/// Server configuration
|
||||||
@@ -71,6 +72,41 @@ pub struct TtlConfig {
|
|||||||
pub cleanup_interval_seconds: u64,
|
pub cleanup_interval_seconds: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Truth scoring engine configuration
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct TruthConfig {
|
||||||
|
/// Enable truth scoring background worker
|
||||||
|
#[serde(default = "default_truth_enabled")]
|
||||||
|
pub enabled: bool,
|
||||||
|
/// Seconds between scoring cycles
|
||||||
|
#[serde(default = "default_scoring_interval_seconds")]
|
||||||
|
pub scoring_interval_seconds: u64,
|
||||||
|
/// Number of memories to score per cycle
|
||||||
|
#[serde(default = "default_truth_batch_size")]
|
||||||
|
pub batch_size: usize,
|
||||||
|
/// Seconds before a scored memory is re-evaluated
|
||||||
|
#[serde(default = "default_rescore_after_seconds")]
|
||||||
|
pub rescore_after_seconds: u64,
|
||||||
|
/// Base confidence for PLN deduction chains
|
||||||
|
#[serde(default = "default_pln_base_confidence")]
|
||||||
|
pub pln_base_confidence: f32,
|
||||||
|
/// ECAN STI decay rate per cycle (0.0–1.0)
|
||||||
|
#[serde(default = "default_ecan_decay_rate")]
|
||||||
|
pub ecan_decay_rate: f32,
|
||||||
|
/// ECAN attention spread factor
|
||||||
|
#[serde(default = "default_ecan_spread_factor")]
|
||||||
|
pub ecan_spread_factor: f32,
|
||||||
|
/// Similarity threshold above which conflicting memories are contradictions
|
||||||
|
#[serde(default = "default_contradiction_threshold")]
|
||||||
|
pub contradiction_threshold: f32,
|
||||||
|
/// Truth value threshold for "verified" categorization
|
||||||
|
#[serde(default = "default_verification_threshold")]
|
||||||
|
pub verification_threshold: f32,
|
||||||
|
/// Max related memories to cross-reference per scoring
|
||||||
|
#[serde(default = "default_cross_ref_limit")]
|
||||||
|
pub cross_ref_limit: i64,
|
||||||
|
}
|
||||||
|
|
||||||
/// Authentication configuration
|
/// Authentication configuration
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct AuthConfig {
|
pub struct AuthConfig {
|
||||||
@@ -139,6 +175,38 @@ fn default_auth_enabled() -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Truth engine defaults
|
||||||
|
fn default_truth_enabled() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
fn default_scoring_interval_seconds() -> u64 {
|
||||||
|
300
|
||||||
|
}
|
||||||
|
fn default_truth_batch_size() -> usize {
|
||||||
|
50
|
||||||
|
}
|
||||||
|
fn default_rescore_after_seconds() -> u64 {
|
||||||
|
86400
|
||||||
|
}
|
||||||
|
fn default_pln_base_confidence() -> f32 {
|
||||||
|
0.85
|
||||||
|
}
|
||||||
|
fn default_ecan_decay_rate() -> f32 {
|
||||||
|
0.95
|
||||||
|
}
|
||||||
|
fn default_ecan_spread_factor() -> f32 {
|
||||||
|
0.05
|
||||||
|
}
|
||||||
|
fn default_contradiction_threshold() -> f32 {
|
||||||
|
0.85
|
||||||
|
}
|
||||||
|
fn default_verification_threshold() -> f32 {
|
||||||
|
0.8
|
||||||
|
}
|
||||||
|
fn default_cross_ref_limit() -> i64 {
|
||||||
|
10
|
||||||
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
/// Load configuration from environment variables
|
/// Load configuration from environment variables
|
||||||
pub fn load() -> Result<Self> {
|
pub fn load() -> Result<Self> {
|
||||||
@@ -167,6 +235,41 @@ impl Config {
|
|||||||
)?
|
)?
|
||||||
// Auth settings
|
// Auth settings
|
||||||
.set_default("auth.enabled", default_auth_enabled())?
|
.set_default("auth.enabled", default_auth_enabled())?
|
||||||
|
// Truth engine settings
|
||||||
|
.set_default("truth.enabled", default_truth_enabled())?
|
||||||
|
.set_default(
|
||||||
|
"truth.scoring_interval_seconds",
|
||||||
|
default_scoring_interval_seconds() as i64,
|
||||||
|
)?
|
||||||
|
.set_default("truth.batch_size", default_truth_batch_size() as i64)?
|
||||||
|
.set_default(
|
||||||
|
"truth.rescore_after_seconds",
|
||||||
|
default_rescore_after_seconds() as i64,
|
||||||
|
)?
|
||||||
|
.set_default(
|
||||||
|
"truth.pln_base_confidence",
|
||||||
|
default_pln_base_confidence() as f64,
|
||||||
|
)?
|
||||||
|
.set_default(
|
||||||
|
"truth.ecan_decay_rate",
|
||||||
|
default_ecan_decay_rate() as f64,
|
||||||
|
)?
|
||||||
|
.set_default(
|
||||||
|
"truth.ecan_spread_factor",
|
||||||
|
default_ecan_spread_factor() as f64,
|
||||||
|
)?
|
||||||
|
.set_default(
|
||||||
|
"truth.contradiction_threshold",
|
||||||
|
default_contradiction_threshold() as f64,
|
||||||
|
)?
|
||||||
|
.set_default(
|
||||||
|
"truth.verification_threshold",
|
||||||
|
default_verification_threshold() as f64,
|
||||||
|
)?
|
||||||
|
.set_default(
|
||||||
|
"truth.cross_ref_limit",
|
||||||
|
default_cross_ref_limit(),
|
||||||
|
)?
|
||||||
// Load from environment with OPENBRAIN_ prefix
|
// Load from environment with OPENBRAIN_ prefix
|
||||||
.add_source(
|
.add_source(
|
||||||
config::Environment::with_prefix("OPENBRAIN")
|
config::Environment::with_prefix("OPENBRAIN")
|
||||||
@@ -231,6 +334,18 @@ impl Default for Config {
|
|||||||
enabled: default_auth_enabled(),
|
enabled: default_auth_enabled(),
|
||||||
api_keys: Vec::new(),
|
api_keys: Vec::new(),
|
||||||
},
|
},
|
||||||
|
truth: TruthConfig {
|
||||||
|
enabled: default_truth_enabled(),
|
||||||
|
scoring_interval_seconds: default_scoring_interval_seconds(),
|
||||||
|
batch_size: default_truth_batch_size(),
|
||||||
|
rescore_after_seconds: default_rescore_after_seconds(),
|
||||||
|
pln_base_confidence: default_pln_base_confidence(),
|
||||||
|
ecan_decay_rate: default_ecan_decay_rate(),
|
||||||
|
ecan_spread_factor: default_ecan_spread_factor(),
|
||||||
|
contradiction_threshold: default_contradiction_threshold(),
|
||||||
|
verification_threshold: default_verification_threshold(),
|
||||||
|
cross_ref_limit: default_cross_ref_limit(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
src/db.rs
14
src/db.rs
@@ -30,6 +30,13 @@ pub struct MemoryRecord {
|
|||||||
pub metadata: serde_json::Value,
|
pub metadata: serde_json::Value,
|
||||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||||
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
|
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
// Truth scoring fields (populated by background worker)
|
||||||
|
pub truth_value: Option<f32>,
|
||||||
|
pub truth_confidence: Option<f32>,
|
||||||
|
pub truth_category: Option<String>,
|
||||||
|
pub truth_evaluated_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
pub ecan_sti: Option<f32>,
|
||||||
|
pub ecan_lti: Option<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Query result with similarity score
|
/// Query result with similarity score
|
||||||
@@ -299,6 +306,13 @@ impl Database {
|
|||||||
metadata: row.get("metadata"),
|
metadata: row.get("metadata"),
|
||||||
created_at: row.get("created_at"),
|
created_at: row.get("created_at"),
|
||||||
expires_at: row.get("expires_at"),
|
expires_at: row.get("expires_at"),
|
||||||
|
// Truth fields will be populated by issue #39
|
||||||
|
truth_value: None,
|
||||||
|
truth_confidence: None,
|
||||||
|
truth_category: None,
|
||||||
|
truth_evaluated_at: None,
|
||||||
|
ecan_sti: None,
|
||||||
|
ecan_lti: None,
|
||||||
},
|
},
|
||||||
similarity: row.get("hybrid_score"),
|
similarity: row.get("hybrid_score"),
|
||||||
vector_score: row.get("vector_score"),
|
vector_score: row.get("vector_score"),
|
||||||
|
|||||||
@@ -12,3 +12,4 @@
|
|||||||
//! importance of memories, enabling natural prioritization of verified knowledge.
|
//! importance of memories, enabling natural prioritization of verified knowledge.
|
||||||
|
|
||||||
pub mod ecan;
|
pub mod ecan;
|
||||||
|
pub mod pln;
|
||||||
|
|||||||
319
src/truth/pln.rs
Normal file
319
src/truth/pln.rs
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
//! Probabilistic Logic Networks (PLN) deduction engine.
|
||||||
|
//!
|
||||||
|
//! Implements core PLN inference rules for truth-value propagation:
|
||||||
|
//! deduction, revision, negation, conjunction, and evidence-based scoring.
|
||||||
|
|
||||||
|
/// A PLN truth value combining strength (probability estimate) and confidence
|
||||||
|
/// (weight of evidence behind that estimate). Both fields are clamped to \[0.0, 1.0\].
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct TruthValue {
|
||||||
|
/// Probability estimate, 0.0–1.0.
|
||||||
|
pub strength: f32,
|
||||||
|
/// Weight of evidence, 0.0–1.0.
|
||||||
|
pub confidence: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TruthValue {
|
||||||
|
/// Create a new `TruthValue`, clamping both fields to \[0.0, 1.0\].
|
||||||
|
pub fn new(strength: f32, confidence: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
strength: strength.clamp(0.0, 1.0),
|
||||||
|
confidence: confidence.clamp(0.0, 1.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clamp a `TruthValue` so both fields lie within \[0.0, 1.0\].
|
||||||
|
#[inline]
|
||||||
|
fn clamped(tv: TruthValue) -> TruthValue {
|
||||||
|
TruthValue::new(tv.strength, tv.confidence)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// PLN Rules
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// **Deduction rule** — chain two implications A→B and B→C.
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// strength = s1 * s2
|
||||||
|
/// confidence = c1 * c2 * (s2 + (1 - s2) * (1 - c1))
|
||||||
|
/// ```
|
||||||
|
pub fn deduction(a_to_b: TruthValue, b_to_c: TruthValue) -> TruthValue {
|
||||||
|
let s1 = a_to_b.strength;
|
||||||
|
let c1 = a_to_b.confidence;
|
||||||
|
let s2 = b_to_c.strength;
|
||||||
|
let c2 = b_to_c.confidence;
|
||||||
|
|
||||||
|
let strength = s1 * s2;
|
||||||
|
let confidence = c1 * c2 * (s2 + (1.0 - s2) * (1.0 - c1));
|
||||||
|
|
||||||
|
clamped(TruthValue { strength, confidence })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **Revision rule** — merge two independent estimates into one.
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// strength = (s1*c1 + s2*c2) / (c1 + c2)
|
||||||
|
/// confidence = (c1 + c2) / (c1 + c2 + 1)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// If both confidences are zero the input `tv1` is returned unchanged
|
||||||
|
/// (no evidence to merge).
|
||||||
|
pub fn revision(tv1: TruthValue, tv2: TruthValue) -> TruthValue {
|
||||||
|
let s1 = tv1.strength;
|
||||||
|
let c1 = tv1.confidence;
|
||||||
|
let s2 = tv2.strength;
|
||||||
|
let c2 = tv2.confidence;
|
||||||
|
|
||||||
|
let denom = c1 + c2;
|
||||||
|
if denom == 0.0 {
|
||||||
|
return tv1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let strength = (s1 * c1 + s2 * c2) / denom;
|
||||||
|
let confidence = denom / (denom + 1.0);
|
||||||
|
|
||||||
|
clamped(TruthValue { strength, confidence })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **Negation rule** — logical NOT.
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// strength = 1.0 - s
|
||||||
|
/// confidence = c (unchanged)
|
||||||
|
/// ```
|
||||||
|
pub fn negation(tv: TruthValue) -> TruthValue {
|
||||||
|
clamped(TruthValue {
|
||||||
|
strength: 1.0 - tv.strength,
|
||||||
|
confidence: tv.confidence,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **Conjunction rule** — logical AND (independent assumptions).
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// strength = s1 * s2
|
||||||
|
/// confidence = min(c1, c2)
|
||||||
|
/// ```
|
||||||
|
pub fn conjunction(tv1: TruthValue, tv2: TruthValue) -> TruthValue {
|
||||||
|
clamped(TruthValue {
|
||||||
|
strength: tv1.strength * tv2.strength,
|
||||||
|
confidence: tv1.confidence.min(tv2.confidence),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **Evidence-based scoring** — refine a base truth value with confirming and
|
||||||
|
/// contradicting evidence.
|
||||||
|
///
|
||||||
|
/// 1. Iteratively apply [`revision`] across all `confirmations` to boost the
|
||||||
|
/// base estimate.
|
||||||
|
/// 2. For each contradiction, reduce confidence proportionally to the
|
||||||
|
/// contradiction's strength.
|
||||||
|
/// 3. Clamp the final result to \[0.0, 1.0\].
|
||||||
|
pub fn score_with_evidence(
|
||||||
|
base: TruthValue,
|
||||||
|
confirmations: &[TruthValue],
|
||||||
|
contradictions: &[TruthValue],
|
||||||
|
) -> TruthValue {
|
||||||
|
// Phase 1: merge confirmations via revision
|
||||||
|
let mut current = base;
|
||||||
|
for &conf in confirmations {
|
||||||
|
current = revision(current, conf);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: reduce confidence proportional to contradiction strength
|
||||||
|
for &contra in contradictions {
|
||||||
|
// Each contradiction chips away at confidence in proportion to its
|
||||||
|
// own strength (strong contradictions hurt more).
|
||||||
|
current.confidence *= 1.0 - contra.strength * contra.confidence;
|
||||||
|
}
|
||||||
|
|
||||||
|
clamped(current)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Helper: approximate f32 equality.
|
||||||
|
fn approx_eq(a: f32, b: f32) -> bool {
|
||||||
|
(a - b).abs() < 1e-5
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- deduction ---------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pln_deduction_basic() {
|
||||||
|
let ab = TruthValue::new(0.8, 0.9);
|
||||||
|
let bc = TruthValue::new(0.7, 0.8);
|
||||||
|
let result = deduction(ab, bc);
|
||||||
|
|
||||||
|
// strength = 0.8 * 0.7 = 0.56
|
||||||
|
assert!(approx_eq(result.strength, 0.56));
|
||||||
|
|
||||||
|
// confidence = 0.9 * 0.8 * (0.7 + 0.3 * 0.1) = 0.72 * 0.73 = 0.5256
|
||||||
|
let expected_c = 0.9 * 0.8 * (0.7 + (1.0 - 0.7) * (1.0 - 0.9));
|
||||||
|
assert!(approx_eq(result.confidence, expected_c));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pln_deduction_boundary() {
|
||||||
|
// (0, 0) -> zero everything
|
||||||
|
let r1 = deduction(TruthValue::new(0.0, 0.0), TruthValue::new(0.0, 0.0));
|
||||||
|
assert!(approx_eq(r1.strength, 0.0));
|
||||||
|
assert!(approx_eq(r1.confidence, 0.0));
|
||||||
|
|
||||||
|
// (1, 1) -> perfect chain
|
||||||
|
let r2 = deduction(TruthValue::new(1.0, 1.0), TruthValue::new(1.0, 1.0));
|
||||||
|
assert!(approx_eq(r2.strength, 1.0));
|
||||||
|
// confidence = 1*1*(1 + 0*0) = 1.0
|
||||||
|
assert!(approx_eq(r2.confidence, 1.0));
|
||||||
|
|
||||||
|
// (0.5, 0.5)
|
||||||
|
let half = TruthValue::new(0.5, 0.5);
|
||||||
|
let r3 = deduction(half, half);
|
||||||
|
assert!(approx_eq(r3.strength, 0.25));
|
||||||
|
let expected_c = 0.5 * 0.5 * (0.5 + 0.5 * 0.5);
|
||||||
|
assert!(approx_eq(r3.confidence, expected_c));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- revision ----------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pln_revision_merge() {
|
||||||
|
let tv1 = TruthValue::new(0.6, 0.5);
|
||||||
|
let tv2 = TruthValue::new(0.8, 0.7);
|
||||||
|
let result = revision(tv1, tv2);
|
||||||
|
|
||||||
|
let expected_s = (0.6 * 0.5 + 0.8 * 0.7) / (0.5 + 0.7);
|
||||||
|
let expected_c = (0.5 + 0.7) / (0.5 + 0.7 + 1.0);
|
||||||
|
assert!(approx_eq(result.strength, expected_s));
|
||||||
|
assert!(approx_eq(result.confidence, expected_c));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pln_revision_symmetry() {
|
||||||
|
let a = TruthValue::new(0.3, 0.4);
|
||||||
|
let b = TruthValue::new(0.9, 0.6);
|
||||||
|
|
||||||
|
let ab = revision(a, b);
|
||||||
|
let ba = revision(b, a);
|
||||||
|
|
||||||
|
assert!(approx_eq(ab.strength, ba.strength));
|
||||||
|
assert!(approx_eq(ab.confidence, ba.confidence));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- negation ----------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pln_negation() {
|
||||||
|
let tv = TruthValue::new(0.7, 0.8);
|
||||||
|
let result = negation(tv);
|
||||||
|
|
||||||
|
assert!(approx_eq(result.strength, 0.3));
|
||||||
|
assert!(approx_eq(result.confidence, 0.8));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- conjunction -------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pln_conjunction() {
|
||||||
|
let tv1 = TruthValue::new(0.8, 0.9);
|
||||||
|
let tv2 = TruthValue::new(0.6, 0.7);
|
||||||
|
let result = conjunction(tv1, tv2);
|
||||||
|
|
||||||
|
assert!(approx_eq(result.strength, 0.48));
|
||||||
|
assert!(approx_eq(result.confidence, 0.7));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- score_with_evidence -----------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pln_score_with_confirmations() {
|
||||||
|
let base = TruthValue::new(0.5, 0.3);
|
||||||
|
let confirmations = [
|
||||||
|
TruthValue::new(0.7, 0.5),
|
||||||
|
TruthValue::new(0.8, 0.6),
|
||||||
|
];
|
||||||
|
|
||||||
|
let result = score_with_evidence(base, &confirmations, &[]);
|
||||||
|
|
||||||
|
// Confirmations should boost both strength and confidence above base
|
||||||
|
assert!(result.strength > base.strength);
|
||||||
|
assert!(result.confidence > base.confidence);
|
||||||
|
assert!(result.strength <= 1.0);
|
||||||
|
assert!(result.confidence <= 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pln_score_with_contradictions() {
|
||||||
|
let base = TruthValue::new(0.8, 0.9);
|
||||||
|
let contradictions = [
|
||||||
|
TruthValue::new(0.7, 0.8),
|
||||||
|
];
|
||||||
|
|
||||||
|
let result = score_with_evidence(base, &[], &contradictions);
|
||||||
|
|
||||||
|
// Contradictions should reduce confidence
|
||||||
|
assert!(result.confidence < base.confidence);
|
||||||
|
// Strength unchanged when no confirmations
|
||||||
|
assert!(approx_eq(result.strength, base.strength));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- bounds ------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pln_values_bounded() {
|
||||||
|
// Extreme inputs that might push results out of range
|
||||||
|
let big = TruthValue::new(2.0, 5.0);
|
||||||
|
assert!(big.strength <= 1.0 && big.strength >= 0.0);
|
||||||
|
assert!(big.confidence <= 1.0 && big.confidence >= 0.0);
|
||||||
|
|
||||||
|
let neg = TruthValue::new(-1.0, -3.0);
|
||||||
|
assert!(neg.strength >= 0.0);
|
||||||
|
assert!(neg.confidence >= 0.0);
|
||||||
|
|
||||||
|
// Deduction of clamped extremes stays in bounds
|
||||||
|
let d = deduction(big, big);
|
||||||
|
assert!(d.strength >= 0.0 && d.strength <= 1.0);
|
||||||
|
assert!(d.confidence >= 0.0 && d.confidence <= 1.0);
|
||||||
|
|
||||||
|
// Negation of boundary stays in bounds
|
||||||
|
let n = negation(TruthValue::new(0.0, 1.0));
|
||||||
|
assert!(n.strength >= 0.0 && n.strength <= 1.0);
|
||||||
|
assert!(n.confidence >= 0.0 && n.confidence <= 1.0);
|
||||||
|
|
||||||
|
// score_with_evidence stays bounded
|
||||||
|
let scored = score_with_evidence(
|
||||||
|
TruthValue::new(1.0, 1.0),
|
||||||
|
&[TruthValue::new(1.0, 1.0)],
|
||||||
|
&[TruthValue::new(1.0, 1.0)],
|
||||||
|
);
|
||||||
|
assert!(scored.strength >= 0.0 && scored.strength <= 1.0);
|
||||||
|
assert!(scored.confidence >= 0.0 && scored.confidence <= 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- zero-confidence edge case -----------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pln_revision_zero_confidence() {
|
||||||
|
let tv1 = TruthValue::new(0.5, 0.0);
|
||||||
|
let tv2 = TruthValue::new(0.8, 0.0);
|
||||||
|
|
||||||
|
// Should not panic; returns tv1 when both confidences are zero
|
||||||
|
let result = revision(tv1, tv2);
|
||||||
|
assert!(approx_eq(result.strength, tv1.strength));
|
||||||
|
assert!(approx_eq(result.confidence, tv1.confidence));
|
||||||
|
|
||||||
|
// One zero, one nonzero — should still work
|
||||||
|
let tv3 = TruthValue::new(0.9, 0.6);
|
||||||
|
let result2 = revision(tv1, tv3);
|
||||||
|
assert!(result2.strength >= 0.0 && result2.strength <= 1.0);
|
||||||
|
assert!(result2.confidence >= 0.0 && result2.confidence <= 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user