stemedb/ai-lookup/services/lens.md
jordan 1ce4004807 feat: Complete Phase 2 (The Cortex) - query, lens, and API layers
This commit adds the read path (Cortex) to complement the write path (Spine):

## Crates
- stemedb-api: HTTP API with axum + utoipa OpenAPI
  - /v1/assert, /v1/query, /v1/epoch, /v1/skeptic, /v1/trace, /v1/audit
  - Metered endpoints with quota enforcement
  - Ed25519 signature verification
- stemedb-lens: Truth resolution lenses
  - RecencyLens, ConsensusLens, ConfidenceLens
  - VoteAwareConsensusLens (Ballot Box pattern)
  - TrustAwareAuthorityLens (The Hive pattern)
  - SkepticLens (conflict analysis)
  - EpochAwareLens (paradigm-safe queries)
- stemedb-query: Query engine with materialized views

## Storage Extensions
- VoteStore: Vote aggregation with cached counts
- TrustRankStore: Agent reputation with decay
- AuditStore: Query audit trail
- IndexStore: SP/P/S index structures
- SupersessionStore: Epoch supersession chains

## SDKs
- sdk/go/steme: Go HTTP client with Ed25519 signing
- sdk/go/adk: ADK-Go tools for AI agents

## Documentation
- Updated CLAUDE.md, architecture.md, roadmap.md
- New ai-lookup entries for all services
- Use case docs for consumer health intelligence
- Arena roadmap for simulation advancement

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 13:22:44 -07:00

10 KiB

Lens

Last Updated: 2026-02-01 Confidence: High Status: Implemented in stemedb-lens v0.1.0

Summary

A Lens resolves conflicting assertions into a deterministic answer at read time. Multiple truths coexist; the Lens chooses which to return.

Key Facts:

  • Stateless compute (no side effects)
  • Deterministic (same input = same output)
  • Fast (runs on every read, avoid allocations)
  • Pluggable (implement Lens trait)

File Pointer: crates/stemedb-lens/src/lib.rs

The Traits

Synchronous Lens

pub trait Lens: Send + Sync {
    fn resolve(&self, candidates: &[Assertion]) -> Resolution;
    fn name(&self) -> &'static str;
}

Async Lens

For lenses requiring I/O (e.g., VoteStore lookups):

#[async_trait]
pub trait AsyncLens: Send + Sync {
    async fn resolve_async(&self, candidates: &[Assertion]) -> Resolution;
    fn name(&self) -> &'static str;
}

Analysis Lens (Trust but Verify)

For lenses that surface conflict instead of resolving it:

#[async_trait]
pub trait AnalysisLens: Send + Sync {
    async fn analyze(&self, candidates: &[Assertion]) -> ConflictAnalysis;
    fn name(&self) -> &'static str;
}

Returns ConflictAnalysis with:

  • status: Unanimous, Agreed, or Contested
  • conflict_score: 0.0 (unanimous) to 1.0 (chaos) using normalized Shannon entropy
  • claims: All distinct claims ranked by weight share

VoteAwareConsensus Implementation

The VoteAwareConsensusLens integrates with the Ballot Box pattern (VoteStore) to resolve based on actual vote counts.

Resolution Strategy:

  1. For each candidate assertion, lookup vote count and aggregate weight (O(1) cached)
  2. Rank by aggregate weight (sum of all vote weights)
  3. Return assertion with highest aggregate weight
  4. Tiebreaker: If weights equal, prefer most recent timestamp

Confidence Calculation:

confidence = winner_weight / total_weight_across_all_candidates

Example:

use stemedb_lens::VoteAwareConsensusLens;
use stemedb_storage::{SledStore, GenericVoteStore};
use std::sync::Arc;

let store = SledStore::open("./data").await?;
let vote_store = Arc::new(GenericVoteStore::new(store));
let lens = VoteAwareConsensusLens::new(vote_store);

let resolution = lens.resolve_async(&candidates).await;

TrustAwareAuthority Implementation

The TrustAwareAuthorityLens integrates with TrustRank to weight assertions by agent reputation. This is the foundation of "The Hive" learning loop.

Resolution Strategy:

  1. For each candidate assertion, lookup the primary signer's TrustRank (O(1) lookup)
  2. Calculate weighted score: assertion.confidence * agent.trust_rank
  3. Return assertion with highest weighted score
  4. Tiebreaker: If scores equal, prefer most recent timestamp
  5. New agents default to 0.5 trust score
  6. Unsigned assertions treated as 0.0 trust

Confidence Calculation:

weighted_score = assertion.confidence * agent.trust_rank
confidence = weighted_score  // Direct weighted score

TrustRank Learning Loop:

  • Agents start at 0.5 (neutral)
  • Accurate predictions: +0.05 per correct assertion
  • Inaccurate predictions: -0.1 per incorrect assertion (higher penalty discourages spam)
  • Confidence half-life: Scores decay over 30 days by default
  • Scores bounded to [0.0, 1.0]

Example:

use stemedb_lens::TrustAwareAuthorityLens;
use stemedb_storage::{SledStore, GenericTrustRankStore};
use std::sync::Arc;

let store = SledStore::open("./data").await?;
let trust_store = Arc::new(GenericTrustRankStore::new(store));
let lens = TrustAwareAuthorityLens::new(trust_store);

let resolution = lens.resolve_async(&candidates).await;

// Record outcome for learning
trust_store.record_outcome(&agent_id, was_accurate, timestamp).await?;

// Apply decay periodically
trust_store.decay_trust_ranks(current_timestamp, None).await?;

Standard Lenses

