/** * Display ids, uid re-keys, their aliases, and the identity quarantine (T12341). * * `T####` is a per-project DISPLAY id: unique inside one store at rest, typed * by people and agents, but allocated locally, so two offline stores can * allocate the same one for different work. The row's uid is its identity * (`store/row-identity.ts`). Spec: `cleo docs fetch t12341-uid-scheme` §6.4, §9. * * ## Receiving a row: place it, or hold it * * An incoming row travels as a {@link WireRow}: its uid, its birth * fingerprint, its values, and every reference as the referenced row's * (uid, birth fingerprint) ({@link WireRef}). {@link receiveRow} places it in * its table, or HOLDS it in `tasks_identity_quarantine` (local-only) when it * cannot be placed yet: * * - `uid-collision`: a local row has its uid with a different birth * fingerprint. Neither row is re-keyed here unless this replica is the * authority ({@link rekeyRowUid}); the others wait for the published * {@link RekeyReceipt} ({@link applyRekey}). * - `display-id-collision`: a local row holds its display id. The loser is * re-numbered only by its authority ({@link remintTaskDisplayId}); the * others wait for the {@link RemintOp} ({@link applyRemintOp}). * - `key-collision`: another local row holds its local key (a key derived * from a local display id, such as an AC id); the merge engine re-derives * the key (T12344). * - `ref-pending`: a reference does not resolve to a local row with the SAME * birth fingerprint ({@link resolveWireRef}), e.g. it names the loser of a * uid collision whose re-key has not arrived. It never resolves to the * other row that shares the uid. * * Held rows never enter the task tables, so no validator, parser or command * ever sees an id that is not a `T####` (the round-1 provisional `prov-` ids * are gone). Every op that could unblock a held row re-tries the held rows * ({@link releaseHeld}). * * ## Display-id collision: one authority, ordered by HLC * * Every replica agrees on the loser ({@link collisionLoser}: the greater * uid). {@link remintAuthority} is a pure function of synced inputs (cloud * sync, the origin, replica membership with join / retire HLCs, the * collision's HLC) and a point in HLC time: the origin first, then each * active replica in turn for one takeover window, cycling, so a silent * authority is always taken over and nothing waits forever. Two authorities * may both re-mint around a window boundary: {@link applyRemintOp} keeps the * op with the greater HLC on every replica, whatever the arrival order, and * the other number becomes an alias. * * ## Uid collision: re-key the loser * * The loser is the greater birth fingerprint. {@link rekeyRowUid} asserts the * row it re-keys carries that fingerprint (a replica holding the winner never * re-keys it), gives it a random uid, and cascades from STORED identity: * minted children get {@link rekeyedChildUid} of their old uid and the new * owner uid, natural rows keyed by a re-keyed row are re-derived, stored uid * copies (`ac_uid`) and display-id aliases follow. The receipt lists every * value; receivers apply them ({@link applyRekey}) and never recompute. * * ## Gate 28 * * Every write here goes through a chokepoint helper in * `sqlite-data-accessor.ts`; this module only reads and decides. * * @module * @task T12341 * @task T12744 * @task T12745 * @task T12748 * @task T12750 * @epic T12323 */ import type { DatabaseSync } from 'node:sqlite'; import type { TaskClaimGuard } from '@cleocode/contracts'; import type { Hlc as HlcWire } from '@cleocode/contracts/cloud'; import { type QuarantineRow } from './sqlite-data-accessor.js'; import { PRE_HLC_REPLICA } from './sync/hlc.js'; /** Physical name of the display-id alias table. */ export declare const DISPLAY_ID_ALIAS_TABLE = "tasks_display_id_aliases"; /** Physical name of the uid re-key alias table. */ export declare const UID_ALIAS_TABLE = "tasks_uid_aliases"; /** Physical name of the identity quarantine (local-only). */ export declare const QUARANTINE_TABLE = "tasks_identity_quarantine"; /** Why a display id was displaced. */ export type DisplayIdAliasReason = 'collision-remint' | 'superseded-remint' /** The display id a re-mint op assigned (the portable re-mint record, T12800). */ | 'remint-assigned' | 'split-brain-import' | 'manual'; /** * Total order of two encoded HLC values (`Hlc` wire strings). * * @param a - One value. * @param b - The other. * @returns Negative, zero or positive. * @throws HlcError when a value is not an encoded HLC. */ export declare function compareHlc(a: HlcWire, b: HlcWire): number; /** * The replica id a pre-HLC value carries: none issued it. * * @deprecated Import from `./sync/hlc.js` (the import-nothing leaf) — kept * re-exported here for existing consumers. */ export { PRE_HLC_REPLICA }; /** * The collision HLC of two rows that have no creation HLC (written before the * change journal, T12342): the later of their two BIRTHS, at counter 0, from * no replica ({@link PRE_HLC_REPLICA}). Every replica derives the same value * from the rows' synced births; it only anchors the authority schedule * ({@link remintAuthority}), never a clock (HLC never reads a uid's time). * * @param birthMsA - One row's birth, epoch ms. * @param birthMsB - The other's. * @returns An encoded HLC. * @task T12802 */ export declare function collisionHlcFromBirths(birthMsA: number, birthMsB: number): HlcWire; /** * Of two rows that carry the same display id, the one that must be re-minted: * the greater uid. Every replica picks the same one for the same pair, and for * UUIDv7 uids the older row keeps its id. * * @param a - Uid of one claimant. * @param b - Uid of the other. * @returns The uid to re-mint. */ export declare function collisionLoser(a: string, b: string): string; /** A replica of the project, as the authority rule sees it (synced membership). */ export interface ProjectReplica { /** Replica id (T12342). */ readonly id: string; /** HLC at which it joined the project. */ readonly joinedHlc: string; /** HLC at which it was retired, when it was. */ readonly retiredHlc?: string | null; } /** Why {@link remintAuthority} chose its authority. */ export type RemintAuthorityReason = 'server' | 'origin' | 'fallback'; /** Default length of one authority window (HLC physical time). */ export declare const REMINT_TAKEOVER_MS: number; /** Inputs of {@link remintAuthority}: all synced, so every replica has the same. */ export interface RemintAuthorityInput { /** The project syncs to cloud: the server is the authority. */ readonly cloudSynced: boolean; /** Replica that originated the losing row (`null`: backfilled before sync). */ readonly origin: string | null; /** Project membership. */ readonly replicas: readonly ProjectReplica[]; /** When the collision came to exist: the later creation HLC of the two rows. */ readonly collisionHlc: string; /** The HLC time to evaluate at (a replica's clock, or an op's HLC). */ readonly atHlc: string; /** Window length; default {@link REMINT_TAKEOVER_MS}. */ readonly takeoverAfterMs?: number; } /** * Which replica may re-mint the losing row of a display-id collision at HLC * time `atHlc` (spec §9.2). A pure function of synced inputs: every replica * evaluating the same time names the same authority. * * 1. The project syncs to cloud → the server. * 2. Else the origin, while it is an active member, for the first window * after the collision. * 3. Else the FALLBACK CHAIN: the active members other than the origin, * sorted, one window each, cycling. A fallback that stays silent loses its * turn when its window ends, so a collision never waits forever. * * @param input - The facts every replica can see. * @returns The authority, the rule, and the window index (0 = origin). */ export declare function remintAuthority(input: RemintAuthorityInput): { readonly authority: string; readonly reason: RemintAuthorityReason; readonly slot: number; }; /** Where a displacement came from: the replica that authored it, and when (HLC). */ export interface Displacement { /** Replica that authored the displacement (the authority). */ readonly origin?: string | null; /** HLC of the displacement. */ readonly displacedHlc?: string | null; /** Wall-clock record time; defaults to now. */ readonly now?: string; } /** * Record that `displayId` used to name the row (`entityUid`, `entityBirthFp`). * Idempotent. * * @param db - Connection on the project `cleo.db`. * @param entry - The displaced id, the row that carried it, and the displacement. */ export declare function recordDisplayIdAlias(db: DatabaseSync, entry: Displacement & { readonly table: string; readonly displayId: string; readonly entityUid: string; readonly entityBirthFp: string | null; readonly reason: DisplayIdAliasReason; }): void; /** A row that carries, or carried, a display id. */ export interface DisplayIdClaimant { /** Uid of the row (`null` for a live row whose uid is not filled yet). */ readonly uid: string | null; /** Its display id today (`null`: the row is not placed here). */ readonly currentId: string | null; /** `live` when it carries the id now, `alias` when it carried it before. */ readonly via: 'live' | 'alias'; /** Alias only: the replica that displaced it. */ readonly origin?: string | null; /** Alias only: the HLC of the displacement. */ readonly displacedHlc?: string | null; } /** Outcome of resolving a display id. */ export type DisplayIdResolution = { readonly status: 'none'; } | { readonly status: 'resolved'; readonly claimant: DisplayIdClaimant; /** Rows that carried this id before (history; the live row still wins). */ readonly alsoKnownAs: readonly DisplayIdClaimant[]; } | { readonly status: 'ambiguous'; readonly candidates: readonly DisplayIdClaimant[]; }; /** * Resolve a display id: a live row always wins (aliases come back as * history); with no live row, one alias resolves and several are ambiguous. * * @param db - Connection on the project `cleo.db`. * @param table - Display-id table (e.g. `tasks_tasks`). * @param displayId - The id to resolve. * @returns `resolved` (live, or the single alias), `ambiguous` with every * candidate (callers report it as an error and never pick), or `none`. */ export declare function resolveDisplayId(db: DatabaseSync, table: string, displayId: string): DisplayIdResolution; /** * Allocate the next `T####` on the caller's handle, with the same lower bound * `allocateNextTaskId` uses (the highest stored numeric id). Only the re-mint * authority allocates for a collision. * * @param db - Connection on the project `cleo.db`. * @returns The new display id. */ export declare function allocateTaskDisplayId(db: DatabaseSync): string; export { taskReferenceColumns } from './task-reference-columns.js'; /** * The re-mint the authority publishes. Other replicas apply it verbatim * ({@link applyRemintOp}); they never recompute the number. */ export interface RemintOp { /** Uid of the re-minted task. */ readonly uid: string; /** Its birth fingerprint (a uid can be shared by two rows during a uid collision). */ readonly birthFp: string; /** The contested display id it was displaced from. */ readonly oldId: string; /** The display id the authority gave it. */ readonly newId: string; /** The authority that re-minted. */ readonly origin: string | null; /** HLC of the re-mint: the greater HLC wins on every replica. */ readonly hlc: string; } /** Receipt of a local re-mint: the op plus what was rewritten here. */ export interface RemintReceipt extends RemintOp { /** Rows rewritten per `table.column`. */ readonly rewritten: Readonly>; /** Held rows placed because the re-mint freed their id. */ readonly released: readonly HeldRowKey[]; } /** * The winning re-mint of a row: the op with the greatest HLC. It is DERIVED * from the row's `remint-assigned` display aliases, which are portable (they * sync with the row), so a replica that lost local state, or never had it, * reaches the same answer (T12800). */ interface RemintRecord { readonly newId: string; readonly hlc: string; readonly origin: string | null; } /** * AUTHORITY ONLY ({@link remintAuthority}): give the losing task of a * display-id collision a new display id. Allocates the next `T####`, renames * through the chokepoint (version CAS, claim columns kept), records the * contested id as an alias and the op as the row's re-mint record, places any * held row the freed id unblocks, and returns the op to publish. * * @param db - Connection on the project `cleo.db`. * @param taskId - Current local display id of the task to re-mint. * @param options - The contested id (default: `taskId`), why, the authority, * the op's HLC, and an optional version / claim guard for a manual rename. * @returns The op to publish, with what was rewritten here. * @throws CleoError NOT_FOUND when the task does not exist or has no identity yet. */ export declare function remintTaskDisplayId(db: DatabaseSync, taskId: string, options: { readonly reason: DisplayIdAliasReason; readonly origin: string | null; readonly hlc: string; readonly contestedId?: string; readonly now?: string; readonly guard?: { readonly expectedUpdatedAt?: string; readonly claim?: TaskClaimGuard; }; }): RemintReceipt; /** Outcome of {@link applyRemintOp}. */ export type ApplyRemintResult = { readonly status: 'applied'; readonly receipt: RemintReceipt; } /** The row is not placed here (held or not yet received): the op is recorded * and the row takes `newId` when it is placed. */ | { readonly status: 'recorded'; readonly released: readonly HeldRowKey[]; } | { readonly status: 'already-applied'; } /** An op with a greater HLC was applied already; this one's number became an alias. */ | { readonly status: 'superseded'; readonly winner: RemintRecord; } /** The new id is held here by another row: a NEW collision for its authority. */ | { readonly status: 'conflict'; readonly holderUid: string | null; }; /** * NON-AUTHORITY: apply a re-mint an authority published. Of several ops for * one row, the one with the greatest HLC wins on every replica, whatever the * arrival order: a lesser op is recorded as an alias (`superseded`), a greater * one renames the row again and turns the previous number into an alias. * Never allocates. * * @param db - Connection on the project `cleo.db`. * @param op - The published re-mint. * @returns What happened. */ export declare function applyRemintOp(db: DatabaseSync, op: RemintOp): ApplyRemintResult; /** A value a wire row carries. */ export type WireValue = string | number | null; /** A reference on the wire: the referenced row's uid and birth fingerprint. */ export interface WireRef { /** * The referenced row's uid; `null` when the SENDER could not find the row * its local key names and knows no uid for it. */ readonly uid: string | null; /** * The referenced row's birth fingerprint. A minted target is only ever * matched on uid AND fingerprint, so a reference without one never matches * a row (T12798 review). `null` for a natural target (its uid is a pure * function; no fingerprint). */ readonly birthFp: string | null; /** * The sender had the reference but no longer has the row it names (deleted * there). The receiver uses the row when it still has it (uid and * fingerprint), else applies the table's `gone` rule instead of waiting * for a row that will never arrive. */ readonly gone?: true; } /** * Version of the wire row format. v2 (T12798) carries no local key of * another row in `values`; a row without it (or older) came from a build * whose values hold raw local keys, and is held (`unsupported-wire`) rather * than applied or silently stripped. */ export declare const WIRE_VERSION = 2; /** An incoming row (the merge engine, T12344, builds these from the outbox). */ export interface WireRow { /** {@link WIRE_VERSION} of the sender (absent: a v1 sender). */ readonly version?: number; readonly table: string; readonly uid: string; /** Birth fingerprint (minted tables); `null` for natural rows. */ readonly birthFp: string | null; /** * Column values, WITHOUT any local key of another row: reference columns, * stored reference uids and the key columns they come from, and JSON arrays * of ids travel as {@link refs} / {@link jsonRefs} and are resolved on the * receiver (T12798). */ readonly values: Readonly>; /** * Reference column → the referenced row's identity, or `null` for NULL. * Also carries each stored reference uid column (`ac_uid`): its resolution * sets both that column and the key column it comes from (`ac_id`). */ readonly refs?: Readonly>; /** JSON array of ids column → the identities of its elements, in order. */ readonly jsonRefs?: Readonly>; } /** Why a row is held. */ export type HoldReason = QuarantineRow['reason']; /** The key of a held row. */ export interface HeldRowKey { readonly entityTable: string; readonly uid: string; readonly birthFp: string; } /** A uid collision found while receiving. */ export interface UidCollision { readonly kind: 'uid'; readonly table: string; readonly uid: string; /** The smaller fingerprint keeps the uid. */ readonly winnerBirthFp: string; /** The greater fingerprint is re-keyed by its authority. */ readonly loserBirthFp: string; /** True when the local (placed) row is the loser. */ readonly localIsLoser: boolean; } /** A display-id collision found while receiving. */ export interface DisplayIdCollision { readonly kind: 'display-id'; readonly table: string; readonly displayId: string; readonly loserUid: string; readonly localIsLoser: boolean; } /** Outcome of {@link receiveRow}. */ export type ReceiveResult = { readonly status: 'inserted'; readonly key: string; readonly uid: string; } | { readonly status: 'duplicate'; readonly uid: string; } | { readonly status: 'held'; readonly reason: HoldReason; readonly collision?: UidCollision | DisplayIdCollision; }; /** Outcome of {@link resolveWireRef}. */ export type WireRefResolution = { readonly status: 'row'; readonly key: string; readonly uid: string; } | { readonly status: 'held'; } | { readonly status: 'pending'; }; /** * Resolve a wire reference to a local row: the row with that uid AND that * birth fingerprint, following `tasks_uid_aliases` when the target was * re-keyed. A row that has the uid with ANOTHER fingerprint (the other side of * a uid collision) never matches: the reference is `held` (its target waits * in the quarantine) or `pending` (its target has not arrived, or its re-key * has not), and the referencing row waits too. * * @param db - Connection on the project `cleo.db`. * @param table - The referenced table. * @param ref - The reference. * @returns Where it points. */ export declare function resolveWireRef(db: DatabaseSync, table: string, ref: WireRef): WireRefResolution; /** The project policy a received row is validated against (T12801). */ export interface ReceivePolicy { /** The resolved `hierarchy.maxDepth` (config, `CLEO_HIERARCHY_MAX_DEPTH`, or the profile). */ readonly maxDepth: number; } /** * Resolve the receive policy of a project from its configuration, exactly as * a local task write does (`resolveHierarchyPolicy(loadConfig())`, so * `hierarchy.maxDepth` and `CLEO_HIERARCHY_MAX_DEPTH` apply). The merge engine * resolves it once per merge and passes it to {@link receiveRow}. * * @param cwd - Project root. * @returns The policy. */ export declare function loadReceivePolicy(cwd?: string): Promise; /** * Receive one row: place it, or hold it (module docs). The first receive * marks the store's identity values as shared, so the open pass never * clears them (T12746). * * @param db - Connection on the project `cleo.db`, inside the merge transaction. * @param wire - The incoming row. * @param policy - The project's resolved receive policy ({@link loadReceivePolicy}). * It stays in force on this connection, also for held rows released later; * without one, the default hierarchy profile applies. * @returns What happened; a `held` result carries the collision for the caller * (the merge engine) to hand to its authority. */ export declare function receiveRow(db: DatabaseSync, wire: WireRow, policy?: ReceivePolicy): ReceiveResult; /** * Re-try the held rows now (after an op this module did not apply). * * @param db - Connection on the project `cleo.db`. * @returns The rows placed. */ export declare function releaseHeld(db: DatabaseSync): HeldRowKey[]; /** A held row, as `cleo doctor` and the merge status report it. */ export interface HeldRow extends HeldRowKey { readonly reason: HoldReason; readonly contestedId: string | null; readonly createdAt: string; readonly row: WireRow; } /** * Every held row (read-only: held rows are never edited in place). * * @param db - Connection on the project `cleo.db`. * @returns The held rows, oldest first. */ export declare function listHeldRows(db: DatabaseSync): HeldRow[]; /** * The wire form of a local row (what a sender publishes): values without the * identity columns, and every declared reference as the target's uid and * birth fingerprint. * * @param db - Connection on the project `cleo.db`. * @param table - Declared table. * @param uid - The row's uid. * @returns The wire row. */ export declare function wireRowOf(db: DatabaseSync, table: string, uid: string): WireRow; /** A natural row whose uid was re-derived because a re-keyed row is in its key. */ export interface NaturalRederive { readonly table: string; readonly oldUid: string; readonly newUid: string; } /** Receipt of {@link rekeyRowUid}: one re-keyed row and everything that followed. */ export interface RekeyReceipt { /** Table of the re-keyed row. */ readonly table: string; /** The colliding uid it carried. */ readonly oldUid: string; /** Its birth fingerprint (unchanged; keys the alias and every match). */ readonly birthFp: string; /** The new uid. */ readonly newUid: string; /** Natural rows re-derived (dependencies, relations, labels, display aliases). */ readonly natural: readonly NaturalRederive[]; /** Minted children re-keyed with it ({@link rekeyedChildUid}), recursively. */ readonly cascaded: readonly RekeyReceipt[]; /** The authority that re-keyed. */ readonly origin: string | null; /** HLC of the re-key. */ readonly hlc: string | null; } /** * AUTHORITY ONLY: re-key the LOSER of a uid collision (same uid, different * birth fingerprints; the loser is the greater fingerprint). The caller names * the loser by its fingerprint and the call asserts it: the row re-keyed is * the one with `loserBirthFp`, wherever it is here (placed, or held because * the winner arrived first), and a fingerprint that is not the greater of the * pair is refused. The row gets a random uid; the cascade follows from stored * identity (module docs). Held rows the re-key unblocks are placed. * * @param db - Connection on the project `cleo.db`. * @param table - Minted table of the row. * @param uid - The colliding uid. * @param collision - The loser's and the winner's birth fingerprints (the * winner must be the smaller; T12801). * @param options - The authority and the HLC of the re-key. * @returns The receipt to publish, with its cascade and natural uids. * @throws CleoError when this replica has no row (uid, loserBirthFp), or the * fingerprint is not the loser's. */ export declare function rekeyRowUid(db: DatabaseSync, table: string, uid: string, collision: { readonly loserBirthFp: string; readonly winnerBirthFp: string; }, options?: Displacement): RekeyReceipt; /** Outcome of {@link applyRekey} for one row of the receipt. */ export type RekeyApplyStatus = 'applied' | 'applied-held' | 'already-applied' | 'alias-only'; /** * RECEIVER: apply a published {@link RekeyReceipt}. Every entry moves the row * with (old uid, birth fingerprint) to the published new uid, wherever it is * here: a placed row (with its stored copies, display aliases and the * published natural uids), a held row (its wire form), or nowhere (the uid * alias is recorded, so the row takes its new uid when it arrives). A row * with the old uid and ANOTHER fingerprint (the winner) is never touched. * Nothing is recomputed. Held rows the re-key unblocks are placed. * * @param db - Connection on the project `cleo.db`. * @param receipt - The published receipt. * @returns Per-row outcome, and the held rows placed. */ export declare function applyRekey(db: DatabaseSync, receipt: RekeyReceipt): { readonly rows: ReadonlyArray<{ table: string; oldUid: string; status: RekeyApplyStatus; }>; readonly released: readonly HeldRowKey[]; }; //# sourceMappingURL=display-id-alias.d.ts.map