tasq/node_modules/@claude-flow/guidance/dist/temporal.js

658 lines
24 KiB
JavaScript

/**
* Temporal Assertions and Validity Windows
*
* Bitemporal semantics for knowledge: distinguishes *assertion time* (when
* something was recorded) from *validity time* (when the fact is/was/will be
* true in the real world).
*
* The system tracks two independent timelines:
* - Assertion time: when the assertion was recorded and when it was retracted
* - Validity time: when the fact becomes true (validFrom) and stops being true
* (validUntil)
*
* Every assertion carries `validFrom`, `validUntil`, `assertedAt`, and
* `supersededBy` fields. Status is computed dynamically from the current clock
* and the assertion's lifecycle flags (retraction, supersession, expiration).
*
* TemporalStore:
* - Creates assertions with explicit validity windows
* - Retrieves assertions active at any point in time (past, present, future)
* - Supports supersession chains (old fact replaced by new fact)
* - Soft-delete via retraction (preserves full history)
* - Conflict detection: finds multiple active assertions in the same namespace
* - Export/import for persistence
*
* TemporalReasoner:
* - High-level queries: whatWasTrue, whatIsTrue, whatWillBeTrue
* - Change detection since a given timestamp
* - Conflict detection at a point in time
* - Forward projection of assertion validity
*
* @module @claude-flow/guidance/temporal
*/
import { randomUUID } from 'node:crypto';
// ============================================================================
// Default Configuration
// ============================================================================
const DEFAULT_TEMPORAL_CONFIG = {
maxAssertions: 100_000,
autoExpireCheckIntervalMs: 60_000,
};
const SERIALIZATION_VERSION = 1;
// ============================================================================
// TemporalStore
// ============================================================================
/**
* In-memory store for temporal assertions with bitemporal semantics.
*
* Assertions are indexed by ID and support supersession chains, retraction,
* temporal queries (what was active at time T), and conflict detection.
* Status is computed dynamically from the assertion's window and lifecycle
* flags; callers never set status directly.
*/
export class TemporalStore {
config;
assertions = new Map();
constructor(config = {}) {
this.config = { ...DEFAULT_TEMPORAL_CONFIG, ...config };
}
/**
* Create a new temporal assertion.
*
* Records a claim with an explicit validity window. The assertion's status
* is computed automatically from the window and the current time.
*
* @param claim - The fact being asserted
* @param namespace - Grouping namespace
* @param window - Validity window (validFrom and validUntil)
* @param opts - Optional parameters (confidence, source, tags, metadata)
* @returns The newly created TemporalAssertion
*/
assert(claim, namespace, window, opts = {}) {
const now = Date.now();
const assertion = {
id: opts.id ?? randomUUID(),
claim,
namespace,
window: {
validFrom: window.validFrom,
validUntil: window.validUntil,
assertedAt: now,
retractedAt: null,
},
status: 'active', // placeholder; computed below
supersededBy: null,
supersedes: null,
confidence: clamp(opts.confidence ?? 1.0, 0, 1),
source: opts.source ?? 'system',
tags: opts.tags ? [...opts.tags] : [],
metadata: opts.metadata ? { ...opts.metadata } : {},
};
assertion.status = computeStatus(assertion, now);
this.assertions.set(assertion.id, assertion);
this.enforceCapacity();
return assertion;
}
/**
* Retrieve an assertion by ID.
*
* The returned assertion has its status recomputed against the current time.
*
* @param id - The assertion ID
* @returns The assertion, or undefined if not found
*/
get(id) {
const assertion = this.assertions.get(id);
if (!assertion)
return undefined;
assertion.status = computeStatus(assertion, Date.now());
return assertion;
}
/**
* Get all assertions that were active at a specific point in time.
*
* An assertion is considered "active at time T" if:
* - Its validity window contains T (validFrom <= T and (validUntil is null or T < validUntil))
* - It has not been retracted
* - It has not been superseded
*
* @param pointInTime - The reference time (ms epoch)
* @param namespace - Optional namespace filter
* @returns Active assertions at the specified time
*/
getActiveAt(pointInTime, namespace) {
const results = [];
for (const assertion of this.assertions.values()) {
if (namespace !== undefined && assertion.namespace !== namespace) {
continue;
}
const status = computeStatus(assertion, pointInTime);
if (status === 'active') {
// Update the stored status to reflect current time
assertion.status = computeStatus(assertion, Date.now());
results.push(assertion);
}
}
return results.sort((a, b) => b.window.assertedAt - a.window.assertedAt);
}
/**
* Get all assertions active right now.
*
* Convenience wrapper around `getActiveAt(Date.now())`.
*
* @param namespace - Optional namespace filter
* @returns Currently active assertions
*/
getCurrentTruth(namespace) {
return this.getActiveAt(Date.now(), namespace);
}
/**
* Get the full history of a claim: all assertions (past and present) that
* share the same claim text and namespace, regardless of status.
*
* Results are ordered by assertedAt ascending (oldest first), giving a
* timeline of how the claim evolved.
*
* @param claim - The claim text to search for
* @param namespace - The namespace to search in
* @returns All matching assertions, oldest first
*/
getHistory(claim, namespace) {
const now = Date.now();
const results = [];
for (const assertion of this.assertions.values()) {
if (assertion.claim === claim && assertion.namespace === namespace) {
assertion.status = computeStatus(assertion, now);
results.push(assertion);
}
}
return results.sort((a, b) => a.window.assertedAt - b.window.assertedAt);
}
/**
* Query assertions with multiple optional filters.
*
* All specified filters are ANDed together. Results are ordered by
* assertedAt descending (newest first).
*
* @param opts - Query filter options
* @returns Matching assertions
*/
query(opts = {}) {
const now = Date.now();
const results = [];
for (const assertion of this.assertions.values()) {
assertion.status = computeStatus(assertion, now);
if (opts.namespace !== undefined && assertion.namespace !== opts.namespace) {
continue;
}
if (opts.pointInTime !== undefined) {
const statusAtTime = computeStatus(assertion, opts.pointInTime);
if (statusAtTime !== 'active')
continue;
}
if (opts.status !== undefined && opts.status.length > 0) {
if (!opts.status.includes(assertion.status))
continue;
}
if (opts.source !== undefined && assertion.source !== opts.source) {
continue;
}
if (opts.tags !== undefined && opts.tags.length > 0) {
const assertionTags = new Set(assertion.tags);
if (!opts.tags.every(t => assertionTags.has(t)))
continue;
}
results.push(assertion);
}
return results.sort((a, b) => b.window.assertedAt - a.window.assertedAt);
}
/**
* Supersede an existing assertion with a new one.
*
* Marks the old assertion as superseded and creates a new assertion that
* declares it replaces the old one. The supersession chain is bidirectional:
* oldAssertion.supersededBy = newAssertion.id
* newAssertion.supersedes = oldAssertion.id
*
* @param oldId - ID of the assertion to supersede
* @param newClaim - The replacement claim text
* @param newWindow - The validity window for the replacement
* @param opts - Optional parameters for the new assertion
* @returns The new assertion, or undefined if the old one was not found
*/
supersede(oldId, newClaim, newWindow, opts = {}) {
const old = this.assertions.get(oldId);
if (!old)
return undefined;
// Create the replacement assertion
const replacement = this.assert(newClaim, old.namespace, newWindow, opts);
// Link the chain
replacement.supersedes = oldId;
old.supersededBy = replacement.id;
old.status = computeStatus(old, Date.now());
return replacement;
}
/**
* Retract an assertion (soft delete).
*
* The assertion is marked with a retractedAt timestamp and its status
* becomes 'retracted'. The assertion remains in the store for historical
* queries but is excluded from active truth queries.
*
* @param id - The assertion to retract
* @param _reason - Optional reason for retraction (stored in metadata)
* @returns The retracted assertion, or undefined if not found
*/
retract(id, _reason) {
const assertion = this.assertions.get(id);
if (!assertion)
return undefined;
assertion.window.retractedAt = Date.now();
if (_reason !== undefined) {
assertion.metadata.retractedReason = _reason;
}
assertion.status = computeStatus(assertion, Date.now());
return assertion;
}
/**
* Get the full supersession timeline for an assertion.
*
* Follows the supersedes/supersededBy chain in both directions to build
* the complete lineage: all predecessors (oldest first) and all successors
* (newest last). Handles cycles by tracking visited IDs.
*
* @param id - The assertion to build a timeline for
* @returns The timeline, or undefined if not found
*/
getTimeline(id) {
const assertion = this.assertions.get(id);
if (!assertion)
return undefined;
const now = Date.now();
assertion.status = computeStatus(assertion, now);
const visited = new Set([id]);
// Walk predecessors (what this assertion replaced)
const predecessors = [];
let currentId = assertion.supersedes;
while (currentId !== null) {
if (visited.has(currentId))
break;
visited.add(currentId);
const pred = this.assertions.get(currentId);
if (!pred)
break;
pred.status = computeStatus(pred, now);
predecessors.unshift(pred); // oldest first
currentId = pred.supersedes;
}
// Walk successors (what replaced this assertion)
const successors = [];
currentId = assertion.supersededBy;
while (currentId !== null) {
if (visited.has(currentId))
break;
visited.add(currentId);
const succ = this.assertions.get(currentId);
if (!succ)
break;
succ.status = computeStatus(succ, now);
successors.push(succ); // newest last
currentId = succ.supersededBy;
}
return { assertion, predecessors, successors };
}
/**
* Detect conflicting assertions: multiple assertions active at the same
* time in the same namespace.
*
* Returns groups of assertions that are simultaneously active, which may
* indicate contradictions that need resolution.
*
* @param namespace - The namespace to check
* @param pointInTime - The reference time (defaults to now)
* @returns Array of assertions that are concurrently active (empty if no conflicts)
*/
reconcile(namespace, pointInTime) {
const refTime = pointInTime ?? Date.now();
const active = this.getActiveAt(refTime, namespace);
// A single or zero active assertions means no conflict
if (active.length <= 1)
return [];
return active;
}
/**
* Export all assertions for persistence.
*
* @returns Serializable store representation
*/
exportAssertions() {
const now = Date.now();
const assertions = [];
for (const assertion of this.assertions.values()) {
assertion.status = computeStatus(assertion, now);
assertions.push({ ...assertion, metadata: { ...assertion.metadata } });
}
return {
assertions,
createdAt: new Date().toISOString(),
version: SERIALIZATION_VERSION,
};
}
/**
* Import previously exported assertions, replacing all current contents.
*
* @param data - Serialized store data
* @throws If the version is unsupported
*/
importAssertions(data) {
if (data.version !== SERIALIZATION_VERSION) {
throw new Error(`Unsupported temporal store version: ${data.version} (expected ${SERIALIZATION_VERSION})`);
}
this.assertions.clear();
const now = Date.now();
for (const assertion of data.assertions) {
const imported = {
...assertion,
tags: [...assertion.tags],
metadata: { ...assertion.metadata },
};
imported.status = computeStatus(imported, now);
this.assertions.set(imported.id, imported);
}
}
/**
* Remove expired assertions whose validity ended before the given timestamp.
*
* Only assertions with status 'expired' and validUntil before the cutoff
* are removed. Retracted and superseded assertions are preserved for
* historical traceability.
*
* @param beforeTimestamp - Remove assertions that expired before this time
* @returns Number of assertions pruned
*/
pruneExpired(beforeTimestamp) {
let pruned = 0;
const now = Date.now();
for (const [id, assertion] of this.assertions) {
assertion.status = computeStatus(assertion, now);
if (assertion.status === 'expired' &&
assertion.window.validUntil !== null &&
assertion.window.validUntil < beforeTimestamp) {
this.assertions.delete(id);
pruned++;
}
}
return pruned;
}
/**
* Get the number of stored assertions.
*/
get size() {
return this.assertions.size;
}
/**
* Get the current configuration.
*/
getConfig() {
return { ...this.config };
}
/**
* Remove all assertions from the store.
*/
clear() {
this.assertions.clear();
}
// ===== Private =====
/**
* Enforce the maximum assertion capacity by pruning the oldest expired
* assertions first.
*/
enforceCapacity() {
if (this.assertions.size <= this.config.maxAssertions)
return;
const now = Date.now();
// Collect expired assertions sorted by validUntil ascending (oldest first)
const expired = [];
for (const [id, assertion] of this.assertions) {
const status = computeStatus(assertion, now);
if (status === 'expired' && assertion.window.validUntil !== null) {
expired.push({ id, validUntil: assertion.window.validUntil });
}
}
expired.sort((a, b) => a.validUntil - b.validUntil);
// Remove oldest expired until under capacity
let removed = 0;
const excess = this.assertions.size - this.config.maxAssertions;
for (const entry of expired) {
if (removed >= excess)
break;
this.assertions.delete(entry.id);
removed++;
}
// If still over capacity, remove oldest retracted
if (this.assertions.size > this.config.maxAssertions) {
const retracted = [];
for (const [id, assertion] of this.assertions) {
if (assertion.window.retractedAt !== null) {
retracted.push({ id, retractedAt: assertion.window.retractedAt });
}
}
retracted.sort((a, b) => a.retractedAt - b.retractedAt);
const stillExcess = this.assertions.size - this.config.maxAssertions;
for (let i = 0; i < Math.min(stillExcess, retracted.length); i++) {
this.assertions.delete(retracted[i].id);
}
}
}
}
// ============================================================================
// TemporalReasoner
// ============================================================================
/**
* High-level temporal reasoning over a TemporalStore.
*
* Provides semantic queries like "what was true at time T", "what will be
* true at time T", change detection, conflict detection, and forward
* projection of assertion validity.
*/
export class TemporalReasoner {
store;
constructor(store) {
this.store = store;
}
/**
* What was true at a past point in time?
*
* Returns all assertions that were active (valid, not retracted, not
* superseded) at the specified historical moment.
*
* @param namespace - The namespace to query
* @param pointInTime - The historical moment (ms epoch)
* @returns Assertions that were active at that time
*/
whatWasTrue(namespace, pointInTime) {
return this.store.getActiveAt(pointInTime, namespace);
}
/**
* What is true right now?
*
* Returns all currently active assertions in the given namespace.
*
* @param namespace - The namespace to query
* @returns Currently active assertions
*/
whatIsTrue(namespace) {
return this.store.getCurrentTruth(namespace);
}
/**
* What will be true at a future point in time?
*
* Returns assertions whose validity window includes the specified future
* time and that have not been retracted or superseded.
*
* @param namespace - The namespace to query
* @param futureTime - The future moment (ms epoch)
* @returns Assertions that will be active at that time
*/
whatWillBeTrue(namespace, futureTime) {
return this.store.getActiveAt(futureTime, namespace);
}
/**
* Detect changes in a namespace since a given timestamp.
*
* Returns a list of changes ordered by their change time, including:
* - New assertions created after the timestamp
* - Assertions superseded after the timestamp
* - Assertions retracted after the timestamp
* - Assertions that expired after the timestamp
*
* @param namespace - The namespace to check
* @param sinceTimestamp - Only include changes after this time (ms epoch)
* @returns List of detected changes
*/
hasChanged(namespace, sinceTimestamp) {
const changes = [];
const now = Date.now();
const all = this.store.query({ namespace });
for (const assertion of all) {
// New assertion
if (assertion.window.assertedAt > sinceTimestamp) {
changes.push({
assertion,
changeType: 'asserted',
changedAt: assertion.window.assertedAt,
});
}
// Retracted
if (assertion.window.retractedAt !== null &&
assertion.window.retractedAt > sinceTimestamp) {
changes.push({
assertion,
changeType: 'retracted',
changedAt: assertion.window.retractedAt,
});
}
// Superseded: we infer the supersession time from the successor's
// assertedAt, since the old assertion is marked superseded when the
// new one is created.
if (assertion.supersededBy !== null) {
const successor = this.store.get(assertion.supersededBy);
if (successor && successor.window.assertedAt > sinceTimestamp) {
changes.push({
assertion,
changeType: 'superseded',
changedAt: successor.window.assertedAt,
});
}
}
// Expired: the assertion expired between sinceTimestamp and now
if (assertion.window.validUntil !== null &&
assertion.window.validUntil > sinceTimestamp &&
assertion.window.validUntil <= now &&
assertion.window.retractedAt === null &&
assertion.supersededBy === null) {
changes.push({
assertion,
changeType: 'expired',
changedAt: assertion.window.validUntil,
});
}
}
return changes.sort((a, b) => a.changedAt - b.changedAt);
}
/**
* Detect conflicting (contradictory) assertions active at the same time
* in the same namespace.
*
* Returns all concurrently active assertions if there are two or more.
* An empty array means no conflicts.
*
* @param namespace - The namespace to check
* @param pointInTime - The reference time (defaults to now)
* @returns Conflicting assertions, or empty if no conflicts
*/
conflictsAt(namespace, pointInTime) {
return this.store.reconcile(namespace, pointInTime);
}
/**
* Project an assertion forward: will it still be valid at a future time?
*
* Checks whether the assertion's validity window includes the future
* timestamp and the assertion has not been retracted or superseded.
*
* @param assertionId - The assertion to project
* @param futureTimestamp - The future time to check (ms epoch)
* @returns true if the assertion will be active at the future time
*/
projectForward(assertionId, futureTimestamp) {
const assertion = this.store.get(assertionId);
if (!assertion)
return false;
const futureStatus = computeStatus(assertion, futureTimestamp);
return futureStatus === 'active';
}
}
// ============================================================================
// Factory Functions
// ============================================================================
/**
* Create a TemporalStore with optional configuration.
*
* @param config - Partial configuration; unspecified values use defaults
* @returns A fresh TemporalStore
*/
export function createTemporalStore(config) {
return new TemporalStore(config);
}
/**
* Create a TemporalReasoner backed by the given store.
*
* @param store - The TemporalStore to reason over
* @returns A fresh TemporalReasoner
*/
export function createTemporalReasoner(store) {
return new TemporalReasoner(store);
}
// ============================================================================
// Helpers
// ============================================================================
/**
* Compute the temporal status of an assertion at a given reference time.
*
* Priority order:
* 1. retracted (retractedAt is set)
* 2. superseded (supersededBy is set)
* 3. expired (validUntil <= referenceTime)
* 4. active (validFrom <= referenceTime and window is open)
* 5. future (validFrom > referenceTime)
*
* @param assertion - The assertion to evaluate
* @param referenceTime - The time to compute status against (ms epoch)
* @returns The computed TemporalStatus
*/
function computeStatus(assertion, referenceTime) {
// Retraction takes absolute priority
if (assertion.window.retractedAt !== null) {
return 'retracted';
}
// Supersession takes next priority
if (assertion.supersededBy !== null) {
return 'superseded';
}
// Check temporal position relative to validity window
const { validFrom, validUntil } = assertion.window;
if (validUntil !== null && referenceTime >= validUntil) {
return 'expired';
}
if (referenceTime >= validFrom) {
return 'active';
}
return 'future';
}
/**
* Clamp a number to the range [min, max].
*/
function clamp(value, min, max) {
return Math.min(max, Math.max(min, value));
}
//# sourceMappingURL=temporal.js.map