/** * E2 policy first-class object (docs/plans/2026-05-30-e2-policy-object.md). * * The "bi-temporal-first" object type: a named rule/statement that is in force * over an EFFECTIVE-TIME range and evolves via supersession. Two time axes: * * - Valid time (effective time): when the policy is in force in the real world, * as first-class columns `valid_from` (required; defaults to creation time) * and `valid_to` (nullable = open-ended). This is the queryable axis: see * `loadPoliciesAsOf` (the active policies in force at a given valid-time). * - Transaction time (system time): when the row was recorded / retired, via * `created_at` + the supersede chain's `superseded_at`. Present, but * time-travel ("what did we BELIEVE was in force at past system time T") is * deferred to a future version. * * The delta lifecycle reuses the process/decision supersede machinery verbatim * (superseded_by self-FK + CAS + INSERT-preflight + server-derived version + * change_summary + supersede tenant-match trigger). It DROPS process's `steps` * (a policy has `policy_text`) and ADDS `valid_from`/`valid_to`. * * The `policies` table is the source of truth (survives memory decay); the * memory mirror is for recall only. memory_id is NULLABLE with ON DELETE SET * NULL so forget/consolidate/archive gracefully orphans the policy row. * * Lifecycle: active -> superseded (a newer version replaces it) or active -> * closed (retired with no successor). Superseding leaves the predecessor's * valid-time range intact (it WAS effective then); only the status flips. * * Date handling: every date input (savePolicy's valid_from/valid_to, * loadPoliciesAsOf's asOfDate) is normalized to canonical ISO-8601 datetime * (`toISOString`) at the store boundary BEFORE any persist or compare, so the * fixed-width values sort lexically and the half-open [valid_from, valid_to) * comparison is correct (plan-eng-critic round-1 CRIT fix: a date-only asOf vs a * datetime valid_from otherwise made a same-day policy invisible). * * Dual-write atomicity: `savePolicy` writes the memory + policies row (and, on * supersede, the predecessor's UPDATE) inside writeEntry's SAVEPOINT. */ export type PolicyStatus = 'active' | 'superseded' | 'closed'; export declare const VALID_POLICY_STATES: ReadonlySet; export interface Policy { id: number; /** Nullable: ON DELETE SET NULL lets memory deletion proceed without breaking * the policy row. */ memoryId: string | null; tenantId: string; policyName: string; policyText: string; /** Canonical ISO-8601 datetime; when the policy takes effect. Always set. */ validFrom: string; /** Canonical ISO-8601 datetime; when it expires. null = open-ended. */ validTo: string | null; /** Server-derived: 1 on a fresh create, predecessor.version + 1 on supersede. */ version: number; status: PolicyStatus; supersededBy: number | null; supersededAt: string | null; /** The per-version delta note; set on a successor row only (NULL on a v1). */ changeSummary: string | null; closedAt: string | null; createdAt: string; } export interface SavePolicyOpts { policyName: string; policyText: string; /** ISO-8601; normalized to canonical datetime. Defaults to now when omitted. */ validFrom?: string; /** ISO-8601; normalized; must be > validFrom. null/undefined = open-ended. */ validTo?: string; /** The delta note for a supersession; ignored (stored NULL) on a fresh create. */ changeSummary?: string; /** Table id of an ACTIVE policy this new version supersedes. */ supersedesPolicyId?: number; /** Extra memory tags merged after ['policy']. */ extraTags?: string[]; } export interface ListPoliciesOpts { status?: PolicyStatus; limit?: number; } /** * Parse + canonicalize a date input to ISO-8601 datetime (`toISOString`). Throws * on an unparseable value. Whatever `new Date()` accepts is re-emitted in the * single fixed-width canonical form, so date-only and datetime inputs collapse to * comparable values and lexical ordering is sound. (Overflow inputs like * '2026-02-30' roll forward per JS Date semantics rather than throwing; the * stored value is still canonical.) */ export declare function normalizePolicyDate(input: string, label?: string): string; /** * Normalize valid_from (defaulting to `nowIso` when undefined) + valid_to (null * when undefined), then enforce valid_to > valid_from. Returns the canonical pair. */ export declare function validatePolicyDates(validFromRaw: string | undefined, validToRaw: string | undefined, nowIso: string): { validFrom: string; validTo: string | null; }; /** * Create a policy (or a new version that supersedes an existing one). Writes the * memory mirror + the policies row atomically inside writeEntry's SAVEPOINT. * valid_from defaults to now; valid_to must be > valid_from (validatePolicyDates). * When supersedesPolicyId is given, the referenced ACTIVE row is preflighted * (status + version) BEFORE the INSERT, then CAS-UPDATEd -> superseded in the same * SAVEPOINT; the new version = predecessor.version + 1 (server-derived). */ export declare function savePolicy(hippoRoot: string, tenantId: string, opts: SavePolicyOpts, actor?: string): Policy; /** * Close (retire) an active policy with no successor. CAS guard WHERE * status='active'; 0 changes distinguishes not-found from not-active. A * superseded row is terminal and cannot be closed. */ export declare function closePolicy(hippoRoot: string, tenantId: string, id: number, actor?: string): Policy; export declare function loadPolicyById(hippoRoot: string, tenantId: string, id: number): Policy | null; export declare function loadPolicies(hippoRoot: string, tenantId: string, opts?: ListPoliciesOpts): Policy[]; export declare function loadActivePolicies(hippoRoot: string, tenantId: string, opts?: { limit?: number; }): Policy[]; /** * The bi-temporal as-of query: the policies in force at `asOfDate` (a valid-time) * per current knowledge. Half-open interval [valid_from, valid_to): a row covers * T when valid_from <= asOf AND (valid_to IS NULL OR asOf < valid_to). asOfDate is * normalized to canonical datetime first so the lexical comparison is sound. * * A row is returned when it covers T AND it is the live answer for T: * - `active` rows that cover T, OR * - `superseded` rows that cover T BUT whose successor was not yet effective at T * (successor.valid_from > asOf) - i.e. an earlier version that was genuinely in * force then. This is the core valid-time correctness: a Jan-Jun policy * superseded in May is still the answer for `asof March`. (codex review * 2026-05-30, P2 #2: filtering on status='active' alone dropped historically- * valid superseded versions, conflating transaction-time with valid-time. The * successor-aware filter mirrors the existing recall-history.ts asOf pattern.) * * `closed` rows are EXCLUDED: closing is a deliberate transaction-time retirement, * and resurrecting closed policies for a historical valid-time is full * transaction-time-travel (deferred). Returns an ARRAY (overlapping same-name * ranges are allowed in v1). Optionally filtered to one policy_name. * * Date-only `asOfDate` (e.g. "2026-05-30", no time component) resolves to the END * of that UTC day (23:59:59.999Z), so "as of [day D]" includes a policy that * became effective at any instant during D - this is the read-side fix for the * common create-then-asof-today workflow, keeping the stored valid_from honest * (codex review 2026-05-30). A full datetime asOf is used as the precise instant. */ export declare function loadPoliciesAsOf(hippoRoot: string, tenantId: string, asOfDate: string, opts?: { name?: string; limit?: number; }): Policy[]; //# sourceMappingURL=policies.d.ts.map