tasq/node_modules/@claude-flow/guidance/dist/truth-anchors.js

488 lines
18 KiB
JavaScript

/**
* 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