/** * Uncertainty as a First-Class State * * Probabilistic belief tracking with confidence intervals, evidence counts, * and opposing evidence pointers. Uncertainty is preserved, not eliminated. * * Every piece of knowledge in the system carries explicit uncertainty metadata. * Claims can be partial, unresolved, or contested. Confidence propagates * through inference chains and decays over time. * * UncertaintyLedger: * - Asserts beliefs with explicit confidence intervals and evidence * - Recomputes confidence from weighted supporting/opposing evidence * - Propagates uncertainty through inference chains (child bounded by parent) * - Applies time-based decay to all beliefs * - Queries by namespace, status, confidence, and tags * - Traces full inference chains back to root beliefs * * UncertaintyAggregator: * - Computes aggregate confidence across multiple beliefs (geometric mean) * - Worst-case and best-case confidence queries * - Contested and confirmed status checks across belief sets * * @module @claude-flow/guidance/uncertainty */ import { randomUUID } from 'node:crypto'; // ============================================================================ // Default Configuration // ============================================================================ const DEFAULT_UNCERTAINTY_CONFIG = { defaultConfidence: 0.7, decayRatePerHour: 0.01, contestedThreshold: 0.3, refutedThreshold: 0.7, minConfidenceForAction: 0.3, }; const SERIALIZATION_VERSION = 1; // ============================================================================ // UncertaintyLedger // ============================================================================ /** * A ledger that tracks beliefs with explicit uncertainty metadata. * * Every belief carries a confidence interval, supporting and opposing evidence, * inference chain links, and time-based decay. The ledger recomputes confidence * from evidence weights and propagates uncertainty through inference chains. */ export class UncertaintyLedger { config; beliefs = new Map(); constructor(config = {}) { this.config = { ...DEFAULT_UNCERTAINTY_CONFIG, ...config }; } /** * Assert a new belief in the ledger. * * Creates a belief with the given claim, namespace, and initial evidence. * If no confidence is provided, the default confidence is used to build * the initial confidence interval. * * @param claim - The claim this belief represents * @param namespace - Namespace for grouping * @param evidence - Initial evidence pointers * @param confidence - Optional explicit confidence interval * @returns The newly created Belief */ assert(claim, namespace, evidence, confidence) { const now = Date.now(); const point = confidence?.point ?? this.config.defaultConfidence; const lower = confidence?.lower ?? clamp(point - 0.1, 0, 1); const upper = confidence?.upper ?? clamp(point + 0.1, 0, 1); const supporting = evidence.filter(e => e.supports); const opposing = evidence.filter(e => !e.supports); const belief = { id: randomUUID(), claim, namespace, confidence: { lower: clamp(lower, 0, 1), point: clamp(point, 0, 1), upper: clamp(upper, 0, 1), }, status: 'unknown', evidence: supporting, opposingEvidence: opposing, inferredFrom: [], firstAsserted: now, lastUpdated: now, decayRate: this.config.decayRatePerHour, tags: [], }; // Compute initial status from evidence belief.status = this.deriveStatus(belief); this.beliefs.set(belief.id, belief); return belief; } /** * Add a piece of evidence to an existing belief. * * Appends the evidence to the appropriate list (supporting or opposing), * then recomputes the belief's confidence and status. * * @param beliefId - The belief to update * @param evidence - The new evidence pointer * @returns The updated belief, or undefined if not found */ addEvidence(beliefId, evidence) { const belief = this.beliefs.get(beliefId); if (!belief) return undefined; if (evidence.supports) { belief.evidence.push(evidence); } else { belief.opposingEvidence.push(evidence); } this.recomputeConfidence(belief); belief.status = this.deriveStatus(belief); belief.lastUpdated = Date.now(); return belief; } /** * Retrieve a belief by its ID. * * @param id - The belief ID * @returns The belief, or undefined if not found */ getBelief(id) { return this.beliefs.get(id); } /** * Query beliefs with optional filters. * * All specified filters are ANDed together. Returns beliefs ordered * by lastUpdated descending. * * @param opts - Filter criteria * @returns Matching beliefs */ query(opts = {}) { const results = []; for (const belief of this.beliefs.values()) { if (opts.namespace !== undefined && belief.namespace !== opts.namespace) { continue; } if (opts.status !== undefined && belief.status !== opts.status) { continue; } if (opts.minConfidence !== undefined && belief.confidence.point < opts.minConfidence) { continue; } if (opts.tags !== undefined && opts.tags.length > 0) { const beliefTags = new Set(belief.tags); if (!opts.tags.every(t => beliefTags.has(t))) { continue; } } results.push(belief); } return results.sort((a, b) => b.lastUpdated - a.lastUpdated); } /** * Get all beliefs with status 'contested'. * * @returns Array of contested beliefs */ getContested() { return this.query({ status: 'contested' }); } /** * Get all beliefs with status 'uncertain' or 'contested'. * * @returns Array of unresolved beliefs */ getUnresolved() { const results = []; for (const belief of this.beliefs.values()) { if (belief.status === 'uncertain' || belief.status === 'contested') { results.push(belief); } } return results.sort((a, b) => b.lastUpdated - a.lastUpdated); } /** * Recompute the confidence interval for a belief from all evidence. * * The point estimate is a weighted average: total supporting weight minus * total opposing weight, normalized to [0, 1]. The interval bounds are * derived from the spread of evidence weights. * * @param beliefId - The belief to recompute * @returns The updated confidence interval, or undefined if not found */ computeConfidence(beliefId) { const belief = this.beliefs.get(beliefId); if (!belief) return undefined; this.recomputeConfidence(belief); belief.status = this.deriveStatus(belief); belief.lastUpdated = Date.now(); return { ...belief.confidence }; } /** * Propagate uncertainty from a parent belief to a child belief. * * The child's confidence is bounded by the parent's confidence multiplied * by the inference weight. This ensures that downstream beliefs cannot * be more confident than their sources warrant. * * @param parentId - The parent belief ID * @param childId - The child belief ID * @param inferenceWeight - How strongly the parent supports the child (0.0 - 1.0) * @returns The updated child belief, or undefined if either belief is not found */ propagateUncertainty(parentId, childId, inferenceWeight) { const parent = this.beliefs.get(parentId); const child = this.beliefs.get(childId); if (!parent || !child) return undefined; const weight = clamp(inferenceWeight, 0, 1); // Record the inference relationship if (!child.inferredFrom.includes(parentId)) { child.inferredFrom.push(parentId); } // Bound child confidence by parent * weight const maxPoint = parent.confidence.point * weight; const maxUpper = parent.confidence.upper * weight; const maxLower = parent.confidence.lower * weight; child.confidence.point = Math.min(child.confidence.point, maxPoint); child.confidence.upper = Math.min(child.confidence.upper, maxUpper); child.confidence.lower = Math.min(child.confidence.lower, maxLower); // Ensure ordering invariant: lower <= point <= upper child.confidence.lower = Math.min(child.confidence.lower, child.confidence.point); child.confidence.upper = Math.max(child.confidence.upper, child.confidence.point); child.status = this.deriveStatus(child); child.lastUpdated = Date.now(); return child; } /** * Apply time-based decay to all beliefs. * * Each belief's confidence.point is reduced by its decayRate for every * hour elapsed since lastUpdated. The lower and upper bounds shrink * proportionally. Status is updated if confidence drops below thresholds. * * @param currentTime - The reference time for computing elapsed decay (defaults to now) */ decayAll(currentTime) { const now = currentTime ?? Date.now(); for (const belief of this.beliefs.values()) { const elapsedMs = now - belief.lastUpdated; if (elapsedMs <= 0) continue; const elapsedHours = elapsedMs / 3_600_000; const decay = belief.decayRate * elapsedHours; if (decay <= 0) continue; // Apply decay to point estimate belief.confidence.point = clamp(belief.confidence.point - decay, 0, 1); // Shrink bounds proportionally belief.confidence.lower = clamp(belief.confidence.lower - decay, 0, belief.confidence.point); belief.confidence.upper = clamp(belief.confidence.upper - decay * 0.5, belief.confidence.point, 1); belief.status = this.deriveStatus(belief); belief.lastUpdated = now; } } /** * Manually resolve a belief to a definitive status. * * This overrides the computed status. Typically used for 'confirmed' or * 'refuted' after human review or authoritative evidence. * * @param beliefId - The belief to resolve * @param status - The new status to assign * @param reason - Human-readable reason for the resolution * @returns The updated belief, or undefined if not found */ resolve(beliefId, status, reason) { const belief = this.beliefs.get(beliefId); if (!belief) return undefined; belief.status = status; belief.lastUpdated = Date.now(); // When confirming, set confidence to high; when refuting, set to low if (status === 'confirmed') { belief.confidence.point = clamp(Math.max(belief.confidence.point, 0.95), 0, 1); belief.confidence.upper = 1.0; belief.confidence.lower = clamp(Math.max(belief.confidence.lower, 0.9), 0, 1); } else if (status === 'refuted') { belief.confidence.point = clamp(Math.min(belief.confidence.point, 0.05), 0, 1); belief.confidence.lower = 0.0; belief.confidence.upper = clamp(Math.min(belief.confidence.upper, 0.1), 0, 1); } // Record the resolution as evidence const resolutionEvidence = { sourceId: `resolution:${beliefId}:${Date.now()}`, sourceType: 'human-input', supports: status === 'confirmed', weight: 1.0, timestamp: Date.now(), }; if (resolutionEvidence.supports) { belief.evidence.push(resolutionEvidence); } else { belief.opposingEvidence.push(resolutionEvidence); } // Store reason in the evidence sourceId for traceability // (reason is captured via the resolution evidence pattern) return belief; } /** * Check whether a belief's confidence meets the minimum threshold for action. * * @param beliefId - The belief to check * @returns true if confidence.point >= minConfidenceForAction, false otherwise */ isActionable(beliefId) { const belief = this.beliefs.get(beliefId); if (!belief) return false; return belief.confidence.point >= this.config.minConfidenceForAction; } /** * Trace the full inference chain from a belief back to its root beliefs. * * Returns an array of { belief, depth } nodes, starting with the queried * belief at depth 0, then its parents at depth 1, their parents at depth 2, * and so on. Handles cycles by tracking visited IDs. * * @param beliefId - The belief whose chain to trace * @returns Array of chain nodes ordered by depth, or empty if not found */ getConfidenceChain(beliefId) { const root = this.beliefs.get(beliefId); if (!root) return []; const result = []; const visited = new Set(); const traverse = (id, depth) => { if (visited.has(id)) return; visited.add(id); const belief = this.beliefs.get(id); if (!belief) return; result.push({ belief, depth }); for (const parentId of belief.inferredFrom) { traverse(parentId, depth + 1); } }; traverse(beliefId, 0); return result.sort((a, b) => a.depth - b.depth); } /** * Export all beliefs for persistence. * * @returns Serialized ledger data suitable for JSON.stringify */ exportBeliefs() { return { beliefs: Array.from(this.beliefs.values()).map(b => ({ ...b })), createdAt: new Date().toISOString(), version: SERIALIZATION_VERSION, }; } /** * Import previously exported beliefs, replacing all current contents. * * @param data - Serialized ledger data * @throws If the version is unsupported */ importBeliefs(data) { if (data.version !== SERIALIZATION_VERSION) { throw new Error(`Unsupported uncertainty ledger version: ${data.version} (expected ${SERIALIZATION_VERSION})`); } this.beliefs.clear(); for (const belief of data.beliefs) { this.beliefs.set(belief.id, { ...belief }); } } /** * Get the number of tracked beliefs. */ get size() { return this.beliefs.size; } /** * Get the current configuration. */ getConfig() { return { ...this.config }; } /** * Remove all beliefs from the ledger. */ clear() { this.beliefs.clear(); } // ===== Private ===== /** * Recompute the confidence interval for a belief from its evidence arrays. * * Point estimate is derived from the balance of supporting vs opposing * evidence weights. Bounds reflect the spread of evidence. */ recomputeConfidence(belief) { const allEvidence = [...belief.evidence, ...belief.opposingEvidence]; if (allEvidence.length === 0) { // No evidence: keep current confidence (from assertion) return; } let supportingWeight = 0; let opposingWeight = 0; for (const e of belief.evidence) { supportingWeight += e.weight; } for (const e of belief.opposingEvidence) { opposingWeight += e.weight; } const totalWeight = supportingWeight + opposingWeight; if (totalWeight === 0) { // All evidence has zero weight: no update return; } // Point estimate: proportion of supporting weight const point = supportingWeight / totalWeight; // Compute spread from evidence count (more evidence = tighter interval) const evidenceCount = allEvidence.length; const spread = Math.max(0.02, 0.3 / Math.sqrt(evidenceCount)); belief.confidence = { lower: clamp(point - spread, 0, 1), point: clamp(point, 0, 1), upper: clamp(point + spread, 0, 1), }; } /** * Derive the belief status from its current confidence and evidence ratios. */ deriveStatus(belief) { const allEvidence = [...belief.evidence, ...belief.opposingEvidence]; if (allEvidence.length === 0) { return 'unknown'; } // If already manually resolved to confirmed or refuted, preserve it if (belief.status === 'confirmed' || belief.status === 'refuted') { // Only preserve if there's resolution evidence (weight 1.0 human-input) const hasResolution = allEvidence.some(e => e.sourceType === 'human-input' && e.weight === 1.0); if (hasResolution) return belief.status; } let supportingWeight = 0; let opposingWeight = 0; for (const e of belief.evidence) { supportingWeight += e.weight; } for (const e of belief.opposingEvidence) { opposingWeight += e.weight; } const totalWeight = supportingWeight + opposingWeight; if (totalWeight === 0) { return 'unknown'; } const opposingRatio = opposingWeight / totalWeight; // Check thresholds from most severe to least if (opposingRatio >= this.config.refutedThreshold) { return 'refuted'; } if (opposingRatio >= this.config.contestedThreshold) { return 'contested'; } // Not contested; determine from confidence level if (belief.confidence.point >= 0.8) { return 'probable'; } if (belief.confidence.point >= 0.5) { return 'uncertain'; } return 'uncertain'; } } // ============================================================================ // UncertaintyAggregator // ============================================================================ /** * Computes aggregate confidence metrics across multiple beliefs. * * Provides geometric mean, worst-case, best-case, and status checks * over sets of beliefs referenced by ID. */ export class UncertaintyAggregator { ledger; constructor(ledger) { this.ledger = ledger; } /** * Compute the aggregate confidence across multiple beliefs using * the geometric mean of their point estimates. * * The geometric mean penalizes any single low-confidence belief more * heavily than an arithmetic mean, making it appropriate for combining * independent confidence estimates. * * @param beliefIds - IDs of beliefs to aggregate * @returns Geometric mean of confidence points, or 0 if no valid beliefs */ aggregate(beliefIds) { const confidences = this.collectConfidences(beliefIds); if (confidences.length === 0) return 0; // Geometric mean via log-space to avoid underflow const logSum = confidences.reduce((sum, c) => { // Protect against log(0) const safe = Math.max(c, 1e-10); return sum + Math.log(safe); }, 0); return Math.exp(logSum / confidences.length); } /** * Return the lowest confidence point among the specified beliefs. * * @param beliefIds - IDs of beliefs to check * @returns The minimum confidence point, or 0 if no valid beliefs */ worstCase(beliefIds) { const confidences = this.collectConfidences(beliefIds); if (confidences.length === 0) return 0; return Math.min(...confidences); } /** * Return the highest confidence point among the specified beliefs. * * @param beliefIds - IDs of beliefs to check * @returns The maximum confidence point, or 0 if no valid beliefs */ bestCase(beliefIds) { const confidences = this.collectConfidences(beliefIds); if (confidences.length === 0) return 0; return Math.max(...confidences); } /** * Check if any of the specified beliefs is contested. * * @param beliefIds - IDs of beliefs to check * @returns true if at least one belief has status 'contested' */ anyContested(beliefIds) { for (const id of beliefIds) { const belief = this.ledger.getBelief(id); if (belief && belief.status === 'contested') return true; } return false; } /** * Check if all of the specified beliefs are confirmed. * * @param beliefIds - IDs of beliefs to check * @returns true only if every belief exists and has status 'confirmed' */ allConfirmed(beliefIds) { if (beliefIds.length === 0) return false; for (const id of beliefIds) { const belief = this.ledger.getBelief(id); if (!belief || belief.status !== 'confirmed') return false; } return true; } // ===== Private ===== /** * Collect the confidence point estimates for all valid belief IDs. */ collectConfidences(beliefIds) { const confidences = []; for (const id of beliefIds) { const belief = this.ledger.getBelief(id); if (belief) { confidences.push(belief.confidence.point); } } return confidences; } } // ============================================================================ // Factory Functions // ============================================================================ /** * Create an UncertaintyLedger with optional configuration. * * @param config - Partial configuration; unspecified values use defaults * @returns A fresh UncertaintyLedger */ export function createUncertaintyLedger(config) { return new UncertaintyLedger(config); } /** * Create an UncertaintyAggregator backed by the given ledger. * * @param ledger - The UncertaintyLedger to aggregate over * @returns A fresh UncertaintyAggregator */ export function createUncertaintyAggregator(ledger) { return new UncertaintyAggregator(ledger); } // ============================================================================ // Helpers // ============================================================================ /** * Clamp a number to the range [min, max]. */ function clamp(value, min, max) { return Math.min(max, Math.max(min, value)); } //# sourceMappingURL=uncertainty.js.map