diff --git a/.gitignore b/.gitignore index 5739422..c1e8416 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ node_modules/ package-lock.json dist/ .angular/ +rust/target/ +cathexis-rs/target/ +target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..4fcc042 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,190 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "cathexis" +version = "0.1.0" +dependencies = [ + "approx", + "eqbsl", + "ndarray", + "serde", +] + +[[package]] +name = "eqbsl" +version = "0.1.0" +dependencies = [ + "ndarray", + "serde", + "serde_json", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "matrixmultiply" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "ndarray" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "rawpointer", + "serde", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3a5cbcd --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +members = ["rust", "cathexis-rs"] +resolver = "2" diff --git a/EQBSL_Report.md b/EQBSL_Report.md index df15ca7..19a4b2f 100644 --- a/EQBSL_Report.md +++ b/EQBSL_Report.md @@ -1,7 +1,7 @@ # EQBSL: Potential Uses and Applications Report **Date:** February 21, 2026 -**Project:** EQBSL (Evidence-Based Quantum-resistant Belief State Logic) +**Project:** EQBSL (Evidence-Qualified Subjective Logic) --- @@ -12,7 +12,7 @@ EQBSL represents a paradigm shift in how digital systems model trust. Unlike tra By combining **Subjective Logic** (which explicitly models uncertainty), **Zero-Knowledge Proofs** (which ensure privacy), and **Vectorized Evidence** (which captures context), EQBSL enables decentralized systems to reason about trust in a way that is: * **Expressive:** Distinguishing between "trusted", "distrusted", and "unknown". * **Private:** Proving reputation without revealing sensitive interaction history. -* **Resilient:** Resistant to Sybil attacks and quantum decryption threats. +* **Resilient:** Better able to separate established evidence from uncertainty about new or weakly observed actors. --- @@ -29,7 +29,7 @@ This mapping allows systems to calculate trust dynamically: $$b = \frac{r}{r+s+K}, \quad d = \frac{s}{r+s+K}, \quad u = \frac{K}{r+s+K}$$ *(Where $r$ is positive evidence, $s$ is negative evidence, and $K$ is a protocol constant)* -### 2.2 Proof-Carrying Trust (ZK-EBSL) +### 2.2 Proof-Carrying Trust Trust updates are not just computed; they are **proven**. Using Zero-Knowledge Machine Learning (ZKML) techniques, an entity can prove that their new reputation score was correctly calculated from valid evidence without revealing *what* that evidence was (e.g., who they traded with or the specific transaction details). ### 2.3 Vectorized & Hypergraph Trust @@ -143,7 +143,7 @@ sequenceDiagram | **Transitivity** | Centralized Algorithm | Manual / Short paths | **Mathematical Discounting Operators** | | **Privacy** | Low (Centralized DB) | Low (Public Graph) | **High (Zero-Knowledge Proofs)** | | **Sybil Resistance**| ID Verification (KYC) | Reliance on Introducers | **Epistemic Uncertainty (High 'u')** | -| **Quantum Safety** | Low (RSA/ECC) | Low (RSA/ECC) | **High (PQ-Commitments)** | +| **Cryptographic Verifiability** | Low | Low | **High (via optional proof-carrying constructions)** | | **Granularity** | Coarse (Global Score) | Binary | **Context-Aware (Multi-dimensional)** | --- diff --git a/README.md b/README.md index 0b3021f..c479c0e 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ ## Documentation - [EQBSL Primer — core objects, state model, operator semantics, and embedding interface](docs/EQBSL-Primer.md) +- [CATHEXIS Manual — architecture and usage of the trust-handle layer](docs/CATHEXIS-Manual.md) - [Applied showcase: ZK-gated reputation airdrops using EQBSL](https://github.com/Steake/Reputation-Gated-Airdrop) --- @@ -15,12 +16,15 @@ https://eqbsl-demo.netlify.app/ -- **Evidence-Based Subjective Logic (EBSL)** – Move beyond binary trust scores to model uncertainty using evidence tuples (r, s, u) -- **Zero-Knowledge EBSL (ZK-EBSL)** – Privacy-preserving trust computations using zero-knowledge proofs -- **EQBSL** – Quantum-resistant extensions for distributed identity and reputation systems +- **Evidence-Based Subjective Logic (EBSL)** – Map evidence counts `(r, s)` into opinion tuples `(b, d, u, a)` with explicit uncertainty +- **Proof-carrying trust / ZK demos** – Explore how EQBSL state updates could be constrained or attested with zero-knowledge proofs +- **EQBSL (Evidence-Qualified Subjective Logic)** – Extend EBSL with vectorized evidence, operator-defined state evolution, hypergraph support, and embedding-oriented outputs - **Cathexis** – Emotional/motivational trust dynamics -Built with Angular 21 and TypeScript, this tool transforms complex cryptographic and epistemic concepts into intuitive, visual experiences. +Built with Angular 21 and TypeScript, this tool turns the papers' trust operators and research ideas into interactive visual experiences. The web app is primarily an exploration/demo environment; the Rust code is split into a layered stack: + +- [rust/](rust/) — the core EQBSL crate (opinions, evidence mapping, state model, embeddings) +- [cathexis-rs/](cathexis-rs/) — the CATHEXIS crate that builds on the shared EQBSL primitives to produce trust handles and categories > **Repository:** [`Steake/EQBSL`](https://github.com/Steake/EQBSL) > **App metadata:** [`metadata.json`](./metadata.json) @@ -36,49 +40,48 @@ Watch this comprehensive introduction to understand how EQBSL revolutionizes tru This video covers: - The fundamental limitations of traditional trust scores - How Evidence-Based Subjective Logic (EBSL) models uncertainty -- Zero-knowledge proofs for privacy-preserving trust verification -- Quantum-resistant extensions for future-proof security +- How proof-carrying / zero-knowledge trust updates fit into the research direction - Real-world applications in decentralized identity and reputation systems --- ## 📖 What is EQBSL? -**EQBSL (Evidence-based Quantum-resistant Belief State Logic)** is a mathematical framework for reasoning about trust, reputation, and epistemic uncertainty in distributed systems. Unlike traditional trust scores (e.g., "85% trusted"), EQBSL models the full epistemic state: +**EQBSL (Evidence-Qualified Subjective Logic)** is a mathematical framework for reasoning about trust, reputation, and epistemic uncertainty in distributed systems. In the terminology used by the papers and primer, EQBSL is **EBSL lifted into vector/tensor evidence, explicit operator-defined state evolution over time, hypergraph-native interactions, and embedding-first outputs, with optional proof-carrying updates**. Unlike traditional trust scores (e.g., "85% trusted"), EQBSL models the full epistemic state: - **Belief (b)** – Evidence supporting a proposition - **Disbelief (d)** – Evidence against a proposition -- **Uncertainty (u)** – Absence of evidence (where b + d + u = 1) +- **Uncertainty (u)** – Absence of evidence (where `b + d + u = 1`) +- **Base rate (a)** – Prior probability when evidence is absent ### Why EQBSL Matters Traditional reputation systems collapse complex trust relationships into a single number, losing critical information about: - **How much evidence** supports the rating (2 reviews vs. 2000 reviews) - **Uncertainty** when data is sparse or conflicting -- **Privacy** when revealing trust judgments -- **Quantum resistance** for future-proof cryptographic security +- **Typed evidence** across different channels or contexts +- **Time and structure** when trust evolves over graphs, hypergraphs, and repeated interactions +- **Privacy** when systems want to attest to trust updates without revealing raw evidence ### Key Innovations 1. **Evidence-Based Reasoning (EBSL)** - Trust opinions are computed from evidence tuples (r, s) representing positive and negative observations. This enables mathematically rigorous: + Trust opinions are computed from evidence counts `(r, s)` representing positive and negative observations, then lifted into opinions `ω = (b, d, u, a)`. This enables mathematically rigorous: - Trust transitivity (A trusts B, B trusts C → A's opinion of C) - Opinion fusion from multiple sources - Uncertainty quantification -2. **Zero-Knowledge Proofs (ZK-EBSL)** - Prove trust properties without revealing: - - The exact trust values - - The evidence supporting them - - The identities involved - - Example: "I can prove this vendor has >80% trust from 50+ verified buyers, without revealing who those buyers are." +2. **EQBSL State & Operators** + EQBSL extends basic EBSL by making the update pipeline explicit: + - Vectorized / tensor evidence per relationship + - Temporal decay and deterministic state evolution + - Hyperedge attribution for multi-party interactions + - Embedding-oriented outputs for downstream ML or policy systems + +3. **Proof-Carrying Trust (optional)** + The papers describe how EQBSL updates can be accompanied by zero-knowledge proofs or commitments so a system can attest that it followed the declared operator without revealing the underlying evidence. -3. **Quantum Resistance (EQBSL)** - Built on post-quantum cryptographic primitives to ensure trust systems remain secure against quantum computers, protecting: - - Long-term reputation data - - Privacy-preserving proofs - - Identity attestations + Example: "I can prove this published trust update respects the EQBSL transition rules without revealing the private interaction log behind it." 4. **Cathexis Integration** Models emotional/motivational dimensions of trust: @@ -90,9 +93,9 @@ Traditional reputation systems collapse complex trust relationships into a singl - **Decentralized Identity**: Web-of-trust without centralized authorities - **Reputation Systems**: Marketplaces, social networks, peer review -- **Secure Voting**: Verifiable ballot privacy with trust in validators +- **Secure Voting / Governance**: Reputation-weighted participation and validator selection - **Supply Chain**: Track product authenticity with uncertainty modeling -- **AI Safety**: Quantify and verify trust in AI agent behaviors +- **AI Safety**: Quantify trust in AI agent behaviors and preserve evidence lineage for downstream verification --- @@ -100,30 +103,101 @@ Traditional reputation systems collapse complex trust relationships into a singl - **EBSL Logic Calculator** – Experiment with belief/disbelief/uncertainty operations - **EQBSL Graph Visualizer** – Model trust networks with AI-assisted node identity generation -- **Zero-Knowledge Demos** – Explore privacy-preserving trust proofs +- **Proof-Carrying Trust Demo** – Explore how private trust updates can be attested - **Cathexis Simulator** – Understand emotional dynamics in trust relationships +- **Reputation-Gated Airdrop Example** – See how reputation scores drive eligibility and payout curves in an applied token distribution flow --- ## 🔬 Research Papers -This implementation is grounded in rigorous academic research. The `Papers/` directory contains: +This implementation is grounded in ongoing academic/research work. The `Papers/` directory contains: -- **EBSL in ZK Reputation Systems** – Foundations of zero-knowledge trust proofs -- **EQBSL+ZK** – Quantum-resistant extensions to EBSL -- **Proof-Carrying-Trust** – Verifiable trust computations +- **EBSL in ZK Reputation Systems** – EBSL integrated into privacy-preserving identity / reputation settings +- **EQBSL+ZK** – The systems-oriented EQBSL extension: vectorized evidence, explicit operators, embeddings, and the bridge to proof-carrying updates +- **Proof-Carrying-Trust** – Zero-knowledge constraints for EQBSL state transitions and verifiable trust computation For formal definitions, proofs, and protocol specifications, explore these papers and the broader [`EQBSL`](https://github.com/Steake/EQBSL) repository. --- +## 🦀 Rust Crates + +This repository contains two Rust crates that form a single layered toolchain: + +- [`rust/`](rust/) — the native EQBSL library implementing the trust/evidence calculus itself +- [`cathexis-rs/`](cathexis-rs/) — the CATHEXIS trust-handle layer that consumes EQBSL embeddings and feature states + +The EQBSL crate focuses on the trust/evidence calculus itself; proof-carrying updates are described in the papers rather than implemented as a proving system here. The CATHEXIS crate stays focused on categorisation and labeling, while reusing the shared EQBSL primitives rather than reimplementing them. + +### EQBSL Core Features + +- **Opinion Tuple** `(b, d, u, a)` with Consensus `⊕` and Discounting `⊗` operators +- **Evidence-to-Opinion mapping** via `calculate_opinion(r, s, k, a)` +- **m-dimensional evidence tensors** per relationship +- **Temporal decay** (`β^Δt` per channel), **hyperedge attribution**, and **transitive propagation** +- **Node trust embeddings** for downstream ML tasks +- **Full `serde` support** for JSON serialization + +### Installation + +Add to your `Cargo.toml`: + +```toml +[dependencies] +eqbsl = { path = "rust" } +ndarray = "0.15" +``` + +Or, from this repository root, build both crates together: + +```bash +cargo test --workspace +``` + +### Quick Start + +```rust +use eqbsl::*; + +// Map evidence to an opinion (10 positive, 0 negative, K=2, base rate=0.5) +let op = calculate_opinion(10.0, 0.0, DEFAULT_K, 0.5); +println!("b={:.3}, u={:.3}, E={:.3}", op.b, op.u, op.expectation()); +// → b=0.833, u=0.167, E=0.917 + +// Transitive trust: A trusts C via B +let op_ab = calculate_opinion(10.0, 0.0, DEFAULT_K, 0.5); +let op_bc = calculate_opinion(5.0, 0.0, DEFAULT_K, 0.5); +let op_ac = op_ab.discount(&op_bc); + +// Fuse two independent witnesses +let op_w = calculate_opinion(8.0, 1.0, DEFAULT_K, 0.5); +let fused = op_ac.fuse(&op_w); +println!("Fused E={:.3}", fused.expectation()); +``` + +For complete EQBSL documentation, architecture diagrams, and real-world examples (supply chain provenance, DAO voting, AI agent swarm trust, P2P lending), see [`rust/README.md`](./rust/README.md). + +### CATHEXIS Layer + +The [`cathexis-rs/`](cathexis-rs/) crate turns trust embeddings and behavioural/graph features into human-readable trust handles. It is designed as the semantic layer above EQBSL, not as a second implementation of the trust algebra. + +```bash +cargo test -p cathexis +``` + +See [`docs/CATHEXIS-Manual.md`](./docs/CATHEXIS-Manual.md) for the architecture and [`cathexis-rs/README.md`](./cathexis-rs/README.md) for crate-level usage. + +--- + ## 🛠️ Technology Stack - **Angular 21** – Modern reactive framework with zoneless change detection - **TypeScript 5.8** – Type-safe development - **RxJS** – Reactive data flows and state management -- **Tailwind CSS** – Utility-first styling for responsive UI +- **Tailwind CSS** – Utility-first styling for the UI - **Google Generative AI** – AI-assisted trust model exploration +- **Rust / Cargo** – Native EQBSL and CATHEXIS crates - **Angular CLI** – Build tooling and development server --- @@ -134,6 +208,7 @@ For formal definitions, proofs, and protocol specifications, explore these paper - **Node.js** 18+ (LTS recommended) – [Download here](https://nodejs.org/) - **npm** (bundled with Node.js) +- **Rust/Cargo** (optional, for the Rust crates) - **(Optional)** Google Generative AI API key – For AI-assisted features ### Installation @@ -151,6 +226,12 @@ cd EQBSL npm install ``` +3. **(Optional) Validate the Rust crates:** + +```bash +cargo test --workspace +``` + ### Configuration (Optional) For AI-assisted network identity generation, configure your Google Generative AI API key: @@ -208,6 +289,19 @@ This serves the app using production configuration (equivalent to `ng serve --co ``` EQBSL/ +├── Cargo.toml # Rust workspace (EQBSL + CATHEXIS) +├── rust/ # Rust crate — native EQBSL library +│ ├── src/ # Library source (opinion, ebsl, model, embedding) +│ ├── examples/ # Runnable examples +│ ├── tests/ # Integration & BDD tests +│ ├── Cargo.toml +│ └── README.md # Full Rust crate documentation +├── cathexis-rs/ # Rust crate — CATHEXIS trust-handle layer +│ ├── src/ # Feature extraction, categorisation, labeling pipeline +│ ├── examples/ # Runnable CATHEXIS demo +│ ├── tests/ # Pipeline tests +│ ├── Cargo.toml +│ └── README.md ├── Papers/ # Research papers (PDFs) ├── src/ │ ├── app.component.ts # Main Angular app component @@ -288,12 +382,12 @@ Visualize trust networks: - AI-generated identity profiles for realistic scenarios - Real-time trust propagation calculations -### ZK Demo +### Proof-Carrying Trust Demo -Explore zero-knowledge proofs: -- Privacy-preserving trust verification -- Commitment schemes for EBSL opinions -- Proof generation and verification +Explore the proof-carrying trust idea at a conceptual level: +- Privacy-preserving trust verification workflows +- Commitment / proof flow for EQBSL-style state updates +- Simulated proof generation and verification in the UI ### Cathexis @@ -345,6 +439,7 @@ EQBSL is part of a growing ecosystem of Subjective Logic implementations: | Project | Language | Description | |---------|----------|-------------| +| **[rust/ (this repo)](./rust/)** | **Rust** | **Native EQBSL crate — opinions, decay, propagation, embeddings** | | [liamzebedee/retrust](https://github.com/liamzebedee/retrust) | JS/Python | Subjective consensus algorithm with EBSL and sybil control | | [waleedqk/subjective-logic](https://github.com/waleedqk/subjective-logic) | Python | Pure subjective logic library with binomial opinions and fusion | | [atenearesearchgroup/uncertainty-datatypes-python](https://github.com/atenearesearchgroup/uncertainty-datatypes-python) | Python | Academic SL implementation from University of Málaga | @@ -358,13 +453,13 @@ See also: [Subjective Logic on GitHub](https://github.com/topics/subjective-logi ## 📄 License -This project is part of ongoing research by O. C. Hirst [Steake] & Shadowgraph Labs (2025). See the repository for license details. +MIT. See [LICENSE](LICENSE). --- ## 🙏 Acknowledgments -- Based on research in subjective logic, zero-knowledge proofs, and quantum-resistant cryptography +- Based on research in subjective logic, evidence-based trust, and zero-knowledge / proof-carrying verification - Built with modern web technologies for accessible epistemic reasoning - Special thanks to the Angular, TypeScript, and open-source communities diff --git a/angular.json b/angular.json index c102c5e..2d35f74 100644 --- a/angular.json +++ b/angular.json @@ -51,5 +51,8 @@ } } } + }, + "cli": { + "analytics": false } } \ No newline at end of file diff --git a/cathexis-rs/Cargo.toml b/cathexis-rs/Cargo.toml new file mode 100644 index 0000000..59bd639 --- /dev/null +++ b/cathexis-rs/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "cathexis" +version = "0.1.0" +edition = "2021" +description = "A Trust-Handle Layer for EQBSL Networks implementation in Rust" + +[dependencies] +eqbsl = { path = "../rust" } +serde = { version = "1.0", features = ["derive"] } +ndarray = { version = "0.15", features = ["serde"] } # Used for categoriser weights, activations, and probability vectors. + +[dev-dependencies] +approx = "0.5" diff --git a/cathexis-rs/README.md b/cathexis-rs/README.md new file mode 100644 index 0000000..03ac98a --- /dev/null +++ b/cathexis-rs/README.md @@ -0,0 +1,52 @@ +# CATHEXIS: A Trust-Handle Layer for EQBSL Networks + +This crate implements the CATHEXIS system as described in the paper "CATHEXIS: A Trust-Handle Layer for EQBSL Networks". + +It sits above the shared EQBSL core crate in [`../rust`](../rust): the trust algebra, evidence-to-opinion mapping, and embedding generation live there, while `cathexis-rs` focuses on feature assembly, categorisation, and labeling. + +## Overview + +CATHEXIS provides a bridge from EQBSL trust embeddings to human-usable trust handles. It consists of: + +1. **Categoriser Network**: Ingests trust signals, graph features, and behavioural features to map agents to latent categories. +2. **Labeling LLM**: Generates human-readable labels and descriptions for these categories. + +## Modules + +- `core`: CATHEXIS-facing Subjective Logic compatibility types (`Opinion`, `Evidence`) backed by the shared EQBSL primitives. +- `eqbsl`: EQBSL-facing structures (`TrustEmbedding`, `EvidenceTensor`) and TrustGraph interface. +- `features`: Feature extraction and representation. +- `categoriser`: Neural network for categorization (MLP baseline). +- `labeling`: Interface for the Labeling LLM. +- `pipeline`: Offline batch processing and online query handling. + +## Usage + +```rust +use cathexis::pipeline::CathexisPipeline; +use cathexis::categoriser::MLPCategoriser; +use cathexis::labeling::DummyLabeler; +// Implement TrustGraph for your data source +// ... +// let pipeline = CathexisPipeline::new(graph, categoriser, labeler); +// pipeline.batch_process()?; +// let handle = pipeline.query_agent_handle("agent_1")?; +``` + +If you are working inside this repository, you can validate both Rust crates together: + +```bash +cargo test --workspace +``` + +## Dependencies + +- `ndarray` for matrix operations. +- `serde` for serialization. +- `uuid`, `chrono` for utilities. + +## Status + +Implements the core logic and data structures. +Requires a concrete implementation of `TrustGraph` to connect to your specific EQBSL data source. +Requires a concrete implementation of `LabelingModel` to connect to an actual LLM (e.g. via API). diff --git a/cathexis-rs/examples/basic.rs b/cathexis-rs/examples/basic.rs new file mode 100644 index 0000000..099c5d6 --- /dev/null +++ b/cathexis-rs/examples/basic.rs @@ -0,0 +1,95 @@ +use cathexis::categoriser::MLPCategoriser; +use cathexis::eqbsl::{TrustGraph, TrustEmbedding}; +use cathexis::features::{FeatureState, TrustFeatures, GraphFeatures, BehaviouralFeatures}; +use cathexis::labeling::{LabelingModel, LabelInfo, CategorySummary}; +use cathexis::pipeline::CathexisPipeline; +use ndarray::{Array1, Array2}; + +// Mock implementation of TrustGraph +struct MockGraph { + nodes: Vec, +} + +impl TrustGraph for MockGraph { + fn get_nodes(&self) -> Vec { + self.nodes.clone() + } + + fn compute_features(&self, _agent_id: &str) -> Result { + // Return dummy features + Ok(FeatureState { + trust: TrustFeatures { + embedding: TrustEmbedding::new(vec![0.1, 0.2, 0.3]), + reputation_score: 0.8, + uncertainty: 0.1, + }, + graph: GraphFeatures { + degree: 10.0, + centrality: 0.5, + clustering_coefficient: 0.2, + extra_metrics: vec![], + }, + behavioural: BehaviouralFeatures { + temporal_activity: 0.9, + platform_metrics: vec![1.0, 0.0], + }, + }) + } +} + +// Mock implementation of LabelingModel +struct MockLabeler; + +impl LabelingModel for MockLabeler { + fn generate_label(&self, summary: &CategorySummary) -> Result { + Ok(LabelInfo { + handle: format!("Category-{}", summary.category_id), + gloss: "Auto-generated mock category".to_string(), + guidance: Some("Use with caution".to_string()), + }) + } +} + +fn main() -> Result<(), String> { + // 1. Setup Graph + let graph = MockGraph { + nodes: vec!["agent_1".to_string(), "agent_2".to_string()], + }; + + // 2. Setup Categoriser (Random weights for demo) + // Input dim: 3 (embedding) + 1 (rep) + 1 (unc) + 1 (deg) + 1 (cent) + 1 (clust) + 0 (extra) + 1 (temp) + 2 (plat) = 11 + // Output dim: 3 categories + // Hidden dim: 5 + let input_dim = 11; + let hidden_dim = 5; + let output_dim = 3; + + let w1 = Array2::zeros((hidden_dim, input_dim)); // In real use, load trained weights + let b1 = Array1::zeros(hidden_dim); + let w2 = Array2::zeros((output_dim, hidden_dim)); + let b2 = Array1::zeros(output_dim); + + let categoriser = MLPCategoriser::new(w1, b1, w2, b2); + + // 3. Setup Labeler + let labeler = MockLabeler; + + // 4. Create Pipeline + let mut pipeline = CathexisPipeline::new(graph, categoriser, labeler); + + // 5. Run Batch Process + println!("Running batch process..."); + pipeline.batch_process()?; + + // 6. Query Agent + let agent_id = "agent_1"; + println!("Querying agent: {}", agent_id); + let response = pipeline.query_agent_handle(agent_id)?; + + println!("Agent Handle: {}", response.label); + println!("Description: {}", response.description); + println!("Category ID: {}", response.category_id); + println!("Probabilities: {:?}", response.probabilities); + + Ok(()) +} diff --git a/cathexis-rs/src/categoriser.rs b/cathexis-rs/src/categoriser.rs new file mode 100644 index 0000000..b682237 --- /dev/null +++ b/cathexis-rs/src/categoriser.rs @@ -0,0 +1,142 @@ +use crate::features::FeatureState; +use ndarray::{Array1, Array2}; +use serde::{Deserialize, Serialize}; + +/// Trait for a Categoriser Network (Section 3, Equation 14). +/// f_theta: R^d -> Delta^{K-1} +pub trait Categoriser { + /// Maps a feature state to a probability distribution over K categories. + fn forward(&self, features: &FeatureState) -> Result, String>; + + /// Returns the hard category assignment (Section 3, Equation 15). + fn predict(&self, features: &FeatureState) -> Result { + let probs = self.forward(features)?; + + // Check for non-finite probabilities and return an error rather than silently picking arbitrary category. + if probs.iter().any(|p| !p.is_finite()) { + return Err("Non-finite probability detected in category prediction".to_string()); + } + + probs + .iter() + .enumerate() + .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) + .map(|(index, _)| index) + .ok_or_else(|| "Empty probability vector".to_string()) + } +} + +/// A simple MLP baseline categoriser (Section 4, Equation 20). +/// f(x) = softmax(W2 * sigma(W1 * x + b1) + b2) +#[derive(Debug, Serialize, Deserialize)] +pub struct MLPCategoriser { + /// W1: Hidden layer weights (dim_hidden x dim_input) + pub w1: Array2, + /// b1: Hidden layer bias (dim_hidden) + pub b1: Array1, + /// W2: Output layer weights (dim_output x dim_hidden) + pub w2: Array2, + /// b2: Output layer bias (dim_output) + pub b2: Array1, +} + +impl MLPCategoriser { + pub fn new( + w1: Array2, + b1: Array1, + w2: Array2, + b2: Array1, + ) -> Self { + Self { w1, b1, w2, b2 } + } +} + +fn sigmoid(x: &Array1) -> Array1 { + x.mapv(|v| 1.0 / (1.0 + (-v).exp())) +} + +fn softmax(x: &Array1) -> Result, String> { + // Validate that all input values are finite to avoid emitting NaN probabilities. + if x.iter().any(|v| !v.is_finite()) { + return Err("Non-finite value detected in softmax input".to_string()); + } + + let max = x.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b)); + let exp = x.mapv(|v| (v - max).exp()); + let sum = exp.sum(); + Ok(exp / sum) +} + +impl Categoriser for MLPCategoriser { + fn forward(&self, features: &FeatureState) -> Result, String> { + let x_vec = features.to_vector(); + // Convert Vec to Array1 + let x = Array1::from(x_vec); + + // Check dimensions + if x.len() != self.w1.shape()[1] { + return Err(format!( + "Input dimension mismatch: expected {}, got {}", + self.w1.shape()[1], + x.len() + )); + } + + // Layer 1: z1 = W1 * x + b1 + let z1 = self.w1.dot(&x) + &self.b1; + + // Activation: a1 = sigma(z1) + let a1 = sigmoid(&z1); + + // Layer 2: z2 = W2 * a1 + b2 + let z2 = self.w2.dot(&a1) + &self.b2; + + // Output: y = softmax(z2) + softmax(&z2) + } +} + +#[cfg(test)] +mod tests { + use super::{sigmoid, softmax}; + use ndarray::array; + + #[test] + fn sigmoid_handles_extreme_values() { + let values = array![-1000.0, 0.0, 1000.0]; + let output = sigmoid(&values); + + assert!(output[0] < 1e-10); + assert!((output[1] - 0.5).abs() < 1e-12); + assert!((output[2] - 1.0).abs() < 1e-10); + } + + #[test] + fn softmax_is_stable_and_normalized() { + let values = array![1000.0, 1001.0, 1002.0]; + let output = softmax(&values).expect("softmax should succeed for finite inputs"); + + assert!(output.iter().all(|value| value.is_finite() && *value > 0.0)); + assert!((output.sum() - 1.0).abs() < 1e-12); + assert!(output[2] > output[1]); + assert!(output[1] > output[0]); + } + + #[test] + fn softmax_rejects_nan_inputs() { + let values = array![1.0, f64::NAN, 2.0]; + let result = softmax(&values); + + assert!(result.is_err()); + assert!(result.unwrap_err().contains("Non-finite")); + } + + #[test] + fn softmax_rejects_infinity_inputs() { + let values = array![1.0, f64::INFINITY, 2.0]; + let result = softmax(&values); + + assert!(result.is_err()); + assert!(result.unwrap_err().contains("Non-finite")); + } +} diff --git a/cathexis-rs/src/core.rs b/cathexis-rs/src/core.rs new file mode 100644 index 0000000..b648135 --- /dev/null +++ b/cathexis-rs/src/core.rs @@ -0,0 +1,152 @@ +use eqbsl::{calculate_opinion, Opinion as EqbslOpinion}; +use serde::{Deserialize, Serialize}; + +/// Represents an opinion in Subjective Logic (Section 2.1). +/// $\omega_X^A = (b, d, u, a)$ where $b + d + u = 1$. +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct Opinion { + /// Belief mass + pub b: f64, + /// Disbelief mass + pub d: f64, + /// Uncertainty mass + pub u: f64, + /// Base rate (prior probability) + pub a: f64, +} + +impl Opinion { + /// Creates a new opinion, ensuring the constraint b + d + u = 1 (approx). + pub fn new(b: f64, d: f64, u: f64, a: f64) -> Result { + if (b + d + u - 1.0).abs() > 1e-6 { + return Err(format!("Invalid opinion: b+d+u must be 1, got {}", b + d + u)); + } + Ok(Self { b, d, u, a }) + } + + /// Vacuous opinion (complete uncertainty). + pub fn vacuous(a: f64) -> Self { + Self { + b: 0.0, + d: 0.0, + u: 1.0, + a, + } + } + + /// Expected probability E = b + a * u + pub fn expected_probability(&self) -> f64 { + EqbslOpinion::from(*self).expectation() + } +} + +impl From for Opinion { + fn from(opinion: EqbslOpinion) -> Self { + Self { + b: opinion.b, + d: opinion.d, + u: opinion.u, + a: opinion.a, + } + } +} + +impl From for EqbslOpinion { + fn from(opinion: Opinion) -> Self { + EqbslOpinion::new(opinion.b, opinion.d, opinion.u, opinion.a) + } +} + +/// Represents evidence counts for Evidence-Based Subjective Logic (EBSL) (Section 2.2). +/// r = positive evidence, s = negative evidence. +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct Evidence { + /// Positive evidence count + pub r: f64, + /// Negative evidence count + pub s: f64, + /// Normalisation constant K > 0 (usually 2.0 for binary, or W for weighted) + pub k: f64, +} + +impl Evidence { + /// Creates new evidence, validating that inputs are non-negative and k > 0. + pub fn new(r: f64, s: f64, k: f64) -> Result { + if r < 0.0 || s < 0.0 { + return Err(format!("Evidence counts must be non-negative: r={}, s={}", r, s)); + } + if k <= 0.0 { + return Err(format!("Normalization constant K must be positive: k={}", k)); + } + Ok(Self { r, s, k }) + } + + /// Maps evidence to a Subjective Logic opinion (Equation 3). + /// b = r / (r + s + K) + /// d = s / (r + s + K) + /// u = K / (r + s + K) + pub fn to_opinion(&self, base_rate: f64) -> Opinion { + calculate_opinion(self.r, self.s, self.k, base_rate).into() + } + + /// Combine with another evidence (additive property). + /// Takes self's K value. Both inputs must already be valid Evidence. + pub fn combine(&self, other: &Evidence) -> Evidence { + // In EBSL, evidence is additive: (r, s) + (r', s') = (r+r', s+s') + // We use self's K. If the caller needs matching K values, they should ensure this beforehand. + Evidence { + r: self.r + other.r, + s: self.s + other.s, + k: self.k, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use approx::assert_relative_eq; + + #[test] + fn test_evidence_to_opinion() { + let e = Evidence::new(8.0, 2.0, 2.0).expect("valid evidence"); // r=8, s=2, K=2 (total=12) + let op = e.to_opinion(0.5); + + // b = 8/12 = 2/3 = 0.666... + // d = 2/12 = 1/6 = 0.166... + // u = 2/12 = 1/6 = 0.166... + + assert_relative_eq!(op.b, 2.0/3.0); + assert_relative_eq!(op.d, 1.0/6.0); + assert_relative_eq!(op.u, 1.0/6.0); + assert_relative_eq!(op.b + op.d + op.u, 1.0); + } + + #[test] + fn test_evidence_rejects_negative_r() { + let result = Evidence::new(-1.0, 2.0, 2.0); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("non-negative")); + } + + #[test] + fn test_evidence_rejects_negative_s() { + let result = Evidence::new(1.0, -2.0, 2.0); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("non-negative")); + } + + #[test] + fn test_evidence_rejects_zero_k() { + let result = Evidence::new(1.0, 2.0, 0.0); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("positive")); + } + + #[test] + fn test_evidence_rejects_negative_k() { + let result = Evidence::new(1.0, 2.0, -1.0); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("positive")); + } +} diff --git a/cathexis-rs/src/eqbsl.rs b/cathexis-rs/src/eqbsl.rs new file mode 100644 index 0000000..b08b873 --- /dev/null +++ b/cathexis-rs/src/eqbsl.rs @@ -0,0 +1,63 @@ +use serde::{Deserialize, Serialize}; + +/// Represents an EQBSL trust embedding for an agent i at time t. +/// u_i(t) ∈ R^d_u (Section 2.3, Equation 9). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TrustEmbedding { + /// The raw embedding vector. + pub vector: Vec, + /// The dimension of the embedding (d_u). + pub dim: usize, +} + +impl TrustEmbedding { + pub fn new(vector: Vec) -> Self { + Self { + dim: vector.len(), + vector, + } + } +} + +impl From<::eqbsl::BasicEmbedding> for TrustEmbedding { + fn from(embedding: ::eqbsl::BasicEmbedding) -> Self { + Self::new(embedding.to_vec()) + } +} + +/// Represents an evidence tensor e_ij(t) ∈ R^m (Section 2.3, Equation 4). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EvidenceTensor { + pub components: Vec, +} + +/// Interface for the underlying Trust Graph / EQBSL engine. +pub trait TrustGraph { + /// Returns a list of all agent IDs in the graph. + fn get_nodes(&self) -> Vec; + + /// Returns the feature state for a given agent. + /// In a real implementation, this would compute features from G_t and U_t. + fn compute_features(&self, agent_id: &str) -> Result; +} + +#[cfg(test)] +mod tests { + use super::TrustEmbedding; + + #[test] + fn converts_eqbsl_embedding_without_losing_shape() { + let embedding = ::eqbsl::BasicEmbedding { + in_expect_mean: 0.2, + in_u_mean: 0.4, + out_expect_mean: 0.6, + out_u_mean: 0.8, + in_count: 2.0, + out_count: 3.0, + }; + let trust_embedding = TrustEmbedding::from(embedding); + + assert_eq!(trust_embedding.dim, 6); + assert_eq!(trust_embedding.vector, vec![0.2, 0.4, 0.6, 0.8, 2.0, 3.0]); + } +} diff --git a/cathexis-rs/src/features.rs b/cathexis-rs/src/features.rs new file mode 100644 index 0000000..9a3e79a --- /dev/null +++ b/cathexis-rs/src/features.rs @@ -0,0 +1,59 @@ +use serde::{Deserialize, Serialize}; +use crate::eqbsl::TrustEmbedding; + +/// Assembled feature state x_i(t) for an agent i (Section 3, Equation 10). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FeatureState { + /// a) Trust features (embedding, global reputation, uncertainty, etc.) + pub trust: TrustFeatures, + /// b) Graph features (degree, centrality, clustering, hyperedge signatures) + pub graph: GraphFeatures, + /// c) Behavioural features (platform-specific metrics, temporal statistics) + pub behavioural: BehaviouralFeatures, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TrustFeatures { + /// The EQBSL embedding u_i(t) + pub embedding: TrustEmbedding, + /// Other global trust metrics (e.g. reputation score) + pub reputation_score: f64, + pub uncertainty: f64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GraphFeatures { + pub degree: f64, + pub centrality: f64, + pub clustering_coefficient: f64, + pub extra_metrics: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BehaviouralFeatures { + pub temporal_activity: f64, + pub platform_metrics: Vec, +} + +impl FeatureState { + /// Flattens the feature state into a single vector x_i(t) ∈ R^d. + pub fn to_vector(&self) -> Vec { + let mut vec = Vec::new(); + // Trust + vec.extend(&self.trust.embedding.vector); + vec.push(self.trust.reputation_score); + vec.push(self.trust.uncertainty); + + // Graph + vec.push(self.graph.degree); + vec.push(self.graph.centrality); + vec.push(self.graph.clustering_coefficient); + vec.extend(&self.graph.extra_metrics); + + // Behavioural + vec.push(self.behavioural.temporal_activity); + vec.extend(&self.behavioural.platform_metrics); + + vec + } +} diff --git a/cathexis-rs/src/labeling.rs b/cathexis-rs/src/labeling.rs new file mode 100644 index 0000000..7564c86 --- /dev/null +++ b/cathexis-rs/src/labeling.rs @@ -0,0 +1,68 @@ +use serde::{Deserialize, Serialize}; + +/// Summary statistics for a category k (Section 5). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CategorySummary { + pub category_id: usize, + /// Top features contributing to this category + pub top_features: Vec, + /// Signed deviations from global means for key features + pub deviations: Vec<(String, f64)>, + /// Exemplar statistics (e.g. centroid or representative member stats) + pub exemplar_stats: Vec, + /// Platform provenance (where this category appears) + pub platform_provenance: String, +} + +/// The output from the Labeling LLM (Section 5). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LabelInfo { + /// A short handle (e.g. "high-risk flaky OTC counterparty") + pub handle: String, + /// A one-sentence gloss + pub gloss: String, + /// Optional risk / usage guidance + pub guidance: Option, +} + +/// Interface for the Labeling LLM. +pub trait LabelingModel { + /// Generates a label for a given category summary. + fn generate_label(&self, summary: &CategorySummary) -> Result; +} + +/// A dummy implementation for testing purposes. +pub struct DummyLabeler; + +impl LabelingModel for DummyLabeler { + fn generate_label(&self, summary: &CategorySummary) -> Result { + Ok(LabelInfo { + handle: format!("Category-{}", summary.category_id), + gloss: "Auto-generated category".to_string(), + guidance: None, + }) + } +} + +#[cfg(test)] +mod tests { + use super::{CategorySummary, DummyLabeler, LabelingModel}; + + #[test] + fn dummy_labeler_formats_category_handles() { + let labeler = DummyLabeler; + let summary = CategorySummary { + category_id: 7, + top_features: vec!["trust_embedding".to_string()], + deviations: Vec::new(), + exemplar_stats: vec![2.0], + platform_provenance: "EQBSL-Network".to_string(), + }; + + let label = labeler.generate_label(&summary).expect("dummy labeler should succeed"); + + assert_eq!(label.handle, "Category-7"); + assert_eq!(label.gloss, "Auto-generated category"); + assert_eq!(label.guidance, None); + } +} diff --git a/cathexis-rs/src/lib.rs b/cathexis-rs/src/lib.rs new file mode 100644 index 0000000..fc4085c --- /dev/null +++ b/cathexis-rs/src/lib.rs @@ -0,0 +1,9 @@ +pub mod core; +pub mod eqbsl; +pub mod features; +pub mod categoriser; +pub mod labeling; +pub mod pipeline; + +pub use core::{Opinion, Evidence}; +pub use eqbsl::TrustEmbedding; diff --git a/cathexis-rs/src/pipeline.rs b/cathexis-rs/src/pipeline.rs new file mode 100644 index 0000000..4616edd --- /dev/null +++ b/cathexis-rs/src/pipeline.rs @@ -0,0 +1,188 @@ +use crate::categoriser::Categoriser; +use crate::eqbsl::TrustGraph; +use crate::labeling::{LabelingModel, LabelInfo, CategorySummary}; +use crate::features::FeatureState; +use std::collections::HashMap; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct AgentHandleResponse { + pub category_id: usize, + pub probabilities: Vec, + pub label: String, + pub description: String, + pub guidance: Option, +} + +#[derive(Serialize, Deserialize)] +pub struct CathexisPipeline +where + G: TrustGraph, + C: Categoriser, + L: LabelingModel, +{ + pub graph: G, + pub categoriser: C, + pub labeler: L, + /// Maps category ID to its LabelInfo + pub category_labels: HashMap, +} + +impl CathexisPipeline +where + G: TrustGraph, + C: Categoriser, + L: LabelingModel, +{ + pub fn new(graph: G, categoriser: C, labeler: L) -> Self { + Self { + graph, + categoriser, + labeler, + category_labels: HashMap::new(), + } + } + + /// Offline batch processing (Section 6). + /// Computes features for all agents, assigns categories, and builds summaries/labels. + pub fn batch_process(&mut self) -> Result<(), String> { + let nodes = self.graph.get_nodes(); + let mut category_features: HashMap> = HashMap::new(); + + // Clear stale labels from previous runs to ensure consistency. + self.category_labels.clear(); + + // 1. Compute features and assign categories + for agent_id in &nodes { + let features = self.graph.compute_features(agent_id)?; + let category_id = self.categoriser.predict(&features)?; + category_features.entry(category_id).or_default().push(features); + } + + // 2. Build summaries and generate labels + for (category_id, features_list) in category_features { + // Build summary (simplified implementation) + let summary = self.build_category_summary(category_id, &features_list); + + // Generate or refresh the current category label from the summary. + let label_info = self.labeler.generate_label(&summary)?; + self.category_labels.insert(category_id, label_info); + } + + Ok(()) + } + + /// Online query (Section 6). + pub fn query_agent_handle(&self, agent_id: &str) -> Result { + let features = self.graph.compute_features(agent_id)?; + let probs_array = self.categoriser.forward(&features)?; + let probs_vec: Vec = probs_array.to_vec(); + + // Derive category_id from the same probability vector (argmax) to ensure consistency. + let category_id = probs_array + .iter() + .enumerate() + .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) + .map(|(index, _)| index) + .ok_or_else(|| "Failed to determine category from probabilities".to_string())?; + + let label_info = self.category_labels.get(&category_id) + .ok_or_else(|| format!("No label found for category {}", category_id))?; + + Ok(AgentHandleResponse { + category_id, + probabilities: probs_vec, + label: label_info.handle.clone(), + description: label_info.gloss.clone(), + guidance: label_info.guidance.clone(), + }) + } + + fn build_category_summary(&self, category_id: usize, feature_samples: &[FeatureState]) -> CategorySummary { + let sample_count = feature_samples.len() as f64; + + // In a real implementation, this would compute means, deviations, etc. + // It would also track drift in μ_k(t) and membership to decide when to split, merge, or re-label categories (Section 5). + // For now, return a lightweight summary derived from the available samples. + CategorySummary { + category_id, + top_features: vec!["trust_embedding".to_string(), "centrality".to_string()], + deviations: Vec::new(), + exemplar_stats: vec![sample_count], // placeholder until richer summary statistics are implemented + platform_provenance: "EQBSL-Network".to_string(), + } + } +} + +#[cfg(test)] +mod tests { + use super::CathexisPipeline; + use crate::categoriser::Categoriser; + use crate::eqbsl::{TrustEmbedding, TrustGraph}; + use crate::features::{BehaviouralFeatures, FeatureState, GraphFeatures, TrustFeatures}; + use crate::labeling::{DummyLabeler, LabelingModel}; + use ndarray::Array1; + + struct MockGraph; + + impl TrustGraph for MockGraph { + fn get_nodes(&self) -> Vec { + vec!["agent_1".to_string()] + } + + fn compute_features(&self, _agent_id: &str) -> Result { + Ok(sample_features()) + } + } + + struct FixedCategoriser; + + impl Categoriser for FixedCategoriser { + fn forward(&self, _features: &FeatureState) -> Result, String> { + Ok(Array1::from(vec![1.0])) + } + } + + fn sample_features() -> FeatureState { + FeatureState { + trust: TrustFeatures { + embedding: TrustEmbedding::new(vec![0.1, 0.2, 0.3]), + reputation_score: 0.8, + uncertainty: 0.1, + }, + graph: GraphFeatures { + degree: 10.0, + centrality: 0.5, + clustering_coefficient: 0.2, + extra_metrics: vec![], + }, + behavioural: BehaviouralFeatures { + temporal_activity: 0.9, + platform_metrics: vec![1.0, 0.0], + }, + } + } + + #[test] + fn build_category_summary_counts_samples() { + let pipeline = CathexisPipeline::new(MockGraph, FixedCategoriser, DummyLabeler); + let features = vec![sample_features(), sample_features()]; + + let summary = pipeline.build_category_summary(3, &features); + + assert_eq!(summary.category_id, 3); + assert_eq!(summary.exemplar_stats, vec![2.0]); + assert_eq!(summary.platform_provenance, "EQBSL-Network"); + assert!(summary.deviations.is_empty()); + assert_eq!(summary.top_features, vec!["trust_embedding", "centrality"]); + } + + #[test] + fn dummy_labeler_remains_compatible_with_pipeline_summary() { + let pipeline = CathexisPipeline::new(MockGraph, FixedCategoriser, DummyLabeler); + let summary = pipeline.build_category_summary(1, &[sample_features()]); + let label = DummyLabeler.generate_label(&summary).expect("dummy labeler should succeed"); + + assert_eq!(label.handle, "Category-1"); + } +} diff --git a/cathexis-rs/tests/pipeline_test.rs b/cathexis-rs/tests/pipeline_test.rs new file mode 100644 index 0000000..95d563e --- /dev/null +++ b/cathexis-rs/tests/pipeline_test.rs @@ -0,0 +1,95 @@ +use cathexis::categoriser::MLPCategoriser; +use cathexis::eqbsl::{TrustGraph, TrustEmbedding}; +use cathexis::features::{FeatureState, TrustFeatures, GraphFeatures, BehaviouralFeatures}; +use cathexis::labeling::{LabelingModel, LabelInfo, CategorySummary}; +use cathexis::pipeline::CathexisPipeline; +use ndarray::{Array1, Array2}; +use serde::{Deserialize, Serialize}; // Needed for mocking if they are used in pipeline serialization + +// Mock implementation of TrustGraph +#[derive(Serialize, Deserialize)] +struct MockGraph { + nodes: Vec, +} + +impl TrustGraph for MockGraph { + fn get_nodes(&self) -> Vec { + self.nodes.clone() + } + + fn compute_features(&self, _agent_id: &str) -> Result { + Ok(FeatureState { + trust: TrustFeatures { + embedding: TrustEmbedding::new(vec![0.1, 0.2, 0.3]), + reputation_score: 0.8, + uncertainty: 0.1, + }, + graph: GraphFeatures { + degree: 10.0, + centrality: 0.5, + clustering_coefficient: 0.2, + extra_metrics: vec![], + }, + behavioural: BehaviouralFeatures { + temporal_activity: 0.9, + platform_metrics: vec![1.0, 0.0], + }, + }) + } +} + +// Mock implementation of LabelingModel +#[derive(Serialize, Deserialize)] +struct MockLabeler; + +impl LabelingModel for MockLabeler { + fn generate_label(&self, summary: &CategorySummary) -> Result { + Ok(LabelInfo { + handle: format!("Category-{}", summary.category_id), + gloss: "Auto-generated mock category".to_string(), + guidance: Some("Use with caution".to_string()), + }) + } +} + +#[test] +fn test_pipeline_flow() { + // 1. Setup Graph + let graph = MockGraph { + nodes: vec!["agent_1".to_string(), "agent_2".to_string()], + }; + + // 2. Setup Categoriser + // Input dim: 3 (embedding) + 1 (rep) + 1 (unc) + 1 (deg) + 1 (cent) + 1 (clust) + 0 (extra) + 1 (temp) + 2 (plat) = 11 + // Output dim: 3 categories + // Hidden dim: 5 + let input_dim = 11; + let hidden_dim = 5; + let output_dim = 3; + + let w1 = Array2::zeros((hidden_dim, input_dim)); + let b1 = Array1::zeros(hidden_dim); + let w2 = Array2::zeros((output_dim, hidden_dim)); + let b2 = Array1::zeros(output_dim); + + let categoriser = MLPCategoriser::new(w1, b1, w2, b2); + + // 3. Setup Labeler + let labeler = MockLabeler; + + // 4. Create Pipeline + let mut pipeline = CathexisPipeline::new(graph, categoriser, labeler); + + // 5. Run Batch Process + let result = pipeline.batch_process(); + assert!(result.is_ok()); + + // 6. Query Agent + let agent_id = "agent_1"; + let response = pipeline.query_agent_handle(agent_id); + assert!(response.is_ok()); + + let response = response.unwrap(); + assert!(response.label.starts_with("Category-")); + assert_eq!(response.probabilities.len(), 3); +} diff --git a/docs/CATHEXIS-Manual.md b/docs/CATHEXIS-Manual.md new file mode 100644 index 0000000..4b1d28a --- /dev/null +++ b/docs/CATHEXIS-Manual.md @@ -0,0 +1,84 @@ +# CATHEXIS: Trust-Handle Layer for EQBSL Networks + +This document details **CATHEXIS**, a system for mapping high-dimensional EQBSL trust embeddings into human-readable "handles" and glosses. + +The reference implementation is provided as the `cathexis-rs` Rust crate in `EQBSL/cathexis-rs`, layered on top of the shared EQBSL core crate in `EQBSL/rust`. + +## 1. System Architecture + +CATHEXIS bridges the gap between raw mathematical trust signals (tensors) and human cognition (labels). + +```mermaid +graph LR + A[Trust Graph] -->|u_i(t)| B(Feature Extractor) + B -->|x_i(t)| C(Categoriser NN) + C -->|probs| D(Labeling LLM) + D -->|handle| E[Human User] +``` + +### The Pipeline + +1. **Feature Extraction**: Aggregates: + * **Trust Features**: The EQBSL embedding $u_i(t)$, reputation scores, uncertainty. + * **Graph Features**: Degree, centrality, clustering coefficients. + * **Behavioural Features**: Platform-specific metrics (e.g., trade volume, governance participation). +2. **Categorisation**: A neural network (MLP) maps the feature vector $x_i(t)$ to a probability distribution over latent categories $C = \{1, \dots, K\}$. +3. **Labeling**: An LLM (or oracle) generates semantic labels (handles) and glosses for each category based on the statistical properties of its members. + +## 2. Rust Implementation (`cathexis-rs`) + +The crate provides a modular, type-safe implementation of the CATHEXIS pipeline. + +### Location +`EQBSL/cathexis-rs` + +### Core Modules +* `core`: CATHEXIS-facing compatibility types that reuse the shared EQBSL opinion/evidence mapping. +* `eqbsl`: CATHEXIS embedding structures and adapters from the EQBSL crate's embedding output. +* `features`: `FeatureState` struct for assembling multi-modal signals. +* `categoriser`: `Categoriser` trait and `MLPCategoriser` implementation. +* `labeling`: `LabelingModel` trait for LLM integration. +* `pipeline`: Orchestrates the batch processing and online query flow. + +### Usage + +**Prerequisites**: Rust and Cargo installed. + +```bash +cd EQBSL +cargo test --workspace +``` + +**Running the Demo:** + +```bash +cargo run --example basic +``` + +**Example Code:** + +```rust +use cathexis::pipeline::CathexisPipeline; +// ... setup graph, categoriser, labeler ... + +let mut pipeline = CathexisPipeline::new(graph, categoriser, labeler); + +// 1. Offline: Build categories and labels +pipeline.batch_process()?; + +// 2. Online: Query a specific agent +let handle = pipeline.query_agent_handle("agent_1")?; +println!("Trust Handle: {}", handle.label); +``` + +## 3. Web Demo Integration + +The EQBSL Explorer includes a visual demonstration of the CATHEXIS layer. While the web demo runs in the browser (TypeScript), it simulates the logic defined in the Rust crate. + +### Key Concepts Demoed +* **Vectorization**: Seeing how simple actions (trades, failures) become high-dimensional vectors. +* **Categorization**: Watching the system classify an agent based on the vector. +* **Label Generation**: The final semantic output. + +--- +*For more details on the underlying mathematics, see the [EQBSL Primer](EQBSL-Primer.md).* diff --git a/metadata.json b/metadata.json index 7c6d770..252822d 100644 --- a/metadata.json +++ b/metadata.json @@ -1,5 +1,5 @@ { "name": "EQBSL Explorer", - "description": "Interactive playground for Evidence-Based Subjective Logic, ZK-EBSL, and EQBSL trust systems.", + "description": "Interactive playground for Evidence-Based Subjective Logic, proof-carrying trust, and EQBSL trust systems.", "requestFramePermissions": [] } \ No newline at end of file diff --git a/rust/Cargo.lock b/rust/Cargo.lock new file mode 100644 index 0000000..5711265 --- /dev/null +++ b/rust/Cargo.lock @@ -0,0 +1,171 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "eqbsl" +version = "0.1.0" +dependencies = [ + "ndarray", + "serde", + "serde_json", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "matrixmultiply" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "ndarray" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "rawpointer", + "serde", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 0000000..6c9fa18 --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "eqbsl" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +ndarray = { version = "0.15", features = ["serde"] } diff --git a/rust/README.md b/rust/README.md new file mode 100644 index 0000000..db178e3 --- /dev/null +++ b/rust/README.md @@ -0,0 +1,569 @@ +# EQBSL (Rust Crate) + +A Rust implementation of the **Evidence-Qualified Subjective Logic (EQBSL)** framework — a mathematically rigorous system for reasoning about trust, reputation, and epistemic uncertainty in distributed systems. + +Within this repository, this crate is the **shared trust core**. The higher-level [`cathexis-rs`](../cathexis-rs) crate builds on these primitives to derive semantic trust handles, rather than reimplementing the opinion algebra a second time. + +Unlike traditional scalar trust scores (e.g., a 5-star rating or a credit score), EQBSL represents trust as a rich **Opinion Tuple** `ω = (b, d, u, a)` that captures not just *how much* you trust something, but *how confident* you are in that assessment. + +--- + +## Table of Contents + +- [Core Concepts](#core-concepts) +- [Architecture](#architecture) +- [Features](#features) +- [Installation](#installation) +- [Quick Start](#quick-start) +- [API Reference](#api-reference) +- [Real-World Examples](#real-world-examples) + - [Supply Chain Provenance](#example-1-supply-chain-provenance) + - [DAO Reputation-Weighted Voting](#example-2-dao-reputation-weighted-voting) + - [AI Agent Swarm Trust](#example-3-ai-agent-swarm-trust) + - [Peer-to-Peer Lending](#example-4-peer-to-peer-lending) +- [Running Tests](#running-tests) +- [License](#license) + +--- + +## Core Concepts + +### The Opinion Tuple `ω = (b, d, u, a)` + +Every trust relationship is represented as a four-component opinion: + +| Component | Symbol | Meaning | +|-----------|--------|---------| +| Belief | `b` | Weight of positive evidence supporting the proposition | +| Disbelief | `d` | Weight of negative evidence against the proposition | +| Uncertainty | `u` | Lack of evidence; starts at 1.0, shrinks as evidence accumulates | +| Base Rate | `a` | Prior probability in the absence of any evidence | + +**Invariant:** `b + d + u = 1.0` always holds. + +### Evidence-to-Opinion Mapping + +Raw evidence counts `(r, s)` map directly to an opinion using the EBSL formula: + +``` +b = r / (r + s + K) +d = s / (r + s + K) +u = K / (r + s + K) +``` + +where `K` is a protocol constant (default `K = 2`) representing the prior weight. As evidence accumulates, `u → 0` and `b` or `d` approaches 1. + +### Expected Probability + +The **expectation** `E(ω) = b + a·u` gives a single scalar summary: your best estimate of the probability that the proposition is true, accounting for both evidence and the prior. + +--- + +## Architecture + +### Evidence-to-Trust Pipeline + +```mermaid +graph TD + subgraph Input["Input Layer"] + RE[Raw Events / Interactions] + HE[Group / Hyperedge Events] + end + + subgraph Evidence["Evidence Layer"] + RE -->|Pairwise extraction| PE["Pairwise Evidence Tensors
e_ij ∈ ℝᵐ"] + HE -->|Hyperedge attribution α| PE + end + + subgraph Decay["Temporal Decay"] + PE -->|"e_ij(t) = β^Δt ⊙ e_ij(t-1)"| DE["Decayed Evidence Tensors"] + end + + subgraph Opinion["Opinion Lifting"] + DE -->|"w_pos · e_ij → r
w_neg · e_ij → s"| RS["Scalar (r, s) pairs"] + RS -->|"EBSL: b=r/(r+s+K)"| OP["Opinion Tuples
ω = (b, d, u, a)"] + end + + subgraph Propagation["Trust Propagation"] + OP -->|"Discounting ⊗"| TR["Transitive Trust
A→C via B"] + OP -->|"Fusion ⊕"| FU["Fused Opinions
(multiple witnesses)"] + end + + subgraph Output["Output Layer"] + TR --> EMB["Node Embeddings
[in_expect, out_expect, ...]"] + FU --> EMB + EMB --> APP["Downstream Applications
Access Control · ML · Governance"] + end + + style Input stroke:#6699cc + style Evidence stroke:#cc9900 + style Decay stroke:#cc6666 + style Opinion stroke:#66cc66 + style Propagation stroke:#9966cc + style Output stroke:#3399cc +``` + +### Transitive Trust (Discounting) + +When A trusts B, and B has an opinion about C, A can derive an *indirect* opinion about C: + +```mermaid +graph LR + A -->|"ω_AB
b=0.83, u=0.17"| B + B -->|"ω_BC
b=0.71, u=0.29"| C + A -.->|"ω_AC = ω_AB ⊗ ω_BC
b=0.59, u=0.41"| C + + style A fill:#d4edda,stroke:#28a745,color:#000000 + style B fill:#fff3cd,stroke:#ffc107,color:#000000 + style C fill:#d1ecf1,stroke:#17a2b8,color:#000000 +``` + +*Note:* Uncertainty increases through each hop — a natural property of transitive inference. + +### Opinion Fusion (Consensus) + +Multiple independent witnesses can be combined via the **Consensus operator ⊕**: + +```mermaid +graph TD + W1["Witness 1
ω₁ = (0.71, 0.0, 0.29, 0.5)"] --> F["Fusion ⊕
ω_fused = (0.83, 0.0, 0.17, 0.5)"] + W2["Witness 2
ω₂ = (0.75, 0.0, 0.25, 0.5)"] --> F + W3["Witness 3
ω₃ = (0.63, 0.08, 0.29, 0.5)"] --> F + F --> R["High-confidence result
E(ω) = 0.91, u = 0.17"] + + style F fill:#d4edda,stroke:#28a745,color:#000000 + style R fill:#d1ecf1,stroke:#17a2b8,color:#000000 +``` + +--- + +## Features + +- **Subjective Logic Core**: Full `Opinion` type `(b, d, u, a)` with algebraically correct Consensus `⊕` and Discounting `⊗` operators. +- **Evidence-Based Mapping**: Direct mapping from evidence counts `(r, s)` to probabilistic opinions via `calculate_opinion`. +- **Vectorized Evidence (Tensors)**: `m`-dimensional evidence vectors per relationship — distinguish "late delivery" from "broken item" in a single tensor. +- **EQBSL Pipeline**: + - **Temporal Decay**: Exponential decay `β^Δt` per evidence channel. + - **Hyperedge Attribution**: Equal-weight distribution of group interaction evidence to all ordered pairs in the group. + - **Transitive Propagation**: Depth-1 indirect evidence aggregation with top-K witness selection. +- **Trust Embeddings**: Deterministic 6-dimensional node embeddings for downstream ML tasks. +- **Serialization**: Full `serde` support for JSON / any serde-compatible format. + +--- + +## Installation + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +eqbsl = { path = "path/to/eqbsl" } +ndarray = "0.15" +``` + +--- + +## Quick Start + +```rust +use eqbsl::*; + +fn main() { + // 1. Map evidence to an opinion + // 10 positive observations, 0 negative, K=2, base rate=0.5 + let op_ab = calculate_opinion(10.0, 0.0, 2.0, 0.5); + println!("Opinion A→B: {:?}", op_ab); + // Opinion { b: 0.833, d: 0.0, u: 0.167, a: 0.5 } + println!("Expectation: {:.3}", op_ab.expectation()); // 0.917 + + // 2. Transitive Trust via Discounting (A trusts C through B) + let op_bc = calculate_opinion(5.0, 0.0, 2.0, 0.5); + let op_ac = op_ab.discount(&op_bc); + println!("Opinion A→C (via B): {:?}", op_ac); + + // 3. Opinion Fusion: combine two independent witnesses + let op_witness = calculate_opinion(8.0, 1.0, 2.0, 0.5); + let op_fused = op_ac.fuse(&op_witness); + println!("Fused Opinion: {:?}", op_fused); + println!("Fused Expectation: {:.3}", op_fused.expectation()); +} +``` + +--- + +## API Reference + +### `Opinion` + +```rust +pub struct Opinion { pub b: f64, pub d: f64, pub u: f64, pub a: f64 } +``` + +| Method | Description | +|--------|-------------| +| `Opinion::new(b, d, u, a)` | Construct and auto-normalize so `b+d+u=1` | +| `opinion.expectation()` | Returns `b + a·u` — best scalar estimate | +| `op1.fuse(&op2)` | Consensus operator `⊕`: combine two independent opinions | +| `op1.discount(&op2)` | Discounting operator `⊗`: propagate trust transitively | + +### `calculate_opinion(r, s, k, a) -> Opinion` + +Maps raw evidence counts to an opinion. Pass `DEFAULT_K` (= 2.0) for the standard EBSL prior weight. + +### `Params` + +Global system configuration. Call `params.validate()` before use. + +```rust +Params { + k: 2.0, // Prior weight (EBSL constant K) + w_pos: array![1.0, 0.5], // Positive evidence channel weights + w_neg: array![0.0, 0.5], // Negative evidence channel weights + decay_beta: array![0.9, 0.9], // Per-channel decay factor β ∈ (0,1] + damping_lambda: 0.5, // Transitive propagation damping + witness_top_k: 10, // Max witnesses per node for propagation +} +``` + +### Pipeline Functions + +| Function | Description | +|----------|-------------| +| `State::new(t)` | Create an empty state at time `t` | +| `decay_state(&mut state, params, dt_steps)` | Apply `β^Δt` decay to all evidence | +| `attribute_hyperedges_to_pairs(&mut state)` | Distribute hyperedge evidence to pairwise edges | +| `compute_opinions(&state, params, base_rate)` | Lift all edges to `Opinion` tuples | +| `depth1_propagation_rs(nodes, opinions, edges, params)` | Add transitive evidence contributions | +| `embed_nodes_basic(nodes, opinions)` | Compute 6-dim trust embeddings per node | + +--- + +## Real-World Examples + +### Example 1: Supply Chain Provenance + +A buyer wants to know whether a product is authentic. Each step in the supply chain (manufacturer → distributor → retailer) adds evidence. Evidence decays over time. + +```mermaid +sequenceDiagram + participant M as Manufacturer + participant D as Distributor + participant R as Retailer + participant B as Buyer + + M->>D: Ships batch (adds positive evidence r=5) + Note over D: ω(M→D) = (0.71, 0.0, 0.29, 0.5) + + D->>R: Delivers on time (r=3, s=0) + Note over R: ω(D→R) = (0.60, 0.0, 0.40, 0.5) + + R->>B: Sells item + B->>B: Derives transitive trust
ω(M→B) via D and R + Note over B: E(ω) = 0.72, u = 0.49
Authentic but uncertain path +``` + +```rust +use eqbsl::*; + +fn supply_chain_example() { + // Multi-channel evidence: [quality_checks, on_time_deliveries] + let params = Params { + k: 2.0, + w_pos: array![0.7, 0.3], // quality matters more + w_neg: array![0.5, 0.5], + decay_beta: array![0.95, 0.90], // quality decays slower than timeliness + damping_lambda: 0.7, + witness_top_k: 5, + }; + + params.validate().expect("Invalid params"); + + let mut state = State::new(0); + state.edges.insert( + ("manufacturer".into(), "distributor".into()), + array![5.0, 2.0], + ); + // Distributor → Retailer: 3 quality checks, 4 on-time deliveries + state.edges.insert( + ("distributor".into(), "retailer".into()), + array![3.0, 4.0], + ); + + // Simulate 10 days of decay + decay_state(&mut state, ¶ms, 10); + + // Lift to opinions + let opinions = compute_opinions(&state, ¶ms, 0.5); + + // Direct trust: manufacturer → distributor + let op_md = opinions[&("manufacturer".into(), "distributor".into())]; + println!("Manufacturer→Distributor: b={:.3}, u={:.3}, E={:.3}", + op_md.b, op_md.u, op_md.expectation()); + + // Transitive trust: manufacturer → retailer via distributor + let nodes = vec!["manufacturer".to_string(), "distributor".to_string(), "retailer".to_string()]; + let propagated = depth1_propagation_rs(&nodes, &opinions, &state.edges, ¶ms); + let &(r, s) = propagated.get(&("manufacturer".into(), "retailer".into())).unwrap(); + let op_mr = calculate_opinion(r, s, params.k, 0.5); + println!("Manufacturer→Retailer (transitive): b={:.3}, u={:.3}, E={:.3}", + op_mr.b, op_mr.u, op_mr.expectation()); +} +``` + +--- + +### Example 2: DAO Reputation-Weighted Voting + +In a decentralized autonomous organization, members vote on proposals. Rather than "one token one vote" (plutocracy) or "one person one vote" (Sybil-vulnerable), EQBSL weights votes by each member's *certainty-adjusted reputation*. + +```mermaid +graph LR + subgraph Members + A["Alice
b=0.91, u=0.09
(veteran, high trust)"] + B["Bob
b=0.62, u=0.25
(active, moderate trust)"] + C["Carol
b=0.33, u=0.67
(new member, high uncertainty)"] + end + + subgraph Proposal["Proposal: Upgrade Protocol"] + V["Reputation-Weighted Vote
weight = b × (1 - u)"] + end + + A -->|"weight = 0.83"| V + B -->|"weight = 0.47"| V + C -->|"weight = 0.11"| V + + V --> R["Result: Passed
(all members favor)"] + + style A fill:#d4edda,stroke:#28a745,color:#000000 + style C fill:#fff3cd,stroke:#ffc107,color:#000000 + style R fill:#d1ecf1,stroke:#17a2b8,color:#000000 +``` + +```rust +use eqbsl::*; + +struct Member { name: String, opinion: Opinion } + +fn dao_voting_example() { + // Opinions derived from on-chain interaction history + let members = vec![ + Member { name: "alice".into(), opinion: calculate_opinion(20.0, 0.0, 2.0, 0.5) }, + Member { name: "bob".into(), opinion: calculate_opinion(5.0, 1.0, 2.0, 0.5) }, + Member { name: "carol".into(), opinion: calculate_opinion(1.0, 0.0, 2.0, 0.5) }, + ]; + + // Vote weight: belief × certainty (1 - uncertainty) + let total_weight: f64 = members.iter() + .map(|m| m.opinion.b * (1.0 - m.opinion.u)) + .sum(); + + println!("=== DAO Vote: Protocol Upgrade ==="); + for m in &members { + let weight = m.opinion.b * (1.0 - m.opinion.u); + println!(" {}: E={:.3}, weight={:.3} ({:.1}%)", + m.name, + m.opinion.expectation(), + weight, + 100.0 * weight / total_weight); + } + + // Fuse all member opinions to get the DAO's collective reputation state. + // This consensus opinion represents how much the group as a whole is trusted + // by an outside observer, not the vote outcome itself. + let consensus = members.iter().skip(1).fold(members[0].opinion, |acc, m| acc.fuse(&m.opinion)); + println!("DAO collective reputation: b={:.3}, u={:.3}, E={:.3}", + consensus.b, consensus.u, consensus.expectation()); +} +``` + +--- + +### Example 3: AI Agent Swarm Trust + +In a swarm of AI agents performing distributed inference tasks, agents accumulate positive evidence for peers that produce correct outputs and negative evidence for peers that hallucinate or behave maliciously. + +```mermaid +graph TD + subgraph Swarm["Agent Swarm (t=0)"] + A1["Agent-1
Coordinator"] + A2["Agent-2
Reliable"] + A3["Agent-3
Unreliable"] + A4["Agent-4
New Agent"] + end + + A1 -->|"r=12, s=1
b=0.80"| A2 + A1 -->|"r=2, s=8
d=0.67"| A3 + A1 -->|"r=0, s=0
u=1.0"| A4 + + subgraph Decision + D["Trust-Gated Task Assignment
Only agents with E > 0.7
and u < 0.3 receive critical tasks"] + end + + A2 -->|"E=0.90 ✓"| D + A3 -->|"E=0.19 ✗
Quarantined"| D + A4 -->|"E=0.50 ?
Probation"| D + + style A3 fill:#f8d7da,stroke:#dc3545,color:#000000 + style A2 fill:#d4edda,stroke:#28a745,color:#000000 + style A4 fill:#fff3cd,stroke:#ffc107,color:#000000 +``` + +```rust +use eqbsl::*; + +fn ai_swarm_trust_example() { + // Evidence channels: [task_correctness, response_latency_ok, format_compliance] + let params = Params { + k: 2.0, + w_pos: array![0.6, 0.2, 0.2], + w_neg: array![0.7, 0.1, 0.2], + decay_beta: array![0.98, 0.95, 0.99], // correctness decays slowly + damping_lambda: 0.6, + witness_top_k: 3, + }; + + let mut state = State::new(100); // t=100 (current step) + + // Coordinator's evidence about peers. + // NOTE: The diagram above uses simplified scalar (r, s) counts for illustration. + // The actual computation uses 3-channel weighted evidence vectors, so the + // numeric opinion values produced by the code will differ from the diagram labels. + state.edges.insert(("coord".into(), "agent_2".into()), array![12.0, 8.0, 10.0]); + state.edges.insert(("coord".into(), "agent_3".into()), array![2.0, 9.0, 1.0]); + state.edges.insert(("coord".into(), "agent_4".into()), array![0.0, 0.0, 0.0]); + + // Group task: agents 2 and 3 collaborated on a task + let mut h = std::collections::HashMap::new(); + h.insert("agent_2".to_string(), "executor".to_string()); + h.insert("agent_3".to_string(), "executor".to_string()); + state.hypers.insert("task_42".into(), Hyperedge { + hid: "task_42".into(), + nodes: vec!["agent_2".into(), "agent_3".into()], + roles: h, + e: array![1.0, 0.5, 1.0], // positive group evidence + }); + attribute_hyperedges_to_pairs(&mut state); + + let opinions = compute_opinions(&state, ¶ms, 0.5); + + println!("=== AI Swarm Trust Assessment ==="); + let agents = ["agent_2", "agent_3", "agent_4"]; + for agent in &agents { + let key = ("coord".to_string(), agent.to_string()); + if let Some(op) = opinions.get(&key) { + let status = if op.expectation() > 0.7 && op.u < 0.3 { + "✓ TRUSTED - eligible for critical tasks" + } else if op.expectation() < 0.4 { + "✗ QUARANTINED" + } else { + "? PROBATION - monitoring" + }; + println!(" {}: b={:.3}, d={:.3}, u={:.3}, E={:.3} => {}", + agent, op.b, op.d, op.u, op.expectation(), status); + } else { + println!(" {}: no evidence => u=1.0, E=0.5 => ? PROBATION", agent); + } + } +} +``` + +--- + +### Example 4: Peer-to-Peer Lending + +A decentralized lending protocol needs creditworthiness scores. EQBSL models credit as a belief state that accumulates over repayment history and decays over time. + +```mermaid +sequenceDiagram + participant B as Borrower + participant P as Protocol + participant L1 as Lender 1 + participant L2 as Lender 2 + + Note over B, P: Borrower has 8 successful repayments, 1 late payment + B->>P: Request loan (collateral = f(u)) + P->>P: Compute ω_credit = calculate_opinion(r=8, s=1, k=2, a=0.5) + Note over P: b=0.73, d=0.09, u=0.18, E=0.82 + + P->>L1: Publish anonymized opinion commitment + P->>L2: Publish anonymized opinion commitment + + L1->>L1: Direct evidence: r=5, s=0 → ω₁=(0.71,0,0.29,0.5) + L2->>L2: Direct evidence: r=3, s=1 → ω₂=(0.50,0.17,0.33,0.5) + + L1->>P: Submit fused opinion + L2->>P: Submit fused opinion + P->>P: Fuse all opinions → ω_final + Note over P: E(ω_final) = 0.88, u = 0.09
High confidence → low collateral required + + P-->>B: Loan approved (collateral = 1 - E = 12%) +``` + +```rust +use eqbsl::*; + +fn p2p_lending_example() { + // Borrower's direct repayment history: 8 on-time, 1 late + let direct = calculate_opinion(8.0, 1.0, 2.0, 0.5); + println!("Borrower credit (direct): b={:.3}, d={:.3}, u={:.3}, E={:.3}", + direct.b, direct.d, direct.u, direct.expectation()); + + // Two lenders have independent evidence + let lender1 = calculate_opinion(5.0, 0.0, 2.0, 0.5); + let lender2 = calculate_opinion(3.0, 1.0, 2.0, 0.5); + + // Fuse all three independent opinions + let fused = direct.fuse(&lender1).fuse(&lender2); + println!("Fused credit opinion: b={:.3}, d={:.3}, u={:.3}, E={:.3}", + fused.b, fused.d, fused.u, fused.expectation()); + + // Required collateral inversely proportional to trust certainty + let collateral_rate = 1.0 - fused.expectation(); + println!("Required collateral rate: {:.1}%", collateral_rate * 100.0); + // → 12.3% collateral (high trust, low uncertainty) + + // Compare: new borrower (no history) requires much more collateral + let new_borrower = calculate_opinion(0.0, 0.0, 2.0, 0.5); + let new_collateral = 1.0 - new_borrower.expectation(); + println!("New borrower collateral rate: {:.1}%", new_collateral * 100.0); + // → 50.0% collateral (maximum uncertainty) +} +``` + +--- + +## Running Tests + +```bash +# Run all tests +cargo test + +# Run a specific example +cargo run --example basic_usage + +# Run with output visible +cargo test -- --nocapture +``` + +--- + +## Comparison: EQBSL vs. EBSL and Traditional Trust Systems + +| Feature | Traditional Score | Web-of-Trust | EBSL | EQBSL | +|:--------|:-----------------|:-------------|:-----|:------| +| Representation | Scalar (e.g., 4.2 ★) | Boolean | Opinion tuple `(b, d, u, a)` | **Tuple (b, d, u, a)** | +| Uncertainty modeling | ✗ | ✗ | ✓ Explicit via `u` | **✓ Explicit via `u`** | +| Evidence dimensionality | Scalar | None | Scalar evidence counts `(r, s)` | **m-dimensional tensors** | +| Transitivity | Proprietary | Manual chains | Discounting `⊗` | **Formal Discounting ⊗** | +| Multi-witness fusion | Weighted avg | None | Consensus `⊕` | **Algebraic Consensus ⊕** | +| Temporal decay | Ad-hoc | None | Not built-in | **Per-channel β^Δt** | +| Sybil resistance | KYC | Introducer trust | Partial† (via uncertainty) | **Partial† (via uncertainty)** | +| Serializable / portable | Partial | ✗ | Varies by implementation | **✓ Full serde support** | + +† EQBSL does not itself provide Sybil resistance; it computes trust (opinions) over observed events. Its explicit modeling of epistemic uncertainty about new or unknown identities can inform downstream Sybil-resistance policies, but it is not a Sybil-resistance mechanism on its own. + +--- + +## License + +MIT diff --git a/rust/examples/basic_usage.rs b/rust/examples/basic_usage.rs new file mode 100644 index 0000000..53fba28 --- /dev/null +++ b/rust/examples/basic_usage.rs @@ -0,0 +1,42 @@ +use eqbsl::*; + +fn main() { + // 1. Define Parameters + let params = Params { + k: 2.0, + w_pos: array![1.0, 0.5], // weights for 2-dimensional evidence + w_neg: array![0.0, 0.5], + decay_beta: array![0.9, 0.9], + damping_lambda: 0.5, + witness_top_k: 10, + }; + params.validate().expect("Invalid params"); + + // 2. Initialize State + let mut state = State::new(0); + + // Node A interacts with Node B + // Evidence: [1.0, 0.0] (1 positive unit in first channel) + state.edges.insert(("A".to_string(), "B".to_string()), array![1.0, 0.0]); + + // 3. Compute Opinions + let opinions = compute_opinions(&state, ¶ms, 0.5); + + if let Some(op_ab) = opinions.get(&("A".to_string(), "B".to_string())) { + println!("Opinion A -> B: {:?}", op_ab); + println!("Expectation A -> B: {}", op_ab.expectation()); + } + + // 4. Test Fusion + let op1 = calculate_opinion(10.0, 2.0, 2.0, 0.5); + let op2 = calculate_opinion(5.0, 1.0, 2.0, 0.5); + let fused = op1.fuse(&op2); + println!("Fused Opinion: {:?}", fused); + + // 5. Test Discounting + let op_ab = calculate_opinion(10.0, 0.0, 2.0, 0.5); // A trusts B + let op_bc = calculate_opinion(10.0, 0.0, 2.0, 0.5); // B trusts C + let op_ac = op_ab.discount(&op_bc); // A trusts C via B + println!("Discounted Opinion A -> C: {:?}", op_ac); + println!("Expectation A -> C: {}", op_ac.expectation()); +} diff --git a/rust/src/ebsl.rs b/rust/src/ebsl.rs new file mode 100644 index 0000000..e2394d8 --- /dev/null +++ b/rust/src/ebsl.rs @@ -0,0 +1,41 @@ +//! Evidence-Based Subjective Logic (EBSL) mapping. + +use crate::opinion::Opinion; + +/// Default protocol parameter K (prior weight / pseudocount mass). +pub const DEFAULT_K: f64 = 2.0; + +/// Maps evidence (r, s) to a Subjective Logic opinion. +/// +/// r: Positive evidence +/// s: Negative evidence +/// k: Normalization constant (prior weight) +/// a: Base rate +/// +/// Formulas: +/// b = r / (r + s + k) +/// d = s / (r + s + k) +/// u = k / (r + s + k) +pub fn calculate_opinion(r: f64, s: f64, k: f64, a: f64) -> Opinion { + let denominator = r + s + k; + Opinion::new( + r / denominator, + s / denominator, + k / denominator, + a + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_calculate_opinion() { + let op = calculate_opinion(2.0, 0.0, 2.0, 0.5); + assert_eq!(op.b, 0.5); + assert_eq!(op.d, 0.0); + assert_eq!(op.u, 0.5); + assert_eq!(op.expectation(), 0.75); + } +} diff --git a/rust/src/embedding.rs b/rust/src/embedding.rs new file mode 100644 index 0000000..3536d50 --- /dev/null +++ b/rust/src/embedding.rs @@ -0,0 +1,76 @@ +//! Node-level Trust Embeddings. + +use std::collections::HashMap; +use crate::opinion::Opinion; +use crate::model::{NodeId, EdgeKey}; + +/// Deterministic baseline embedding for a node. +/// +/// The embedding vector contains: +/// [in_expect_mean, in_u_mean, out_expect_mean, out_u_mean, in_count, out_count] +#[derive(Debug, Clone, PartialEq)] +pub struct BasicEmbedding { + pub in_expect_mean: f64, + pub in_u_mean: f64, + pub out_expect_mean: f64, + pub out_u_mean: f64, + pub in_count: f64, + pub out_count: f64, +} + +impl BasicEmbedding { + pub fn to_vec(&self) -> Vec { + vec![ + self.in_expect_mean, + self.in_u_mean, + self.out_expect_mean, + self.out_u_mean, + self.in_count, + self.out_count, + ] + } +} + +pub fn embed_nodes_basic( + nodes: &[NodeId], + opinions: &HashMap +) -> HashMap { + let mut out = HashMap::new(); + + for i in nodes { + let mut in_exps = Vec::new(); + let mut in_us = Vec::new(); + let mut out_exps = Vec::new(); + let mut out_us = Vec::new(); + let mut in_count = 0.0; + let mut out_count = 0.0; + + for (key, op) in opinions { + if &key.1 == i { + in_count += 1.0; + in_exps.push(op.expectation()); + in_us.push(op.u); + } + if &key.0 == i { + out_count += 1.0; + out_exps.push(op.expectation()); + out_us.push(op.u); + } + } + + fn mean(x: &[f64]) -> f64 { + if x.is_empty() { 0.0 } else { x.iter().sum::() / x.len() as f64 } + } + + out.insert(i.clone(), BasicEmbedding { + in_expect_mean: mean(&in_exps), + in_u_mean: mean(&in_us), + out_expect_mean: mean(&out_exps), + out_u_mean: mean(&out_us), + in_count, + out_count, + }); + } + + out +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs new file mode 100644 index 0000000..4364b91 --- /dev/null +++ b/rust/src/lib.rs @@ -0,0 +1,13 @@ +pub mod opinion; +pub mod ebsl; +pub mod model; +pub mod embedding; + +pub use opinion::Opinion; +pub use ebsl::{calculate_opinion, DEFAULT_K}; +pub use model::*; +pub use embedding::{embed_nodes_basic, BasicEmbedding}; + +/// Re-export ndarray for convenience when using this crate +pub use ndarray::Array1; +pub use ndarray::array; diff --git a/rust/src/model.rs b/rust/src/model.rs new file mode 100644 index 0000000..db62222 --- /dev/null +++ b/rust/src/model.rs @@ -0,0 +1,214 @@ +//! EQBSL State Model and Pipeline Operators. +//! +//! This module implements the core EQBSL pipeline: +//! Ingest -> Decay -> Hyperedge Attribution -> Propagation -> Opinion Lift. + +use std::collections::HashMap; +use serde::{Deserialize, Serialize}; +use ndarray::Array1; +use crate::opinion::Opinion; +use crate::ebsl::calculate_opinion; + +/// Unique identifier for a node/agent. +pub type NodeId = String; +/// Key for a directed edge between two nodes. +pub type EdgeKey = (NodeId, NodeId); +/// Unique identifier for a hyperedge. +pub type HyperId = String; + +/// Global parameters for the EQBSL system. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Params { + /// EBSL normalization constant K. + pub k: f64, + /// Weights for positive evidence aggregation (length m). + pub w_pos: Array1, + /// Weights for negative evidence aggregation (length m). + pub w_neg: Array1, + /// Temporal decay factors per channel (length m, values in (0, 1]). + pub decay_beta: Array1, + /// Damping constant for transitive propagation (values in (0, 1]). + pub damping_lambda: f64, + /// Maximum number of witnesses to consider for propagation. + pub witness_top_k: usize, +} + +impl Params { + /// Validates that parameters are within expected ranges. + pub fn validate(&self) -> Result<(), String> { + if self.k <= 0.0 { + return Err("k must be > 0".to_string()); + } + let m = self.w_pos.len(); + if m == 0 || self.w_neg.len() != m || self.decay_beta.len() != m { + return Err("w_pos, w_neg, decay_beta must have same nonzero length".to_string()); + } + if self.w_pos.iter().any(|&x| x < 0.0) || self.w_neg.iter().any(|&x| x < 0.0) { + return Err("w_pos and w_neg must be nonnegative".to_string()); + } + if self.decay_beta.iter().any(|&x| x <= 0.0 || x > 1.0) { + return Err("decay_beta must be in (0,1]".to_string()); + } + if !(0.0 < self.damping_lambda && self.damping_lambda <= 1.0) { + return Err("damping_lambda must be in (0,1]".to_string()); + } + Ok(()) + } +} + +/// Represents a multi-party interaction involving multiple nodes with specific roles. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Hyperedge { + pub hid: HyperId, + pub nodes: Vec, + pub roles: HashMap, + /// m-dimensional evidence tensor. + pub e: Array1, +} + +/// The global state of the EQBSL system at time t. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct State { + /// Current timestamp or step. + pub t: u64, + /// Pairwise evidence tensors. + pub edges: HashMap>, + /// Hyperedge evidence tensors. + pub hypers: HashMap, +} + +impl State { + /// Creates a new empty state. + pub fn new(t: u64) -> Self { + Self { + t, + edges: HashMap::new(), + hypers: HashMap::new(), + } + } +} + +/// Projects an m-dimensional evidence vector into scalar (r, s) space. +pub fn rs_from_vec(e: &Array1, w_pos: &Array1, w_neg: &Array1) -> (f64, f64) { + let r = e.dot(w_pos); + let s = e.dot(w_neg); + (r.max(0.0), s.max(0.0)) +} + +/// Applies temporal decay to all evidence in the state. +pub fn decay_state(state: &mut State, params: &Params, dt_steps: u32) { + if dt_steps == 0 { + return; + } + + let beta_dt = params.decay_beta.mapv(|x| x.powi(dt_steps as i32)); + + for e in state.edges.values_mut() { + *e *= &beta_dt; + } + + for h in state.hypers.values_mut() { + h.e *= &beta_dt; + } +} + +/// Allocates hyperedge evidence into pairwise evidence tensors. +/// +/// This baseline implementation distributes evidence equally across all ordered pairs in the hyperedge. +pub fn attribute_hyperedges_to_pairs(state: &mut State) { + let mut additions: HashMap> = HashMap::new(); + + for h in state.hypers.values() { + let n = h.nodes.len(); + if n < 2 { continue; } + + let alpha = 1.0 / (n * (n - 1)) as f64; + + for i in &h.nodes { + for j in &h.nodes { + if i == j { continue; } + let key = (i.clone(), j.clone()); + let scaled = h.e.clone() * alpha; + + additions.entry(key) + .and_modify(|e| *e += &scaled) + .or_insert(scaled); + } + } + } + + for (key, val) in additions { + state.edges.entry(key) + .and_modify(|e| *e += &val) + .or_insert(val); + } +} + +/// Lifts evidence tensors into Subjective Logic opinions. +pub fn compute_opinions(state: &State, params: &Params, base_rate: f64) -> HashMap { + let mut out = HashMap::new(); + for (key, e) in &state.edges { + let (r, s) = rs_from_vec(e, ¶ms.w_pos, ¶ms.w_neg); + out.insert(key.clone(), calculate_opinion(r, s, params.k, base_rate)); + } + out +} + +/// Performs depth-1 transitive propagation of trust in (r, s) space. +/// +/// r_ij_total = r_ij_direct + Σ_k λ * δ_ik * r_kj +/// s_ij_total = s_ij_direct + Σ_k λ * δ_ik * s_kj +pub fn depth1_propagation_rs( + nodes: &[NodeId], + opinions: &HashMap, + direct_edges: &HashMap>, + params: &Params +) -> HashMap { + let mut result = HashMap::new(); + + // Precompute direct (r, s) + let mut direct_rs = HashMap::new(); + for (key, e) in direct_edges { + direct_rs.insert(key.clone(), rs_from_vec(e, ¶ms.w_pos, ¶ms.w_neg)); + } + + // Precompute witness lists per i by expectation (descending) + let mut witness_by_i: HashMap> = HashMap::new(); + for i in nodes { + let mut lst = Vec::new(); + for k in nodes { + if i == k { continue; } + if let Some(ok) = opinions.get(&(i.clone(), k.clone())) { + lst.push((ok.expectation(), k.clone())); + } + } + lst.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap()); + if lst.len() > params.witness_top_k { + lst.truncate(params.witness_top_k); + } + witness_by_i.insert(i.clone(), lst); + } + + for i in nodes { + for j in nodes { + if i == j { continue; } + let (r0, s0) = direct_rs.get(&(i.clone(), j.clone())).cloned().unwrap_or((0.0, 0.0)); + let mut rind = 0.0; + let mut sind = 0.0; + + if let Some(witnesses) = witness_by_i.get(i) { + for (delta_ik, k) in witnesses { + if let Some(&(rk, sk)) = direct_rs.get(&(k.clone(), j.clone())) { + if rk == 0.0 && sk == 0.0 { continue; } + let w = params.damping_lambda * delta_ik; + rind += w * rk; + sind += w * sk; + } + } + } + result.insert((i.clone(), j.clone()), (r0 + rind, s0 + sind)); + } + } + + result +} diff --git a/rust/src/opinion.rs b/rust/src/opinion.rs new file mode 100644 index 0000000..aa701bd --- /dev/null +++ b/rust/src/opinion.rs @@ -0,0 +1,77 @@ +use serde::{Deserialize, Serialize}; + +/// Represents a Subjective Logic opinion (b, d, u, a). +/// +/// b: Belief - Evidence supporting the proposition +/// d: Disbelief - Evidence against the proposition +/// u: Uncertainty - Lack of evidence +/// a: Base rate - Prior expectation in the absence of evidence +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct Opinion { + pub b: f64, + pub d: f64, + pub u: f64, + pub a: f64, +} + +impl Opinion { + /// Creates a new Opinion and ensures normalization (b + d + u = 1). + pub fn new(b: f64, d: f64, u: f64, a: f64) -> Self { + let total = b + d + u; + if total == 0.0 { + // Default to full uncertainty if no evidence and no prior + return Self { b: 0.0, d: 0.0, u: 1.0, a }; + } + if (total - 1.0).abs() > 1e-9 { + Self { + b: b / total, + d: d / total, + u: u / total, + a, + } + } else { + Self { b, d, u, a } + } + } + + /// Calculates the expected probability: E(ω) = b + a * u + pub fn expectation(&self) -> f64 { + self.b + self.a * self.u + } + + /// Fusion Operator (Consensus) + /// + /// Combines two independent opinions about the same proposition. + /// ω₁ ⊕ ω₂ = ((b₁u₂ + b₂u₁)/k, (d₁u₂ + d₂u₁)/k, (u₁u₂)/k, a) + /// where k = u₁ + u₂ − u₁u₂ + pub fn fuse(&self, other: &Self) -> Self { + let k = self.u + other.u - self.u * other.u; + if k == 0.0 { + // Both have zero uncertainty, return average as a simple conflict resolution + return Self::new( + (self.b + other.b) / 2.0, + (self.d + other.d) / 2.0, + 0.0, + self.a + ); + } + Self::new( + (self.b * other.u + other.b * self.u) / k, + (self.d * other.u + other.d * self.u) / k, + (self.u * other.u) / k, + self.a, + ) + } + + /// Discounting Operator (Transitivity) + /// + /// Propagates trust through a transitive relationship. + /// If A has opinion ω_ij about B, and B has opinion ω_jk about C, + /// then A's discounted opinion about C is ω_ik = ω_ij ⊗ ω_jk. + pub fn discount(&self, other: &Self) -> Self { + let b = self.b * other.b; + let d = self.b * other.d; + let u = self.d + self.u + self.b * other.u; + Self::new(b, d, u, other.a) + } +} diff --git a/rust/tests/bdd_tests.rs b/rust/tests/bdd_tests.rs new file mode 100644 index 0000000..116cc65 --- /dev/null +++ b/rust/tests/bdd_tests.rs @@ -0,0 +1,43 @@ +use eqbsl::*; + +#[test] +fn test_opinion_fusion_scenario() { + // Given two independent observers A and B have opinions about C + let r_ac = 10.0; + let s_ac = 0.0; + let op_ac = calculate_opinion(r_ac, s_ac, DEFAULT_K, 0.5); + + let r_bc = 5.0; + let s_bc = 0.0; + let op_bc = calculate_opinion(r_bc, s_bc, DEFAULT_K, 0.5); + + // When the opinions are fused + let fused = op_ac.fuse(&op_bc); + + // Then the resulting uncertainty should be lower than individual uncertainties + assert!(fused.u < op_ac.u); + assert!(fused.u < op_bc.u); + + // And the belief should be higher + assert!(fused.b > op_ac.b); + + println!("Fused belief: {}", fused.b); +} + +#[test] +fn test_transitive_trust_decay_scenario() { + // Given A trusts B with high certainty + let op_ab = calculate_opinion(20.0, 0.0, DEFAULT_K, 0.5); + + // And B trusts C with high certainty + let op_bc = calculate_opinion(20.0, 0.0, DEFAULT_K, 0.5); + + // When A derives an opinion about C via B (discounting) + let op_ac = op_ab.discount(&op_bc); + + // Then A's trust in C should be lower than B's trust in C (discounting effect) + assert!(op_ac.b < op_bc.b); + + // And A's uncertainty about C should be higher than B's uncertainty about C + assert!(op_ac.u > op_bc.u); +} diff --git a/rust/tests/integration_tests.rs b/rust/tests/integration_tests.rs new file mode 100644 index 0000000..447acd3 --- /dev/null +++ b/rust/tests/integration_tests.rs @@ -0,0 +1,72 @@ +use eqbsl::*; +use std::collections::HashMap; + +#[test] +fn test_full_pipeline() { + let params = Params { + k: 2.0, + w_pos: array![1.0], + w_neg: array![0.0], + decay_beta: array![0.5], + damping_lambda: 1.0, + witness_top_k: 10, + }; + + let mut state = State::new(0); + + // A -> B direct evidence + state.edges.insert(("A".to_string(), "B".to_string()), array![2.0]); + + // B -> C direct evidence + state.edges.insert(("B".to_string(), "C".to_string()), array![2.0]); + + // Compute direct opinions + let opinions = compute_opinions(&state, ¶ms, 0.5); + let op_ab = opinions.get(&("A".to_string(), "B".to_string())).unwrap(); + let op_bc = opinions.get(&("B".to_string(), "C".to_string())).unwrap(); + + assert_eq!(op_ab.b, 0.5); // 2/(2+2) + assert_eq!(op_bc.b, 0.5); + + // Test propagation + let nodes = vec!["A".to_string(), "B".to_string(), "C".to_string()]; + let prop_rs = depth1_propagation_rs(&nodes, &opinions, &state.edges, ¶ms); + + // A -> C should have indirect evidence from B + // delta_ab = E(op_ab) = 0.5 + 0.5*0.5 = 0.75 + // r_ac_indirect = lambda * delta_ab * r_bc = 1.0 * 0.75 * 2.0 = 1.5 + // r_ac_total = 0.0 + 1.5 = 1.5 + let (r_ac, s_ac) = prop_rs.get(&("A".to_string(), "C".to_string())).unwrap(); + assert_eq!(*r_ac, 1.5); + assert_eq!(*s_ac, 0.0); + + // Test decay + decay_state(&mut state, ¶ms, 1); + assert_eq!(state.edges.get(&("A".to_string(), "B".to_string())).unwrap()[0], 1.0); // 2.0 * 0.5 +} + +#[test] +fn test_hyperedge_attribution() { + let mut state = State::new(0); + let mut roles = HashMap::new(); + roles.insert("A".to_string(), "worker".to_string()); + roles.insert("B".to_string(), "worker".to_string()); + roles.insert("C".to_string(), "manager".to_string()); + + let h = Hyperedge { + hid: "H1".to_string(), + nodes: vec!["A".to_string(), "B".to_string(), "C".to_string()], + roles, + e: array![6.0], + }; + + state.hypers.insert("H1".to_string(), h); + + attribute_hyperedges_to_pairs(&mut state); + + // Pairs: (A,B), (A,C), (B,A), (B,C), (C,A), (C,B) -> 6 pairs + // alpha = 1 / (3*2) = 1/6 + // Each pair should get 6.0 * 1/6 = 1.0 evidence + assert_eq!(state.edges.get(&("A".to_string(), "B".to_string())).unwrap()[0], 1.0); + assert_eq!(state.edges.get(&("C".to_string(), "A".to_string())).unwrap()[0], 1.0); +} diff --git a/src/app.component.ts b/src/app.component.ts index e0b5418..7061ea8 100644 --- a/src/app.component.ts +++ b/src/app.component.ts @@ -5,6 +5,7 @@ import { EbslPlaygroundComponent } from './components/ebsl-playground.component' import { EqbslGraphComponent } from './components/eqbsl-graph.component'; import { ZkDemoComponent } from './components/zk-demo.component'; import { CathexisComponent } from './components/cathexis.component'; +import { AirdropExampleComponent } from './components/airdrop-example.component'; import { PapersComponent } from './components/papers.component'; import { PaperDetailComponent } from './components/paper-detail.component'; @@ -18,6 +19,7 @@ import { PaperDetailComponent } from './components/paper-detail.component'; EqbslGraphComponent, ZkDemoComponent, CathexisComponent, + AirdropExampleComponent, PapersComponent, PaperDetailComponent ], @@ -37,6 +39,7 @@ import { PaperDetailComponent } from './components/paper-detail.component'; @case ('eqbsl') { } @case ('zk') { } @case ('cathexis') { } + @case ('airdrop') { } @case ('papers') { } @case ('paper-detail') { +
+
+ Applied Example +
+

Reputation-Gated Airdrops

+

+ This example adapts the flow from the Shadowgraph Reputation-Gated Airdrop project into the EQBSL Explorer. + It shows how evidence becomes an opinion, how that opinion becomes a scaled reputation score, and how that score gates an airdrop claim. +

+ + View reference implementation + + + + + +
+ +
+
+
Expectation
+
{{ expectation().toFixed(3) }}
+
Expected trust probability from the current opinion.
+
+
+
Scaled score
+
{{ scaledScore() | number:'1.0-0' }}
+
Mapped into the airdrop’s 0..1,000,000 score range.
+
+
+
Eligibility
+
+ {{ eligible() ? 'Pass' : 'Fail' }} +
+
Threshold is {{ floorScore | number:'1.0-0' }} for this campaign.
+
+
+
Quoted payout
+
{{ payoutTokens().toFixed(2) }}
+
Current payout result using the selected claim path and curve.
+
+
+ +
+
+
+

1. Claimant evidence

+

+ Think of this as a simple reputation record. Positive evidence means "things this wallet did well." Negative evidence means "things that count against trust." +

+
+ +
+
+
Good history
+
{{ r() }}
+
Derived trust-supporting evidence total
+
+
+
Bad history
+
{{ s() }}
+
Derived trust-reducing evidence total
+
+
+ +
+
+ + {{ positiveAttestations() }} +
+ +

How many credible attestations or endorsements the claimant received from other parties.

+
+ +
+
+ + {{ successfulActions() }} +
+ +

Completed deliveries, valid prior claims, or other successful on-chain / off-chain outcomes.

+
+ +
+
+ + {{ negativeReports() }} +
+ +

Complaints, bad attestations, disputes, or moderation-relevant negative outcomes.

+
+ +
+
+ + {{ fraudFlags() }} +
+ +

Heavier-weight negative signals such as sybil detection, fraud markers, or severe policy violations.

+
+ +
+
+ +
+ + +
+

Switch between signed claims and proof-based claims.

+
+ +
+ + +

Controls how quickly higher reputation reaches maximum payout.

+
+
+ +
+
+ + {{ reputationAgeDays() }} +
+ +

The reference ZK path uses a freshness window; the example defaults to 7 days.

+
+
+ +
+
+

2. Derived reputation + claim outcome

+

+ Parameters below mirror the reference project: score scale 0..1,000,000, floor 600,000, cap 1,000,000, payout range 100..1000 tokens. +

+
+ +
+
+
Opinion
+
+
b{{ opinion().b.toFixed(3) }}
+
d{{ opinion().d.toFixed(3) }}
+
u{{ opinion().u.toFixed(3) }}
+
E(ω){{ expectation().toFixed(3) }}
+
+
+ +
+
Scaled score
+
{{ scaledScore() | number:'1.0-0' }}
+
Eligibility threshold: {{ floorScore | number:'1.0-0' }}
+
Cap for max payout: {{ capScore | number:'1.0-0' }}
+
+
+ +
+
+
+

Score ramp

+

Where the current score sits relative to floor and cap.

+
+
+ Below floor + Eligible ramp + Max payout zone +
+
+ +
+
+
+
+
+
+
+
+
+ {{ scaledScore() | number:'1.0-0' }} +
+
Floor {{ floorScore | number:'1.0-0' }}
+
Cap {{ capScore | number:'1.0-0' }}
+
+
+
+ +
+
+
Eligibility
+
+ {{ eligible() ? 'Eligible to claim' : 'Below claim threshold' }} +
+

+ {{ eligibilityMessage() }} +

+
+ +
+
Freshness
+
+ {{ freshEnough() ? 'Reputation window valid' : 'Reputation is stale' }} +
+

+ {{ freshnessMessage() }} +

+
+
+ +
+
+

Quoted payout

+ + {{ claimPath() === 'zk' ? 'ZK path' : 'ECDSA path' }} + +
+
+
+
{{ payoutTokens().toFixed(2) }}
+
Tokens
+
+

+ {{ claimPathDescription() }} +

+
+
+ +
+
+

How the score is assembled

+

+ In a real system, these values would come from attestations, claim history, dispute data, fraud heuristics, and timestamps. This demo compresses that into a few believable source counts. +

+
+ +
+
+
Source inputs
+
+
Positive attestations{{ positiveAttestations() }}
+
Successful prior actions{{ successfulActions() }}
+
Negative reports{{ negativeReports() }}
+
Fraud / sybil flags{{ fraudFlags() }}
+
+
+ +
+
Aggregation rule
+
+
+
r (supporting evidence)
+
{{ positiveAttestations() }} + {{ successfulActions() }} = {{ r() }}
+
+
+
s (negative evidence)
+
{{ negativeReports() }} + 2 × {{ fraudFlags() }} = {{ s() }}
+
+
+
+
+ +
+
+
Opinion
+
({{ opinion().b.toFixed(2) }}, {{ opinion().d.toFixed(2) }}, {{ opinion().u.toFixed(2) }})
+
Belief, disbelief, uncertainty
+
+
+
Expectation
+
{{ expectation().toFixed(3) }}
+
Scalar trust estimate
+
+
+
Scaled airdrop score
+
{{ scaledScore() | number:'1.0-0' }}
+
Mapped into campaign thresholds
+
+
+
+
+
+ +
+
+
+
+

3. End-to-end flow

+

+ The airdrop pipeline is easier to read as a sequence: reputation input, trust computation, proof or signature preparation, eligibility checks, then payout. +

+
+
+ +
+ @for (step of steps; track step.title) { +
+
+
+
+ {{ step.step.replace('Step ', '') }} +
+ @if (step.step !== 'Step 5') { +
+ } +
+ +
+
{{ step.step }}
+
{{ step.title }}
+

{{ step.body }}

+
+
+
+ } +
+
+ +
+

Reference parameters

+
+
Score scale0 to 1,000,000
+
Floor score600,000
+
Cap score1,000,000
+
Min payout100 tokens
+
Max payout1000 tokens
+
Default curveSQRT
+
ZK freshness window7 days
+
+ +
+ The explorer example stays focused on the trust and eligibility logic. The full reference project adds the SvelteKit frontend, + proof worker pipeline, on-chain verifier, and claim contracts. +
+
+
+ + ` +}) +export class AirdropExampleComponent { + private readonly ebsl = inject(EbslService); + + readonly floorScore = 600_000; + readonly capScore = 1_000_000; + readonly minPayout = 100; + readonly maxPayout = 1000; + readonly maxReputationAgeDays = 7; + readonly floorPercent = (this.floorScore / 1_000_000) * 100; + readonly capPercent = (this.capScore / 1_000_000) * 100; + readonly activeButtonClass = 'rounded-lg border border-indigo-500/40 bg-indigo-500/15 text-indigo-300 px-3 py-2 text-sm font-medium transition-colors'; + readonly inactiveButtonClass = 'rounded-lg border border-slate-700 bg-slate-900 text-slate-400 px-3 py-2 text-sm font-medium hover:text-white hover:bg-slate-800 transition-colors'; + + readonly positiveAttestations = signal(4); + readonly successfulActions = signal(4); + readonly negativeReports = signal(1); + readonly fraudFlags = signal(0); + readonly claimPath = signal('zk'); + readonly curve = signal('SQRT'); + readonly reputationAgeDays = signal(3); + + readonly r = computed(() => this.positiveAttestations() + this.successfulActions()); + readonly s = computed(() => this.negativeReports() + (2 * this.fraudFlags())); + + readonly opinion = computed(() => this.ebsl.calculateOpinion(this.r(), this.s(), 0.5)); + readonly expectation = computed(() => this.ebsl.expectedProbability(this.opinion())); + readonly scaledScore = computed(() => Math.round(this.expectation() * 1_000_000)); + readonly scorePercent = computed(() => Math.max(0, Math.min(100, this.scaledScore() / 10_000))); + readonly eligible = computed(() => this.scaledScore() >= this.floorScore); + readonly freshEnough = computed(() => this.reputationAgeDays() <= this.maxReputationAgeDays); + readonly payoutTokens = computed(() => { + if (!this.eligible()) { + return 0; + } + + const score = this.scaledScore(); + const normalized = Math.max(0, Math.min(1, (score - this.floorScore) / (this.capScore - this.floorScore))); + + let curveValue = normalized; + switch (this.curve()) { + case 'SQRT': + curveValue = Math.sqrt(normalized); + break; + case 'QUADRATIC': + curveValue = normalized * normalized; + break; + default: + curveValue = normalized; + } + + return this.minPayout + (this.maxPayout - this.minPayout) * curveValue; + }); + readonly claimPathDescription = computed(() => this.claimPath() === 'zk' + ? 'The claimant first proves reputation to the verifier contract, then claims through the ZK airdrop path if the score is fresh enough.' + : 'The claimant submits a signed artifact authorizing a payout derived from the same reputation score and payout curve.' + ); + readonly eligibilityMessage = computed(() => this.eligible() + ? `Score ${this.scaledScore().toLocaleString()} clears the campaign floor of ${this.floorScore.toLocaleString()}.` + : `Score ${this.scaledScore().toLocaleString()} is below the campaign floor of ${this.floorScore.toLocaleString()}.` + ); + readonly freshnessMessage = computed(() => this.claimPath() === 'zk' + ? (this.freshEnough() + ? `Age ${this.reputationAgeDays()}d is within the ${this.maxReputationAgeDays}-day freshness window.` + : `Age ${this.reputationAgeDays()}d exceeds the ${this.maxReputationAgeDays}-day freshness window used by the ZK claim path.`) + : 'The ECDSA path does not require the same freshness check in this simplified example.' + ); + + readonly steps = [ + { + step: 'Step 1', + title: 'Load attestations', + body: 'Trust evidence is gathered from the network and organized into the claimant’s local reputation inputs.' + }, + { + step: 'Step 2', + title: 'Compute EBSL opinion', + body: 'Evidence is fused into an opinion tuple and converted into an expectation score for gating decisions.' + }, + { + step: 'Step 3', + title: 'Prepare proof or signature', + body: 'The ECDSA path signs an authorization artifact; the ZK path prepares a privacy-preserving proof of the reputation result.' + }, + { + step: 'Step 4', + title: 'Verify eligibility', + body: 'The campaign checks threshold, payout curve, and, for the ZK path, whether the verified reputation is still fresh.' + }, + { + step: 'Step 5', + title: 'Claim payout', + body: 'Eligible users receive a payout between the configured minimum and maximum based on the scaled reputation score.' + } + ]; + + toNumber(event: Event): number { + return Number((event.target as HTMLInputElement).value); + } + + toCurve(event: Event): PayoutCurve { + return (event.target as HTMLSelectElement).value as PayoutCurve; + } +} diff --git a/src/components/intro.component.ts b/src/components/intro.component.ts index da3d8d1..3da2ecf 100644 --- a/src/components/intro.component.ts +++ b/src/components/intro.component.ts @@ -33,11 +33,11 @@ import { Component, ChangeDetectionStrategy, output } from '@angular/core';

- This video covers the fundamental limitations of traditional trust scores, EBSL's uncertainty modeling, zero-knowledge proofs, quantum-resistant extensions, and real-world applications. + This video covers the limitations of traditional trust scores, EBSL uncertainty modeling, proof-carrying trust, and applied reputation use cases.

-
+

1. EBSL Logic

@@ -81,6 +81,17 @@ import { Component, ChangeDetectionStrategy, output } from '@angular/core'; Generate Handles →
+ +
+
+

5. Reputation-Gated Airdrops

+

+ Applied use case. See how EBSL/EQBSL reputation becomes an eligibility score, a payout curve, and either an ECDSA or ZK claim path. +

+
+ Explore Airdrop Example → +
+
diff --git a/src/components/nav.component.ts b/src/components/nav.component.ts index 682d27d..f98669e 100644 --- a/src/components/nav.component.ts +++ b/src/components/nav.component.ts @@ -64,6 +64,7 @@ import { CommonModule } from '@angular/common'; .menu-item:nth-child(4) { animation-delay: 0.2s; } .menu-item:nth-child(5) { animation-delay: 0.25s; } .menu-item:nth-child(6) { animation-delay: 0.3s; } + .menu-item:nth-child(7) { animation-delay: 0.35s; } `], template: `
diff --git a/src/components/paper-detail.component.ts b/src/components/paper-detail.component.ts index 9ba8d31..dc080d8 100644 --- a/src/components/paper-detail.component.ts +++ b/src/components/paper-detail.component.ts @@ -307,8 +307,8 @@ export class PaperDetailComponent { }, ] }, - 'eqbsl-quantum': { - id: 'eqbsl-quantum', + 'eqbsl-against-trust-scores': { + id: 'eqbsl-against-trust-scores', title: 'EQBSL: Against Trust Scores', authors: 'Oliver C. Hirst, Independent Researcher (December 2025)', abstract: 'Most "trust scores" are numerology with a user interface: confident decimals that conceal their own provenance. Evidence-Based Subjective Logic (EBSL) is a corrective, because it refuses to treat trust as a mystical scalar and instead anchors it in manipulable, auditable evidence. This paper presents EQBSL, a systems-oriented extension that lifts evidence-based trust into a structured, vectorised, operator-defined form suitable for dynamic graphs and hypergraphs, and for downstream machine learning via stable trust embeddings.', diff --git a/src/components/papers.component.ts b/src/components/papers.component.ts index 55b14d4..1362b0b 100644 --- a/src/components/papers.component.ts +++ b/src/components/papers.component.ts @@ -59,8 +59,8 @@ import { CommonModule } from '@angular/common';

Additional Resources

- These papers represent ongoing research into EQBSL (Extended Quantum Belief State Logic), - zero-knowledge proofs, and trust-based systems. For the latest updates and source code, + These papers represent ongoing research into EQBSL, proof-carrying trust, + and evidence-based trust systems. For the latest updates and source code, visit our GitHub repository.