Lens Strategy Use Case Status
Recency Latest timestamp wins News, real-time Implemented
Consensus Most common object value Democratic truth (basic) Implemented
VoteAwareConsensus Highest vote weight from VoteStore Democratic truth (advanced) Implemented
Confidence Highest assertion confidence field Source-declared certainty Implemented
Authority Alias for TrustAwareAuthority Reputation-weighted (user-friendly name) Implemented
TrustAwareAuthority Weighted by TrustRank reputation Expert truth (The Hive) Implemented
Skeptic Returns all claims with conflict score "Trust but Verify" dashboards Implemented
EpochAware Filters superseded epochs first Paradigm-safe queries Implemented
Constraints Returns must_use/forbidden predicates Pre-flight checks 🔜 Planned

Note: The Authority lens is now an alias for TrustAwareAuthority (both use agent reputation via TrustRank). Use Confidence if you want to select by the assertion's self-declared confidence field without considering agent reputation.

EpochAwareLens Implementation

The EpochAwareLens filters assertions from superseded epochs before delegating to an inner lens. This enables "paradigm-safe" queries where obsolete worldviews are automatically excluded.

Resolution Strategy:

  1. Collect all unique epoch IDs from candidate assertions
  2. For each epoch, read E:{epoch_id} from store
  3. Walk the supersedes chain to build a set of superseded epoch IDs
  4. Filter candidates: exclude any assertion whose epoch is in the superseded set
  5. Delegate filtered candidates to inner lens (default: RecencyLens)

Key Design Decisions:

Behavior Choice Rationale
Missing epoch record Include assertion (fail-open) Data availability > metadata consistency
Cycle in supersession chain Stop walking, include assertions Pathological data shouldn't hide valid assertions
Max depth exceeded (100) Stop walking, log warning Prevent infinite loops
No epochs in candidates Delegate directly to inner lens Optimization for common case

Use Case: Accounting Standard Migration (GAAP → IFRS)

# Create epochs representing paradigm shift
POST /v1/epoch {"name": "GAAP-Era", "start_timestamp": 0}
# Returns epoch_id: "abc123..."

POST /v1/epoch {
  "name": "IFRS-Transition",
  "supersedes": "abc123...",
  "supersession_type": "Temporal",
  "start_timestamp": 1704067200
}
# Returns epoch_id: "def456..."

# Query with epoch awareness
GET /v1/query?subject=Acme&predicate=lease_liability&lens=EpochAware

# Returns IFRS treatment (new epoch)
# GAAP treatment (old epoch) automatically excluded

Example:

use stemedb_lens::EpochAwareLens;
use stemedb_storage::SledStore;
use std::sync::Arc;

let store = Arc::new(SledStore::open("./data").expect("store"));

// Default: filter superseded epochs, then pick most recent
let lens = EpochAwareLens::with_recency(store.clone());

// Custom: filter superseded epochs, then use consensus
use stemedb_lens::ConsensusLens;
let lens = EpochAwareLens::with_sync_lens(store, ConsensusLens);

let resolution = lens.resolve_async(&candidates).await;

Limitation: The lens only filters assertions when assertions from the superseding epoch are present in the candidates. If you only have old-epoch assertions (no new-epoch assertions exist for the query), they will pass through. This is intentional fail-open behavior.

SkepticLens Implementation

The SkepticLens surfaces conflict instead of hiding it. It implements AnalysisLens rather than Lens, returning a ConflictAnalysis with all competing claims.

Resolution Strategy:

  1. Group assertions by object value
  2. For each group, calculate aggregate vote weight (or fallback to confidence)
  3. Calculate normalized Shannon entropy as conflict score
  4. Determine status: Unanimous (<0.1), Agreed (<0.4), or Contested (>=0.4)
  5. Build ClaimSummary for each group with supporting agents and source provenance

Conflict Score Formula:

entropy = -sum(p * log2(p)) for each claim weight proportion
conflict_score = entropy / log2(num_claims)  // Normalized to 0.0-1.0

API Endpoint:

GET /v1/skeptic?subject=Semaglutide&predicate=muscle_effect

{
  "status": "Contested",
  "conflict_score": 0.72,
  "claims": [
    {
      "value": {"type": "Text", "value": "Significant loss"},
      "weight_share": 0.45,
      "assertion_count": 12,
      "supporting_agents": [...]
    },
    {
      "value": {"type": "Text", "value": "Minimal loss"},
      "weight_share": 0.35,
      "assertion_count": 3
    }
  ],
  "candidates_count": 17
}

Example:

use stemedb_lens::SkepticLens;
use stemedb_storage::{SledStore, GenericVoteStore, GenericTrustRankStore};
use std::sync::Arc;

let store = SledStore::open("./data").await?;
let vote_store = Arc::new(GenericVoteStore::new(store.clone()));
let trust_store = Arc::new(GenericTrustRankStore::new(store));
let lens = SkepticLens::new(vote_store, trust_store);

let analysis = lens.analyze(&candidates).await;
if analysis.status == ResolutionStatus::Contested {
    println!("⚠️ This fact is disputed! Conflict score: {:.2}", analysis.conflict_score);
}

Lens::Constraints (Pre-Flight Check)

Special lens for agent safety. Returns rules, not facts.

GET /query?context=python_http&lens=constraints

-> Returns:
{
  "constraints": [
    { "must_use": "axios", "forbidden": "requests", "reason": "User correction" }
  ]
}

Origin: Solves the "Optimization Conflict" where agents forget corrections. Acts as a compiler error for agent intent.

See agile-agent-team.md for full explanation.

Query Flow

  1. Client: GET(Subject="Tesla", Predicate="Revenue", Lens="Consensus")
  2. Index lookup: SP:Tesla:Revenue -> [Hash1, Hash2, Hash3]
  3. Hydrate: Load assertions from hashes
  4. Resolve: ConsensusLens.resolve(assertions, context)
  5. Return: Single deterministic answer with confidence