/** * Truth Anchor System * * Immutable, externally-signed facts that anchor the system to reality * beyond tool outputs and internal memory. * * A Truth Anchor is a fact that: * - Is immutable once recorded (cannot be overwritten or decayed) * - Is externally signed (by a human, hardware sensor, or external authority) * - Supersedes any internal belief that contradicts it * - Can be referenced but never mutated * - Has a verifiable signature chain * * TruthAnchorStore: * - Creates and signs new truth anchors with HMAC-SHA256 * - Append-only storage (anchors are never mutated after creation) * - Retrieval by ID, time range, kind, attester, or tags * - Signature verification for individual anchors or the full store * - Supersession chain: new anchors can declare they supersede old ones * - Export/import for persistence and transfer * - Max 50,000 anchors with LRU eviction of expired ones only * * TruthResolver: * - Resolves conflicts between internal beliefs and truth anchors * - Memory conflict resolution (truth anchor always wins) * - Decision conflict resolution (constrains proposed actions) * - Topic-based ground truth retrieval with fuzzy tag matching * * @module @claude-flow/guidance/truth-anchors */ import { createHmac, randomUUID } from 'node:crypto'; // ============================================================================ // Default Configuration // ============================================================================ const DEFAULT_CONFIG = { signingKey: '', maxAnchors: 50_000, }; // ============================================================================ // Signing Helpers // ============================================================================ /** * Compute the canonical string representation of an anchor for signing. * * Deterministic ordering ensures the same anchor always produces * the same signature regardless of property insertion order. */ function canonicalize(anchor) { return [ anchor.id, anchor.kind, anchor.claim, anchor.evidence, anchor.attesterId, String(anchor.timestamp), String(anchor.validFrom), String(anchor.validUntil ?? 'null'), anchor.supersedes.join(','), anchor.tags.join(','), JSON.stringify(anchor.metadata), ].join('|'); } /** * Produce an HMAC-SHA256 signature for the given data using the provided key. */ function sign(data, key) { return createHmac('sha256', key).update(data).digest('hex'); } // ============================================================================ // Truth Anchor Store // ============================================================================ /** * Append-only store for truth anchors. * * Anchors are immutable once created. The store provides signing, * verification, querying, supersession, and capacity management * with LRU eviction of expired anchors only. */ export class TruthAnchorStore { config; anchors = []; indexById = new Map(); constructor(config = {}) { this.config = { ...DEFAULT_CONFIG, ...config }; if (!this.config.signingKey) { throw new Error('TruthAnchorStore requires a signingKey in config. ' + 'Anchors cannot be created without a signing key.'); } } /** * Create and sign a new truth anchor. * * The anchor is appended to the store and can never be mutated. * If the store exceeds capacity, expired anchors are evicted * starting from the oldest. */ anchor(params) { const now = Date.now(); const partial = { id: randomUUID(), kind: params.kind, claim: params.claim, evidence: params.evidence, attesterId: params.attesterId, timestamp: now, validFrom: params.validFrom ?? now, validUntil: params.validUntil ?? null, supersedes: params.supersedes ?? [], tags: params.tags ?? [], metadata: params.metadata ?? {}, }; const signature = sign(canonicalize(partial), this.config.signingKey); const truthAnchor = { ...partial, signature, }; // Append (never mutate existing entries) this.anchors.push(truthAnchor); this.indexById.set(truthAnchor.id, this.anchors.length - 1); // Enforce capacity by evicting expired anchors this.enforceCapacity(now); return truthAnchor; } /** * Retrieve a truth anchor by its ID. * * Returns undefined if the anchor does not exist. */ get(id) { const index = this.indexById.get(id); if (index === undefined) return undefined; return this.anchors[index]; } /** * Get all anchors that are valid at the given timestamp. * * An anchor is active when: * - `validFrom <= timestamp` * - `validUntil` is null (indefinite) or `validUntil > timestamp` * * Defaults to the current time if no timestamp is provided. */ getActive(timestamp) { const ts = timestamp ?? Date.now(); return this.anchors.filter(a => isActive(a, ts)); } /** * Query anchors with optional filters. * * All provided filters are ANDed together. An anchor must match * every specified filter to be included in the result. */ query(opts) { return this.anchors.filter(a => { if (opts.kind !== undefined && a.kind !== opts.kind) return false; if (opts.attesterId !== undefined && a.attesterId !== opts.attesterId) return false; if (opts.tags !== undefined && opts.tags.length > 0) { const hasMatch = opts.tags.some(tag => a.tags.includes(tag)); if (!hasMatch) return false; } if (opts.validAt !== undefined && !isActive(a, opts.validAt)) return false; return true; }); } /** * Verify the HMAC-SHA256 signature of a single anchor. * * Recomputes the signature from the anchor's content and compares * it to the stored signature. Returns true if they match. */ verify(id) { const anchor = this.get(id); if (!anchor) return false; const { signature, ...rest } = anchor; const expected = sign(canonicalize(rest), this.config.signingKey); return timingSafeEqual(signature, expected); } /** * Verify all anchors in the store. * * Returns a summary with the count of valid anchors and the * IDs of any anchors whose signatures do not match. */ verifyAll() { let valid = 0; const invalid = []; for (const anchor of this.anchors) { const { signature, ...rest } = anchor; const expected = sign(canonicalize(rest), this.config.signingKey); if (timingSafeEqual(signature, expected)) { valid++; } else { invalid.push(anchor.id); } } return { valid, invalid }; } /** * Create a new anchor that supersedes an existing one. * * The old anchor remains in the store (immutable) but the new * anchor's `supersedes` array includes the old anchor's ID. * This creates a verifiable supersession chain. * * Throws if the old anchor ID does not exist. */ supersede(oldId, params) { const old = this.get(oldId); if (!old) { throw new Error(`Cannot supersede: anchor "${oldId}" not found`); } const supersedes = [...(params.supersedes ?? [])]; if (!supersedes.includes(oldId)) { supersedes.push(oldId); } return this.anchor({ ...params, supersedes, }); } /** * Resolve a claim against an internal belief. * * Searches for active truth anchors whose claim matches the * provided claim text. If a matching truth anchor exists and * is currently valid, it wins over the internal belief. * * Returns the truth anchor if it exists, otherwise returns * undefined (meaning the internal belief stands). */ resolve(claim, _internalBelief) { const now = Date.now(); const normalizedClaim = claim.toLowerCase().trim(); // Find active anchors whose claim matches for (const anchor of this.anchors) { if (!isActive(anchor, now)) continue; if (anchor.claim.toLowerCase().trim() === normalizedClaim) { return anchor; } } return undefined; } /** * Export all anchors for persistence or transfer. * * Returns a shallow copy of the anchor array. The individual * anchor objects are returned as-is since they are immutable. */ exportAnchors() { return [...this.anchors]; } /** * Import anchors from an external source. * * Imported anchors are appended to the store. Duplicate IDs * (anchors already in the store) are silently skipped. * Capacity enforcement runs after import. */ importAnchors(anchors) { const now = Date.now(); for (const anchor of anchors) { // Skip duplicates if (this.indexById.has(anchor.id)) continue; this.anchors.push(anchor); this.indexById.set(anchor.id, this.anchors.length - 1); } this.enforceCapacity(now); } /** * Get the total number of anchors in the store. */ get size() { return this.anchors.length; } // ===== Private ===== /** * Enforce the maximum anchor capacity. * * Only expired anchors are evicted, starting from the oldest. * If no expired anchors can be evicted and the store is still * over capacity, the oldest expired anchors are removed first. * Active (non-expired) anchors are never evicted. */ enforceCapacity(now) { if (this.anchors.length <= this.config.maxAnchors) return; // Collect indices of expired anchors (oldest first, array is append-only) const expiredIndices = []; for (let i = 0; i < this.anchors.length; i++) { const a = this.anchors[i]; if (a.validUntil !== null && a.validUntil <= now) { expiredIndices.push(i); } } // Determine how many we need to evict const excess = this.anchors.length - this.config.maxAnchors; const toEvict = Math.min(excess, expiredIndices.length); if (toEvict <= 0) return; // Build a set of indices to remove (oldest expired first) const removeSet = new Set(expiredIndices.slice(0, toEvict)); // Rebuild the array and index, preserving order const surviving = []; this.indexById.clear(); for (let i = 0; i < this.anchors.length; i++) { if (removeSet.has(i)) continue; this.indexById.set(this.anchors[i].id, surviving.length); surviving.push(this.anchors[i]); } // Replace contents (splice to preserve the same array reference) this.anchors.length = 0; for (const a of surviving) { this.anchors.push(a); } } } // ============================================================================ // Truth Resolver // ============================================================================ /** * Resolves conflicts between internal system beliefs and externally * anchored truth. * * The fundamental principle: truth anchors always win. If a valid, * active truth anchor contradicts an internal belief, the anchor * takes precedence. */ export class TruthResolver { store; constructor(store) { this.store = store; } /** * Check if any active truth anchor contradicts a memory value. * * Searches by namespace and key as tags, and by the memory value * as a claim. If a truth anchor exists that covers the same topic, * it wins over the internal memory. */ resolveMemoryConflict(memoryKey, memoryValue, namespace) { const now = Date.now(); const active = this.store.getActive(now); // Search for anchors tagged with the namespace or memory key const normalizedKey = memoryKey.toLowerCase().trim(); const normalizedNs = namespace.toLowerCase().trim(); const normalizedValue = memoryValue.toLowerCase().trim(); for (const anchor of active) { const lowerTags = anchor.tags.map(t => t.toLowerCase()); // Check if the anchor is relevant to this memory entry const tagMatch = lowerTags.includes(normalizedKey) || lowerTags.includes(normalizedNs) || lowerTags.includes(`${normalizedNs}:${normalizedKey}`); if (!tagMatch) continue; // Check if the anchor's claim contradicts the memory value const anchorClaim = anchor.claim.toLowerCase().trim(); if (anchorClaim !== normalizedValue) { return { truthWins: true, anchor, reason: `Truth anchor "${anchor.id}" (${anchor.kind}) contradicts memory ` + `"${namespace}:${memoryKey}". Anchor claim: "${anchor.claim}" ` + `supersedes internal value: "${memoryValue}".`, }; } } return { truthWins: false, reason: `No active truth anchor contradicts memory "${namespace}:${memoryKey}". ` + `Internal belief stands.`, }; } /** * Check if any active truth anchor constrains a proposed action. * * Searches for anchors whose claims or tags relate to the proposed * action and its context. Returns a conflict resolution indicating * whether the action is constrained. */ resolveDecisionConflict(proposedAction, context) { const now = Date.now(); const active = this.store.getActive(now); const normalizedAction = proposedAction.toLowerCase().trim(); const contextKeys = Object.keys(context).map(k => k.toLowerCase()); for (const anchor of active) { const lowerTags = anchor.tags.map(t => t.toLowerCase()); const lowerClaim = anchor.claim.toLowerCase(); // Check if the anchor is relevant: tags intersect with context keys // or the action text appears in the claim const tagOverlap = lowerTags.some(t => contextKeys.includes(t) || normalizedAction.includes(t)); const claimRelevance = lowerClaim.includes(normalizedAction) || normalizedAction.includes(lowerClaim); if (!tagOverlap && !claimRelevance) continue; // The anchor is relevant -- it constrains this action return { truthWins: true, anchor, reason: `Truth anchor "${anchor.id}" (${anchor.kind}) constrains the ` + `proposed action "${proposedAction}". Anchor claim: "${anchor.claim}". ` + `Attested by: "${anchor.attesterId}".`, }; } return { truthWins: false, reason: `No active truth anchor constrains the proposed action ` + `"${proposedAction}". Action may proceed.`, }; } /** * Get all active truth anchors relevant to a topic. * * Uses fuzzy tag matching: a tag matches the topic if either * the tag contains the topic or the topic contains the tag * (case-insensitive). Also matches against the claim text. */ getGroundTruth(topic) { const now = Date.now(); const active = this.store.getActive(now); const normalizedTopic = topic.toLowerCase().trim(); return active.filter(anchor => { // Fuzzy tag match const tagMatch = anchor.tags.some(tag => { const lowerTag = tag.toLowerCase(); return lowerTag.includes(normalizedTopic) || normalizedTopic.includes(lowerTag); }); // Claim text match const claimMatch = anchor.claim.toLowerCase().includes(normalizedTopic); return tagMatch || claimMatch; }); } } // ============================================================================ // Helpers // ============================================================================ /** * Check whether a truth anchor is active at a given timestamp. */ function isActive(anchor, timestamp) { if (anchor.validFrom > timestamp) return false; if (anchor.validUntil !== null && anchor.validUntil <= timestamp) return false; return true; } /** * Constant-time string comparison to prevent timing attacks on signatures. * * Compares two hex strings character by character, accumulating * differences without short-circuiting. */ function timingSafeEqual(a, b) { if (a.length !== b.length) return false; let mismatch = 0; for (let i = 0; i < a.length; i++) { mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i); } return mismatch === 0; } // ============================================================================ // Factory Functions // ============================================================================ /** * Create a TruthAnchorStore with the given configuration. * * @param config - Must include `signingKey`. `maxAnchors` defaults to 50,000. */ export function createTruthAnchorStore(config) { return new TruthAnchorStore(config); } /** * Create a TruthResolver backed by the given store. */ export function createTruthResolver(store) { return new TruthResolver(store); } //# sourceMappingURL=truth-anchors.js.map