658 lines
24 KiB
JavaScript
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
|