UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

1,249 lines • 90.3 kB
import { scopeDirName, type ChallengeEvent, type MemoryPartitionIncidentSink } from "./layout.js";
import type { MemoryBundleImportPlan, MemoryImportReport } from "./export-bundle.js";
import type { HarvestRejection, MemoryBackend, MemoryEntry, MemoryEntryHeader, MemoryScopeEnumeration, NotePatch, PatchReport, ScoredMemoryEntry } from "./types.js";
export { DEFAULT_MAX_ENTRY_DEPTH, MEMORY_INDEX_FILENAME, scanEntryFiles, type ScannedEntryFile } from "./entry-scan.js";
/** design/178 v2-b §3.3(b) — the explicit erasure selector (each form resolves to an explicit id
 *  list before anything is deleted; there is no whole-store selector). */
export type ErasureSelect = {
    ids: readonly string[];
} | {
    scope: string;
} | {
    sessionId: string;
};
/** design/178 v2-b §3.3(c) — one erased row's coordinates (the snapshot's tagged binding law,
 *  without the account-trace extras: a wire validator refuses hybrid objects). */
export type ErasedBinding = {
    state: "bound";
    scope: string;
    slug: string;
} | {
    state: "unbound";
};
/** The input of the erasure lane (engine host API `eraseMemoryEntries` and the File capability face
 *  `eraseWithEvidence` share it; the File face ignores `allowUnevidenced` — with the evidence
 *  capability present there is nothing to degrade to). */
export interface EraseMemoryEntriesInput {
    /** Idempotency identity, caller-minted (challenge-family law: the engine never mints one). */
    requestId: string;
    select: ErasureSelect;
    /** Evidence-capability-ABSENT degradation opt-in (engine lane only; default = loud refusal). */
    allowUnevidenced?: boolean;
}
/**
 * design/178 v2-b §3.3(c) — the erasure attestation (the deliverable a compliance caller archives).
 * Every claim is black-box re-checkable: an `erased` row is verified by `committedSnapshotOf(id)`
 * answering `"absent"`, the chain containing its `evidenceEv` delete row, and `getByIds([id])`
 * answering empty.
 */
export interface MemoryErasureAttestation {
    /** Version envelope — a consumer reading `v > 1` must refuse, never reinterpret. */
    v: 1;
    requestId: string;
    at: number;
    /** `"complete"` ⇔ conflicts empty ∧ every pinned id ∈ erased ∪ erasedPreviously ∪ (notFound with
     *  `custodyState === "complete"`). Everything else is `"partial"` (replay converges it). */
    status: "complete" | "partial";
    /** `"journal"` = the File evidence path; `"none"` = the explicit `allowUnevidenced` degradation
     *  lane (always present — a silent downgrade is the forbidden shape). */
    evidenceCapability: "journal" | "none";
    /** The evidence chain's integrity as read at assembly (v2-a custody tri-state law). `"damaged"`
     *  ⇒ every notFound row carries `historyUnknown` and `erasedPreviously` is never produced ("no
     *  row" cannot be historically judged over a chain with a known hole). */
    custodyState: "complete" | "damaged" | "capability-absent";
    /** The selector, echoed verbatim. */
    select: ErasureSelect;
    /** sha256 hex of the selector's canonical JSON — equals the anchor row's (`"none"` mode: computed
     *  for this call only; there is no anchor to reconcile against). */
    selectHash: string;
    /** The pinned id set (= the anchor row's `ids`; a replay reads it back, never re-resolves). */
    resolvedIds: string[];
    erased: Array<{
        id: string;
        rev: string;
        binding: ErasedBinding;
        /** The delete evidence row's `ev` — absent in `"none"` mode. */
        evidenceEv?: string;
        /** Distinct census-confirmed projection files this transaction physically deleted (the sweep
         *  arm can answer >1: stray copies and P≥2 unbound duplicates are all removed). */
        projectionsRemoved: number;
        /** Contributing sessions (pre-capture ∪ the same-lock lineage capture-and-clear; `"none"` mode
         *  answers `[]`). NON-durable beyond the chain's pre-capture: a replay after a crash carries
         *  the chain's set — honest downgrade, never a fabricated rebuild. */
        sessions: string[];
    }>;
    /** No row at judgment time and not claimable as erased under THIS request. Covers never-existed /
     *  organically deleted / deleted by another request — the chain can distinguish them, the
     *  attestation does not pretend to. `historyUnknown` ⇔ `custodyState !== "complete"`. */
    notFound: Array<{
        id: string;
        historyUnknown?: true;
    }>;
    /** Chain-read rows: an origin-ABSENT delete row carrying THIS requestId (per-request judgment —
     *  another request's delete of the id answers notFound, never a claim). Present only when
     *  non-empty; never produced in `"none"` mode or over a damaged chain. */
    erasedPreviously?: Array<{
        id: string;
        ev: string;
        at: number;
        /** The pre-delete binding from the most recent same-request delete row that RECORDED one —
         *  carried so a REPLAY can still run the index-line sweep (the ledger row is gone; the evidence
         *  row is the only surviving source of the projection path). `ev`/`at` come from the MOST RECENT
         *  same-request delete row (which may be a later, binding-less re-erase). Absent in THREE cases:
         *  no same-request delete row ever recorded a binding (unbound / pre-v2-form), or the recorded
         *  path is OCCUPIED now (a live account row binds that scope+slug, or bytes exist at the path —
         *  the binding is known but suppressed: the index line there belongs to the occupant, and a
         *  consumer must not read absence as "no residue possible"). */
        from?: {
            scope: string;
            slug: string;
        };
    }>;
    /** Per-id planning-stage faults (io/plan errors under the lock — there is no CAS loser inside a
     *  single lock span). Non-fatal; a replay converges them. */
    conflicts: Array<{
        id: string;
        reason: string;
    }>;
    residuals: {
        /** Quarantine files whose frontmatter id is in the pinned set (preserve-not-delete: the
         *  attestation enumerates, an explicit second action clears). */
        quarantineHits: string[];
        /** Quarantine files that could not be attributed (fragments / non-entry shapes) — the honest
         *  bound of the enumeration. */
        quarantineOpaque: number;
        /** Derived MEMORY.md index files that may STILL carry a line naming an erased entry, because
         *  the erase-time index sweep could not read or rewrite them (a non-file at the index path, a
         *  non-ENOENT read failure, or a refused write — an entry may carry a parenthesized failure
         *  detail). Present only when non-empty. Minted by the ENGINE lane
         *  (`MemoryEngine.eraseMemoryEntries` — the derived index is the engine's plane; this backend
         *  face answers without the seat). The next successful materialize's rebuild also clears
         *  orphaned lines, but a one-off compliance erasure cannot count on one — hence the disclosure
         *  instead of a silent clean residual set. */
        indexUncleared?: string[];
        /** Propagation boundary, self-declared always: deletion is a THIS-store fact; remote-peer
         *  convergence is the sync deployment's half to prove. */
        propagation: "local-store-only";
    };
}
/** Canonical JSON (recursively sorted keys, no whitespace) — the erasure protocol's ONE comparison
 *  and hash basis: `selectHash` complains bind to it, and evidence rows are written/compared in this
 *  form so a re-serialized import (key order changed by a middle layer) still compares equal. */
export declare function canonicalJsonStringify(value: unknown): string;
/** sha256 hex over the selector's canonical JSON + LF — the replay-identity judge (§4.6) and the
 *  anchor validator's recomputation basis (a 64-hex shape check alone would let a forged anchor swap
 *  the selector under a kept hash). */
export declare function erasureSelectHash(select: ErasureSelect): string;
/** Erasure input validation, shared by the engine host API and the File capability face (one rule
 *  set — the selector the anchor row echoes verbatim must pass the anchor's own validator, so the
 *  entry check and the chain check are the same predicate). Returns the violation, or undefined. */
export declare function erasureRequestInvalid(input: EraseMemoryEntriesInput): string | undefined;
/**
 * Snapshot the erasure input into PLAIN data by reading every accessor exactly once (round-2 #3/#5):
 * validation and use must observe the SAME value, so an accessor-backed (getter/Proxy) selector or
 * requestId cannot pass validation on read #1 and yield a different (unvalidated) set on read #2.
 * The returned object is inert — its `requestId` is a captured primitive and its `select` is a fresh
 * plain object. Malformed shapes (a non-object select, or one with several/no keys) capture as-is so
 * the validator that runs next reports them; the capture never throws.
 */
export declare function captureErasureInput(input: EraseMemoryEntriesInput): EraseMemoryEntriesInput;
/**
 * design/178 v2-a §1.3 — ONE `transfers.jsonl` evidence row on the PUBLIC audit face, as an OPEN
 * discriminated form: the base keys (`ev` — the row's unique identity, `channel` — the event kind,
 * `at` — ms epoch) are the contract; everything else is variant payload surfaced through the index
 * signature. Known channels today (each validated in full by the store's own rule set):
 *  - `"adopted-move"` / `"applyPatches-move"`: an ownership transfer of one id — payload
 *    `{ id, from: {scope, slug}, to: {scope, slug} }`;
 *  - `"migration-bind"`: an unbound row converging to a binding — payload `{ id, to }` (`from`
 *    optional: honest absence, an unbound row has no prior coordinates);
 *  - `"migration"`: the one-shot v1→v2 baseline summary — payload `{ boundRows, unboundRows }`
 *    (store management history, never per-entry custody).
 * `channel` is deliberately `string`, not a closed union: later releases append new variants to the
 * SAME chain (one custody chain, one recovery protocol — never a second evidence file), and a closed
 * union would break every consumer at the source level on each addition. CONSUMER OBLIGATION: tolerate
 * unknown channels (report/skip/display raw — never an exhaustive `switch` that throws); the per-id
 * custody read passes them through so a chain written by a newer engine still answers here.
 */
export interface TransferEvidence {
    /** Unique event id (the append-idempotency key of the evidence chain). */
    ev: string;
    /** Event kind — open on purpose; see the known-channel table above. */
    channel: string;
    /** ms epoch of the event. */
    at: number;
    /** Variant payload (known channels documented above; unknown channels' payload rides verbatim). */
    [key: string]: unknown;
}
/**
 * design/178 v2-a §1.3 — the committed-account BINDING half of one ledger row, shared verbatim by
 * {@link CommittedEntrySnapshot} and `EntryProvenanceAccount.binding` (one tagged definition — never
 * a look-alike copy: an untagged `{scope, slug}` beside `{state: "unbound"}` can be satisfied by one
 * hybrid object under both readings). `"unbound"` is a LEGAL transitional state (a v1→v2 migration
 * row whose projection could not be derived), never a refusal.
 */
export type CommittedBinding = {
    state: "bound";
    scope: string;
    slug: string;
    at?: number;
    prev?: {
        scope: string;
        slug: string;
        at: number;
    };
} | {
    state: "unbound";
};
/**
 * design/178 v2-a §1.3 — the AUDIT SNAPSHOT of one entry's committed account: the account half
 * (`rev` + {@link CommittedBinding}) and the content half are SEPARATE, because the account's
 * authority is the ledger row while the content may be independently unavailable — an auditor asking
 * "where is this entry bound, at which rev" gets the account facts even when no committed carrier
 * can be produced ("cannot read it" must never collapse into "it does not exist").
 *  - `state: "absent"` — the account has NO row for this id (never a throw, never a guess);
 *  - `content.state: "present"` — the committed entry is servable: the committed bytes (control-plane
 *    shadow; for a pre-shadow row, the in-place projection at the bound coordinates carrying the
 *    committed rev) with the projection carrier in place at the bound address;
 *  - `content.state: "unavailable"` — the row exists (binding/rev answered) but no committed carrier
 *    serves it: `"shadowless-legacy"` = no shadow copy exists and the bound projection cannot vouch
 *    (missing/divergent/foreign id); `"carrier-missing"` = the projection carrier is not in place at
 *    the committed address (deleted or squatted out-of-band) or the shadow cannot carry the
 *    committed rev — including every UNBOUND row (no committed address exists to mint
 *    `MemoryEntry.scope`/`slug` from; the shadow bytes, where present, stay readable via
 *    `readCommittedShadow`).
 */
export type CommittedEntrySnapshot = {
    state: "absent";
    id: string;
} | {
    state: "row";
    id: string;
    rev: string;
    binding: CommittedBinding;
    content: {
        state: "present";
        entry: MemoryEntry;
    } | {
        state: "unavailable";
        reason: "shadowless-legacy" | "carrier-missing";
    };
};
/**
 * design/178 v2-a §1.3 — the LEDGER-DRIVEN scope enumeration's return form. `rows` carries every
 * BOUND row whose `binding.scope` is in the requested set (the account is the enumeration authority
 * — a shadow-backed row whose projection was deleted out-of-band is still answered; a disk scan
 * would miss it). `unbound` is the STORE-LEVEL appendix of unbound row ids (they have no scope to
 * belong to — the enumerating consumer disposes of them explicitly: an export lists them as
 * residuals, an erasure scope-selector never resolves them). `complete: false` means a stable
 * committed snapshot could not be taken (the journal-quiescence fence stayed contended past its
 * bound) and the caller must FAIL CLOSED — a partial answer is never dressed as a full one; the
 * union is DISCRIMINATED on `complete` so the incomplete arm cannot even carry rows to misread.
 */
export type CommittedScopeSnapshots = {
    complete: true;
    rows: Array<Extract<CommittedEntrySnapshot, {
        state: "row";
    }>>;
    unbound: string[];
} | {
    complete: false;
};
/**
 * design/178 v2-a §1.3 — the per-id custody read's return form. `"damaged"` covers every chain whose
 * integrity is impeached while a sound reading still exists to answer with: a torn tail (crash
 * mid-append — the sound prefix IS the answer and the file is NOT touched: inspection and repair are
 * separate duties; the recovery leg quarantines the fragment, and an audit read that "helpfully"
 * rewrites the evidence it is auditing has destroyed its own subject), the evidence-loss cross-check
 * (a prev-bearing ledger row beside a chain with zero events for the id), and the ev-identity law
 * (one `ev` under two DIFFERENT payloads — the chain was spliced/corrupted out of band; the store's
 * own write path refuses to operate on it, so custody must never read it back as `"complete"`).
 * `reason` carries the impeachment detail when one is known (today: the ev-identity arm); its
 * absence means the damage form speaks for itself (torn tail / evidence loss).
 * `"capability-absent"` never originates here — it is the ENGINE's honest answer for a backend
 * without this capability face.
 */
export interface EntryCustodyReport {
    state: "complete" | "damaged";
    events: TransferEvidence[];
    reason?: string;
}
/**
 * design/178 v2-c §3 — what the ONE-LOCK export composite ({@link FileMemoryEngineBackend.exportSnapshotOf})
 * answers: every face of the store the bundle needs, read inside a single txn-mutex hold (see the
 * method doc for the fence law). Entries carry committed content only; a bound row whose committed
 * carrier cannot be served is enumerated under `contentUnavailable` instead (never silently
 * dropped, never dressed as content). `unbound`/`pendingLatch` are STORE-LEVEL residual sets (no
 * tenant to belong to); the bundle assembly decides whether their id lists travel (full-store
 * export) or only their counts (subset export). `challenges`/`lineage`/`pollutedSessions` are the
 * RAW governance faces — the (pure) bundle assembly slices them to the exported entry/session
 * sets; `custody` is already sliced (the slicing consumes row-level scope fields this module owns).
 */
export interface MemoryExportSnapshot {
    storeId: string;
    /** True ⇔ the requested scopes cover every registered scope (one trust domain — the residual id
     *  lists and unbound-deletion custody rows may travel). */
    fullStore: boolean;
    entries: MemoryEntry[];
    contentUnavailable: Array<{
        id: string;
        scope: string;
        slug: string;
        rev: string;
        reason: string;
    }>;
    unbound: string[];
    pendingLatch: string[];
    custody: TransferEvidence[];
    unsliceableCustody: number;
    challenges: ChallengeEvent[];
    lineage: Array<{
        entryId: string;
        sessionId: string;
        lastRev: string;
        lastAt: number;
        marked?: true;
    }>;
    pollutedSessions: Array<{
        sessionId: string;
        at: number;
        reason: string;
        cause?: import("./types.js").MemoryOriginCause;
    }>;
    quarantined: string[];
    quarantineOpaque: number;
}
/** Test hook (证伪式复审 L3): small constants so the crash-window leg does not really wait 15-30s. */
interface TxnLockTimings {
    staleMs?: number;
    waitMs?: number;
    stealGraceMs?: number;
}
/** Construction options for {@link FileMemoryEngineBackend}. */
export interface FileMemoryEngineBackendOptions {
    /** ⚠️ 分家坑(service 报告 2026-07-09):不显式传时,控制平面按无参 `resolveMemoryEngineRoot()`
     *  派生(env/默认 config 根)——若你的记忆目录带自定义根,控制面会与记忆目录分家。自建 backend 的
     *  consumer 应显式钉本字段(engine 同参)。 */
    controlDir?: string;
    /** 注入时钟(determinism,engine 同款);默认 `Date.now`。 */
    now?: () => number;
    /**
     * Where mount-time partition-key incidents go. The migration probes promise "never silent", and a
     * self-constructed backend has no other channel to keep that promise on — the runner-assembled path
     * bridges its own sink into `RunnerDeps.onError`, and without this seat the identical incident was
     * unobservable here. Absent ⇒ the probe stays silent (it is advisory; a mount never fails over it).
     */
    onIncident?: MemoryPartitionIncidentSink;
}
/**
 * ⚠️ DEPLOYMENT BOUNDARY (ruled 2026-07-09): this backend — and the
 * file memory engine's materialize/harvest — operates on the WORKER-HOST fs plane (node:fs), matching
 * CC's `~/.claude` host-plane memory (the only behavior anchor). It is NOT ExecutionEnv-aware: when a
 * deployment routes the model's file tools to a REMOTE sandbox (e2b/k8s/ssh/adb/local-docker), the two
 * planes do not intersect — the model cannot see the memory dir and harvest cannot see sandbox writes.
 * Remote-hands deployments MUST gate the engine off for non-host lanes (reference: service
 * `memoryEngineBackendFor` lane gate, fail-closed + `MEMORY_ENGINE_REMOTE_LANE=allow` override for
 * lanes that genuinely share the worker fs, e.g. ssh+NFS). A future "remote-memory-materialize" design
 * (ExecutionEnv-seam placement/harvest with sandbox-lifecycle contracts) is demand-triggered — do not
 * bolt it on here.
 */
export declare class FileMemoryEngineBackend implements MemoryBackend {
    /** The repo-level memory dir this backend is the authority for (engine reads it for zero-copy). */
    readonly directoryRoot: string;
    /** B3 — the engine control plane home (ledger/scopes/shadow/quarantine/journal). Model-invisible:
     *  NEVER under {@link directoryRoot}, always on the config-root side even for an in-repo memory dir.
     *  The engine picks this up (like `directoryRoot`) so both sides share ONE control plane. */
    readonly controlPlaneRoot: string;
    /** {@link directoryRoot}'s physical form at construction — the root every act on a store path is judged against. */
    private readonly physicalRoot;
    /** Injected clock (opus 1.258 LOW: match the engine's `this.now` determinism discipline). */
    private readonly now;
    /** The construction-time incident sink (partition adoption, layout adoption on the standalone faces). */
    private readonly onIncident;
    /**
     * The COMMITTED-rev ledger (`revs.json`, id → row; design/186 schema v2). Zero-copy makes it
     * load-bearing for CAS: the entry files are the SAME files the session edits, so "read the current
     * rev from the file" would make every legitimate update self-conflict (the file already holds the
     * new content). Each row records the rev as of the last COMMIT (applyPatches) — a CAS compares
     * against IT, so an in-session edit passes (baseRev == ledger) while a concurrently-committed
     * change conflicts (ledger advanced) — PLUS the projection BINDING (owning scope + slug): bytes
     * are model-plane material anyone can copy or relocate, so the account — model-invisible, outside
     * every file-tool root — is what holds ownership. "rev matches" alone is no longer a serve
     * credential; "rev matches AND the coordinates match the binding" is.
     * Read-side (`listHeaders`/`getByIds`/`search`) SYNCS the ledger to disk THROUGH the inbound gate
     * (B2 — out-of-session edits, e.g. a git pull into an in-repo memory dir, are secret/cap-scanned;
     * rejects are quarantined + shadow-restored, never adopted); the applyPatches lookup does NOT sync
     * (that would erase the very baseline CAS needs). B3: corrupt ⇒ {@link ControlPlaneCorruptError}
     * (fail-closed), never a silent empty-ledger downgrade.
     */
    private ledger;
    /** Top-level unknown keys of the loaded v2 envelope — write-preserved (§1.1). */
    private envelopeExtras;
    /**
     * §3.1 (r3-④) — the migration retry judge: the PERSISTED schema form, set at every real disk read
     * of the ledger and flipped only by the locked migration. Deliberately NOT derived from the
     * in-memory ledger shape: the v1 compat arm upgrades rows in memory (§3.2), so the memory shape
     * reads "already v2" after the first stand-down and the retry would die. `"v1"` arms the
     * per-entry opportunistic retry (construction / reads / checkControlPlane / every locked
     * mutation); undefined = the disk form has not been observed yet.
     */
    private persistedSchemaVersion;
    /** §3.1 — the v1-compat window is loud ONCE per instance (per-row unbound noise stays suppressed;
     *  the OVERALL state does not). */
    private announcedV1Compat;
    /** §3.4 — O(1) retry judge for the READ entries' unbound-convergence attempts: recomputed at
     *  every disk load and every persist (both already iterate the rows), never by scanning per read.
     *  Slightly stale-TRUE is harmless (one try-lock + one early-return reload); stale-FALSE cannot
     *  happen (every seat that changes the rows passes through a maintaining seat). While a blocker
     *  is genuinely stuck (a standing duplicate), each read pays one census — the same bounded cost
     *  the v1 read retry accepts for an incomplete store, in a state the sweeps warn about loudly. */
    private unboundRowsKnown;
    /** Adopting-face notice dedup (copy / undecidable-probe / ambiguous / census-incomplete): one
     *  still-pending state announces once per instance; changed bytes mint a new key and re-announce
     *  (same posture as the restricted finding keys, which are per-MOUNT — this face has no mount). */
    private adoptionNoticeKeys;
    /** Test seam (§7.4 r4-④ arm): force a post-commit-point transfer-append failure. */
    private transfersAppendFault?;
    /** Test seam (#262 ③/#191 review r1 fence pins): runs inside `recoverJournal` between the redo
     *  replay and the commit-boundary ownership re-assertion — a test flips the lock owner (or swaps
     *  the journal) here to exercise the mid-replay steal arms. Production leaves it unset. */
    private recoveryCommitProbe?;
    /** Test seam (#262 ③/#191 review r2): runs inside the torn-tail heal between the marker work and
     *  the pre-truncation ownership re-assertion — a test flips the lock owner here to prove the
     *  chain is never truncated after an ownership loss. Production leaves it unset. */
    private chainHealProbe?;
    /** Test seam (v2-c §3-2 fence pins): runs INSIDE the export composite's mutex hold, after the
     *  data reads and before the second fingerprint — a test injects a governance-face write here to
     *  exercise the drift-retry and the three-round refusal. Production leaves it unset. */
    private exportFaceProbe?;
    /** v2-b §3-4 — the resurrection backstop's chain digest, keyed by the evidence log's stat
     *  fingerprint (size + mtimeMs — append-only growth and a torn-tail shrink both move `size`).
     *  NEVER a mount-lifetime cache: the fingerprint is re-verified under the txn mutex on every
     *  consultation, so a sibling process's erasure invalidates it (r3: a B-process erasure must not
     *  leave an A-process adopting off the old deleted-id set). The digest carries its chainState —
     *  a damaged read caches as damaged, never as a bare (falsely reassuring) id set. */
    private deletedIdsDigest;
    /** B2 — inbound-gate findings accumulated by read-side syncs; the engine drains them into
     *  `HarvestReport.inboundFindings` at harvest. */
    private inboundFindings;
    /** Batch-planning scan memo (applyPatchesLocked only): planning reads pre-batch disk exclusively,
     *  so one `readScope(scope, false)` per scope serves every patch of the batch. Set for the planning
     *  loop, cleared before EXECUTE mutates the disk. Never caches `sync:true` reads. */
    private batchScan;
    constructor(dir: string, opts?: FileMemoryEngineBackendOptions);
    /** The adoption under a held transaction lock (before the ledger migration, whose census derives
     *  paths from the registry); the heartbeat refreshes the lock and re-proves it before every action. */
    private adoptLayoutLocked;
    /** The seat at the head of every lock-free public face: adopt under a NON-BLOCKING try-acquire. A live
     *  holder stands this caller down (its locked faces, or the next mount, adopt); the read that follows
     *  refuses fail-closed on a v1 registry rather than serving a layout it cannot describe. */
    private ensureLayout;
    /** The engine's mount seat: adopt under the transaction lock (`ownerCandidate` = the write scope; `onIncident` = this call's sink). */
    adoptRootScopeLayout(opts?: {
        ownerCandidate?: string | null;
        onIncident?: MemoryPartitionIncidentSink;
    }): Promise<void>;
    /** B3 fail-closed preflight (engine harvest duck-types this): force-reload every load-bearing
     *  sidecar from disk; throws {@link ControlPlaneCorruptError} when one is corrupt. Also completes
     *  any interrupted transaction (H4 recovery). */
    checkControlPlane(): void;
    /** B2 — drain the accumulated inbound-gate findings (engine harvest reporting). */
    drainInboundFindings(): HarvestRejection[];
    /** B1 — read an entry's COMMITTED content copy from the control-plane shadow (undefined = none).
     *  The engine's missing-file recovery reads this (zero-copy: a deleted file is gone from the
     *  directory scan, so the shadow is the only committed copy left). */
    readCommittedShadow(id: string): string | undefined;
    private shadowPath;
    private loadLedger;
    /** B3: atomic + fail-closed — a ledger that cannot be persisted throws (the commit is incomplete
     *  and the caller must know), never a silent swallow. Always writes the v2 envelope; writing over
     *  an unmigrated v1 disk is refused loudly (mutations migrate FIRST — §3.1's "变异必先迁" is an
     *  invariant here, not a convention: a v2 write minus the census would strand every row unbound). */
    private saveLedger;
    /** v1-journal restore ONLY (§1.2 dual-read): the snapshot predates the upgrade and is persisted in
     *  ITS OWN form — the migration (which runs after recovery, under the lock) owns the v1→v2 swap
     *  together with its backup + evidence protocol. */
    private persistLedgerV1;
    /** Fail-open announcement convenience (S2-B queue posture — an unwritable queue never fails the
     *  operation that had something to say; the fail-CLOSED evidence lane is `transfers.jsonl`). */
    private enqueueExternalItems;
    /** One-shot dedup for adopting-face notices (see {@link adoptionNoticeKeys}). */
    private announceAdoptionNotice;
    /**
     * §3.4 — the standing unbound-row disclosure, in ONE spelling for BOTH faces that carry it (the
     * harvest preflight `checkControlPlane` and the restricted sweep `auditRestrictedDivergence` used
     * to hold drifting hand-built copies of this sentence; the sweep's copy had lost the remediation
     * clause, which is if anything MORE apt there — a restricted session has no write channel of its
     * own to converge the row).
     *
     * De-duplicated against what is ALREADY QUEUED, not against a per-instance latch. The repeat this
     * closes was not merely noise: the queue is bounded ({@link MEMORY_ANNOUNCEMENTS_MAX}), so a
     * long-lived instance with one stuck row folded REAL notices out of it to make room for copies of
     * this one. But the latch the sibling adoption notices use ({@link announceAdoptionNotice}) is the
     * wrong instrument HERE, twice over, and both were found in review:
     *  · a backend instance is a RUNNER-level dependency reused across every session it serves, so a
     *    per-instance latch would announce the standing condition to the first session and then hold
     *    its peace forever — turning a flood into silence, against this disclosure's own contract
     *    (「the STATE does not go silent」). The queue is DRAINED at each session's first injection, so
     *    "is an identical line still pending?" is exactly the right question: no repeat within a
     *    session, one fresh statement in every session that has not yet been told.
     *  · a latch key would have to encode the id set, and ledger row ids are arbitrary external
     *    strings (a comma is legal) — `join(",")` makes `["a,b","c"]` and `["a","b,c"]` the same key,
     *    so a real change of state could be suppressed. Comparing the RENDERED LINE has no such seam:
     *    two states that render the same sentence are, to every consumer, the same statement.
     *
     * The test runs INSIDE the queue's own locked read-modify-write
     * ({@link enqueueMemoryAnnouncement}'s `supersedesPrefix`), not as a peek followed by an append: a
     * peek-then-enqueue would race a concurrent drain, and two processes could both peek-miss and both
     * append. Fail-open like every announce site — an unwritable queue never fails an operation that
     * had something to say.
     */
    private announceUnboundRows;
    /** Resolve a binding's absolute projection path WITHOUT registering anything (probes must stay
     *  side-effect-free; `scopeDir` would register an unknown scope name from a hand-edited row). */
    private bindingAbsPath;
    private bindingRelPath;
    /**
     * §3.3/§2.2 — the strict projection census: EVERY id-bearing file under EVERY registered scope,
     * with `{complete}` honesty. Absence is ENOENT-only AT EVERY LEVEL — deliberately NOT
     * `scanEntryFiles`, whose root arm folds an unreadable scope root into "empty scope" (`onSkip`
     * fires for non-root only): copied here, one transient root EACCES would turn "only B was
     * visible" into "only B exists" and mint a permanent wrong binding (bound rows are never
     * re-derived). Symlinks / depth overflow / non-.md are exclusions by the entry-scan contract,
     * not census gaps.
     */
    private censusProjectionsLocked;
    /** §3.6 — capture the pre-migration v1 bytes exactly once (`wx`: the first scene is never
     *  overwritten by a retry). */
    private writeV1BackupOnce;
    /** §3.1 — non-blocking txn-mutex acquisition for the opportunistic migration entries (the
     *  constructor cannot await). LIVE contention stands down — wait semantics belong to the async
     *  writers; every entry retries later. A STALE lock (past its deadline, owner token present) gets
     *  ONE synchronous single-winner steal, same judge and same token discipline as the async
     *  waiter's steal: without it, a crashed holder's leftover lock would stand down every sync entry
     *  forever, and a READ-ONLY deployment — which never runs an async writer to do the stealing —
     *  would sit in v1-compat indefinitely (the very suspension §3.1 bounds to seconds). Steal-safety
     *  is unchanged: the caller runs journal recovery INSIDE the acquired lock before touching state. */
    private tryAcquireTxnLockSync;
    /** §3.1 — one opportunistic migration attempt (read entries / construction / checkControlPlane):
     *  try-lock, recover, migrate, release. A busy lock is a clean stand-down (the mutation entries
     *  bootstrap the migration under their own lock regardless). */
    private tryOpportunisticMigrate;
    /**
     * §3.3-§3.5 — the migration body. MUST hold the txn mutex. Reloads the DISK truth (the retry
     * judge is {@link persistedSchemaVersion}, set by that reload — never the in-memory shape), then:
     * v1 ⇒ full census-derived rebind + one-shot backup + journaled swap + one migration-summary
     * evidence event; v2-with-unbound ⇒ re-derive the unbound rows only (bound rows are FINAL — only
     * explicit transactions rewrite a binding), each new bind carrying a `migration-bind` evidence
     * event. An INCOMPLETE census abandons the pass with ZERO writes ("only what was visible" is not
     * "all that exists"; a wrong binding is worse than no binding) and retries at the next entry.
     * P=0 rows stay unbound (「缺失≠删除」— dropping the row would endorse an unattributed delete);
     * P≥2 rows stay unbound (the migration never adjudicates which projection is real — an existing
     * dup audit keeps ringing until a harvest or a human resolves it). Idempotent: v2 + zero unbound
     * is a no-op version check.
     */
    private migrateLockedIfNeeded;
    /** Is the durable chain-degradation marker in place? Presence is ENOENT-only-absent (a probe
     *  failure is never read as "not degraded" — fail-closed). */
    private chainDegradedMarkerPresent;
    /** §3-4 r6 — mint the chain-degradation marker DURABLY (wx + fsync data + fsync DIRECTORY +
     *  read-back verification). Idempotent on an existing marker. Throws when the marker cannot be
     *  proven on disk — the caller (the heal) must then ABORT with the chain untouched: truncating
     *  first would reopen the exact window this marker closes ("chain reads complete, no marker"
     *  after an out-of-band tear). */
    private mintChainDegradedMarker;
    /** backlog #270 — make a just-created control-plane directory ENTRY power-loss durable: open the
     *  control-plane root and fsync it. Fail-closed on failure: the one caller (the marker mint) must
     *  abort its heal with the chain byte-untouched — an unproven marker must never authorize a
     *  truncation, and the abort leaves the SAFER state either way (the standing marker or the still-
     *  visible tear both re-enter the heal on the next chain read). Platform notes: Node's fsync maps
     *  to fsync(2) — on macOS a full power-loss guarantee would additionally need F_FULLFSYNC
     *  (fcntl), which Node cannot reach; this is the same accepted bar as every other fsync in this
     *  store. On win32 a directory cannot be opened as an fd (fs.open rejects directories there), so
     *  the entry-durability half is structurally out of reach — an explicit documented arm, not a
     *  swallowed error; NTFS metadata journaling is the (weaker) cover that platform gets. */
    private fsyncControlPlaneDir;
    /**
     * Read the WHOLE evidence chain in order (validated, collision-checked), healing a TORN TAIL (a
     * crash mid-append): the fragment is quarantined for evidence, the sound prefix stands (lines are
     * self-contained — damage never spreads backward). Any NON-tail corruption is fail-closed (B3
     * family). Two heal forms (§3-4 r5/r6):
     *  - transaction journal PRESENT beside the tear = the honest crash form — the committed-pending
     *    protocol re-appends the lost row from the journal, so the healed chain is genuinely complete;
     *  - journal ABSENT = out-of-band tail damage — the lost row is unrecoverable, so the heal mints
     *    the durable {@link CHAIN_DEGRADED_FILE} marker BEFORE it truncates (write order is
     *    load-bearing: isolate fragment → marker wx + read-back → only then truncate; a marker
     *    failure aborts the heal with the chain byte-untouched).
     * Same-`ev` collision law (§4.4, read-time arm): canonical-form-equal duplicates are tolerated
     * (append-idempotency residue); DIFFERENT payloads under one ev are fail-closed — an event id is
     * one identity, and a reader must never pick a side by file order.
     *
     * `fenceToken` (#262 ③/#191 review r1) — this "read" MUTATES on a torn tail (quarantine + marker
     * + truncation), so a caller whose entry assert is far behind it (the recovery leg's validation
     * reads sit after unbounded parsing work) passes its lock token and the heal re-verifies
     * ownership immediately before its first mutation: a recoverer stolen from mid-validation must
     * not truncate a chain the stealer is appending to. Callers whose entry assert is adjacent keep
     * their documented assert→act residue and may omit it.
     */
    private readTransferChain;
    /** The chain's ev → canonical-payload map (§4.4 upgraded load shape — the append path compares
     *  PAYLOADS, never mere membership). */
    private readTransferEvents;
    /**
     * v2-b §3-4 — the deleted-id digest behind the resurrection backstop: every `delete` evidence
     * row's id (LOCAL AND IMPORTED — an origin filter here would disarm exactly the cross-store
     * anti-resurrection evidence a custody import carries; the origin distinction belongs to the
     * replay/anchor judgments alone), read WITHOUT healing (a backstop that rewrites the chain it
     * consults has destroyed its own evidence; a torn tail answers `"damaged"` with the file
     * byte-untouched). Non-tail corruption stays the fail-closed throw. Unknown channels are
     * tolerated (open-form law). Must be called with the txn mutex held — the stat fingerprint is
     * the cross-process staleness judge, and holding the lock is what makes "fingerprint unchanged
     * ⇒ digest current" sound.
     */
    private deletedIdsDigestLocked;
    /**
     * v2-b §3-4 — the CHAIN-LEVEL resurrection backstop's verdict for one NEW id-bearing disk file
     * (an id with no committed account row) meeting the adopting read:
     *  - `"erased"` — the chain carries a delete evidence row for the id: the bytes are a deleted
     *    entry resurfacing (whatever journal shape produced the "account gone, disk copy back"
     *    scene), refused + quarantined — restoration is an EXPLICIT write with a new id, never a
     *    disk replant;
     *  - `"degraded"` — the chain cannot prove the deleted set (durable degradation marker, or a
     *    live torn tail): "the id is not on the chain" is no evidence on an incomplete chain, so
     *    EVERY new id-bearing file is refused adoption (left on disk, not served) until the explicit
     *    rebuild clears the state — the same epistemic rule a custody import's completeness
     *    precondition applies;
     *  - `"clear"` — adoption proceeds normally.
     * `applyPatches` adds are NOT consulted here (host authority — an explicit add of a previously
     * erased id is a legitimate new commit).
     */
    private resurrectionBackstopVerdict;
    /** v2-b §3-4 — the LOCK-FREE retrieval face of the deleted-id set (retrievalView / batch-plan
     *  locateById). Reuses the fingerprint-cached digest: a deleted id's replanted bytes never reach
     *  the retrieval face either. Best-effort by construction — a lock-free reader carries the same
     *  cross-process staleness the whole retrieval face does, and a degraded chain filters only its
     *  known deleted set (it never refuses-all here — retrieval must stay a plane report). */
    private retrievalDeletedIds;
    /** v2-b §3-4 (round-3 #1) — a PURE, fresh read of the committed ledger rows for the retrieval
     *  backstop's account check: bytes → parsed rows, installing no read cache and no schema memo — so
     *  the guard's account view is as fresh as its chain digest,
     *  never a stale instance cache paired with a fresh deletion read. A v1-form or absent ledger
     *  answers empty (a v1 store has no delete rows to police anyway). FAULT DIRECTION, stated
     *  truthfully (rescan corrected the original note, which claimed the opposite): a read/parse
     *  FAULT also answers empty, and with an empty account the consuming filter WITHHOLDS every id
     *  on the delete list — conservative on purpose (a host re-add cannot be distinguished from a
     *  replant over an unreadable account, and serving bytes that carry delete evidence during a
     *  fault window would reopen exactly the resurrection the backstop exists to close). No erase
     *  CLAIM is minted (nothing is attested); entries with no delete evidence keep serving. The
     *  fault is DISCLOSED on the announcement lane (坏值响亮度 — the pre-fix swallow was silent),
     *  once per fault code per mount. */
    private freshCommittedRowsPure;
    /** §1.1 commit-point honesty: any failure BEFORE the journal write must refuse the whole
     *  transaction cleanly — so the append target's writability (and the log's soundness, ev-collision
     *  law included) is probed here, never discovered after the point of no return. */
    private precheckTransfersAppendable;
    /** Append evidence events, idempotent by `ev` (the recovery leg re-runs this for a journal whose
     *  append crashed). Rows land in CANONICAL form (sorted keys) so idempotency and collision checks
     *  compare one representation across writers and re-serializing middle layers. Three collision
     *  checks (§4.4): read-time (inside {@link readTransferChain}), batch-time (one batch minting two
     *  identities for one ev is a minting bug — fail-closed), append-time (an ev already on the chain
     *  with a DIFFERENT payload is corruption, named; equal = idempotent skip). Behavior narrowed,
     *  disclosed: the pre-v2b reader silently skipped/overwrote same-ev rows regardless of payload.
     *  The ONLY failure class here is I/O — never capacity: a size threshold would freeze legitimate
     *  moves behind a full log, so growth is disclosed, not refused. A failure AFTER the commit point
     *  is COMMITTED-PENDING (the caller keeps the journal and throws — calling it "refused" would
     *  invite a retry of a transaction the next recovery completes anyway). */
    private appendTransfers;
    /**
     * @param opts.unlocked RB-447 (2026-07-31, hardening review) — set by the two callers that run
     * WITHOUT the txn mutex (the constructor and {@link checkControlPlane}). `applyPatches` serializes
     * reload→plan→journal→execute→ledger behind a cross-process lock precisely because two writers over
     * one root would otherwise interleave; these two ran the recovery and the staged sweep with no lock
     * at all, and both are DESTRUCTIVE. A second process merely CONSTRUCTING a backend on the same
     * memory dir — which happens at every task start — while process A sat between STAGE and EXECUTE
     * removed A's `.stage-*` tmps, so A's `renameSync(op.staged, op.target)` threw ENOENT and the engine
     * converted a healthy transaction into a `sidecar_corrupt` incident. Dropping A's journal has the
     * worse shape: if A then crashed, its roll-forward record is gone.
     *
     * The gate is the txn mutex ITSELF, not a heuristic: an unlocked caller stands down only while
     * someone else is holding a LIVE lock (present and not past its stale line), because that is
     * precisely the window in which the journal and the `.stage-*` files belong to a transaction in
     * flight. With no live holder — a genuine crash, or the ordinary cold start these two callers were
     * written for — recovery and the sweep run exactly as before, so the documented "a crash is rolled
     * forward/back on the next construction" behavior is unchanged. A live holder's own transaction
     * finishes its journal itself; if that holder dies, the lock goes stale and the next construction or
     * the next locked `applyPatches` recovers it. Nothing is deferred indefinitely.
     */
    private recoverJournal;
    /** RB-447 — is another writer mid-transaction right now? True while the txn lock dir exists and is
     *  not past its stale line (a stale lock is a crashed holder: its artifacts ARE ours to recover).
     *  Only consulted by the two UNLOCKED callers — inside `applyPatches` the lock is ours by
     *  construction. Cheap: one `stat` on the lock dir, no lock acquisition, never blocks.
     *  §3.1 ⓪ (r5-①): "no lock" is ENOENT-only — a stat FAILURE folded into "nobody is committing"
     *  let a transient EIO run the unlocked recovery (and its staged sweep) over a live transaction. */
    private txnInFlight;
    /**
     * The act gate for store paths ({@link storeTargetEscape}) over `ops`' writes and deletes — shadow
     * ops name control-plane files, not store paths. Refuses with `memory.target_outside_store` at the
     * first op that would land outside the store, naming the op and where it resolves. Runs over a
     * whole batch before its commit marker (no journal is written; a caller that staged files removes
     * them) and per op at the act itself; past the marker a refusal keeps the journal, which replays
     * once the path resolves inside the store again.
     */
    private refuseEscapingTargets;
    /** At the head of every face (both recovery legs, the lock-free faces' layout seat, the adoption face):
     *  the store root still resolves to the directory this backend opened. A root re-pointed since would
     *  carry every read, adoption, sweep and restore — not only the journal's acts — into another directory. */
    private assertRootIntact;
    /** Remove orphaned `.stage-*` files (a crash BEFORE the journal rename = clean rollback: nothing
     *  was committed, the staged tmps are litter). Dotfiles are invisible to entry scans regardless. */
    private sweepStagedFiles;
    private scopeDir;
    /** B2/C-F9 — contain one inbound-gate REJECT, shared by the adopting read (`readScope` sync) and
     *  the adoption-restricted committed view (`readScopeCommitted`) so the two doors can never drift:
     *  report the finding, capture the SCANNED bytes into control-plane quarantine (never a path
     *  re-read — TOCTOU), restore the committed shadow over the suspect where one exists (the write
     *  lands by `rename`, which REPLACES a link at the final component instead of traversing it —
     *  pinned by test), and escalate any deviation as a report-visible `quarantine_failed`. Returns
     *  the shadow text that now backs the entry (undefined ⇔ never committed / shadow-less legacy —
     *  each caller decides what leaving the set means on its channel). */
    private containInboundReject;
    /** B2 inbound gate for content that did NOT come through applyPatches (git pull / hand edits /
     *  a hand-seeded file): secret + cap + structure. Returns the rejection, or undefined (clean). */
    private inboundGate;
    /** Read every COMMITTED entry of `scope` — a file must carry a valid `id:` to count. An id-less
     *  file is UNCOMMITTED session material: it becomes an entry only through the engine's harvest
     *  gates (which mint the id on commit). Reading it as an entry here would let gate-rejected files
     *  (secret / cap / quarantine) leak into the authoritative set through the zero-copy disk scan.
     *  `sync` (read-side callers) adopts a committed file's CURRENT disk rev into the committed-rev
     *  ledger ONLY through the inbound gate (B2: out-of-session edits, e.g. git-pull, are scanned;
     *  a reject is moved to control-plane quarantine and the committed shadow content is restored to
     *  disk). The applyPatches lookup passes false (the ledger must keep the baseline for CAS).
     *
     *  Ledger-write discipline: the sync=true adoption path WRITES `revs.json`, and that write is
     *  wholesale (atomic replace) — unserialized against applyPatches' transaction it is a
     *  lost update (the later writer drops every row the earlier one added). sync=true is therefore
     *  reachable ONLY through {@link readScopeAdopting}: `probeOnly` runs the scan with NO ledger
     *  side effects and returns undefined at the FIRST entry that needs an ACCOUNT transaction (an
     *  edit adoption, or a move candidate — the caller re-runs the scan under the txn mutex against
     *  a freshly reloaded ledger); a completed probe pass returns the entries. A COPY or an
     *  undecidable binding probe needs no transaction (withheld + announced, account untouched), so
     *  the probe classifies those inline and keeps going — a store with a lingering copy never
     *  contends for the writer lock on every read. */
    private readScope;
    /** #1076b — the observation-table door of the adopting read: bytes a session recorded appearing
     *  while it held this layer read-only are never adopted. Gate-failing bytes are not taken here —
     *  the inbound gate's containment keeps its door. A hit is announced, the committed copy stands in
     *  when this path is the entry's bound projection, and the disk is left as found (the account it
     *  is judged against is current — the scan sees to that). The notice is deduplicated against what
     *  is still queued, so every session that meets the file is told once. Returns whether the file
     *  was settled (the caller moves on). */
    private settledAsObserved;
    /**
     * design-A RB-5 ([A3]) — the read-side whitewash gate, shared by the bound-here divergence lane
     * and the move+edit lane so the two can never drift: an out-of-session disk change (git pull /
     * hand edit) of a repo-ingested entry that strips `provenance`, or strips a present `trust`
     * marker, is the same whitewash applyPatches' I-2 clause refuses — adoption must not be the
     * laundering back door. Baseline = the COMMITTED state (control-plane shadow; the disk file is
     * the very thing under suspicion). A shadow-less legacy entry has no independent baseline, so the
     * gate cannot fire for it — but every adoption/commit backfills the shadow, closing that window
     * on first contact. `containment` keeps the reason honest per lane (the move lane quarantines
     * WITHOUT restoring at the never-bound path).
     */
    private whitewashInboundFinding;
    /**
     * §2.2/§2.3 — one MOVE candidate under the txn mutex: a committed entry's bytes at non-bound
     * coordinates whose bound projection is CONFIRMED absent (ENOENT, probed under this same lock).
     * Adoption needs all three conditions — candidate present + bound path ENOENT + store-wide
     * uniqueness through a census that proves its own completeness: with B and C both planted,
     * single-condition ENOENT would let READ ORDER pick the owner; with a half census, "only B was
     * visible" is not "only B exists" (r4-③/r5-②). Gate-clean adoption is an EXPLICIT transaction:
     * row rebind + `prev` trace in one atomic ledger write, an `adopted-move` evidence event in the
     * same journaled commit, and a loud announcement — external move/rename is the writeScope-null
     * plane's PRIMARY legitimate input channel (git pull), so it is upgraded to a traced transaction,
     * never refused wholesale. Candid boundary (D-1, ruled): a byte-identical delete-then-plant is
     * THIS SAME disk observation — the adoption is a TRACED acceptance, not a rejection; what v2
     * removes is the delete's invisibility (missing now rings), the relocation's tracelessness
     * (transfers row + prev), and the silence. Out-of-band writers do not take this lock, so a
     * TOCTOU window between the ENOENT re-verification and the commit remains for an adversary that
     * can recreate the original mid-window — that actor is the delete-then-plant actor (same candid
     * boundary), not a new capability; the tri-state probe closes FAULT misjudgment, and claims no
     * more. Returns the entry to serve, or undefined (refused/deferred this pass).
     */
    private adoptMoveCandidateLocked;
    /**
     * The adopting read: {@link readScope} sync=true with its ledger write serialized behind the
     * SAME cross-process txn mutex applyPatches holds (acquisition/steal/timeout semantics shared,
     * never duplicated). Without it, an adoption landing beside a sibling process's commit is a
     * lost update: both read the ledger, both write it wholesale, the later atomic write drops the
     * earlier writer's rows (the entry file stays on disk but the ledger no longer knows it).
     *  - PROBE first (no lock): disk agreeing with the ledger is the common case, and that pass writes
     *    no LEDGER row — the read stays lock-free and never contends with a writer over the ledger.
     *    (Precision, merged-code scan: the non-divergent branch may still BACKFILL a missing shadow
     *    file for a committed entry, an unserialized control-plane write with a narrow pre-existing
     *    last-writer window against a sibling's commit — see the shadow-backfill site below; that
     *    window predates this probe and is the same one the v2 single-lock design closes.)
     *  - Divergence ⇒ acquire the mutex, finish any interrupted transaction (the steal-safety
     *    contract: a stolen stale lock is safe BECAUSE recovery runs inside the lock), RELOAD the
     *    ledger from disk (the probe's cached view is exactly the stale baseline that loses
     *    updates), then re-run the scan with adoption and save inside the lock. The save re-verifies
     *    lock OWNERSHIP first (a holder stalled past the stale deadline was stolen from — its
     *    snapshot is behind the stealer's commit and must abort, exactly like the applyPatches
     *    journal write).
     *  - A lock that cannot be acquired throws (same loud arm as the write path): an adoption that
     *    cannot be serialized must surface, never silently skip the gate or serve stale state as
     *    adopted.
     * No self-deadlock: nothing under the lock re-enters acquireTxnLock — the scan's quarantine /
     * shadow / announcement writes use their own per-file sidecar locks (whose holders never take
     * the txn mutex), and applyPatchesLocked only issues sync=false reads. The two UNLOCKED
     * recovery observers (constructor / checkControlPlane) are unaffected: they already stand down
     * while a live holder exists ({@link txnInFlight}), which now covers adoption holds too.
     */
    private readScopeAdopting;
    /**
     * design/376 slice ③ (#437③) — the File leg's scope-enumeration capability face: every scope
     * this store has hosted, derived from the store root's durable scope registry (the control
     * plane's `scopes.json` — the one source that can invert a scope's sanitized directory label
     * back to its name; a listing of raw subdirectories could not). Side-effect-free committed
     * read; a corrupt/unreadable registry THROWS its fail-closed refusal — enumeration over an
     * untrustworthy registry must never answer an empty (= clean-looking) list. Honest residual,
     * disclosed: the registry records scopes as they are first touched THROUGH this control plane,
     * so a hand-assembled tree's never-touched scope dirs appear here only once a caller first
     * names them — store-level fact, session-roster-free either way.
     */
    listScopes(): MemoryScopeEnumeration;
    listHeaders(scopes: readonly string[]): Promise<MemoryEntryHeader[]>;
    private listHeadersFrom;
    getByIds(ids: readonly string[]): Promise<MemoryEntry[]>;
    private getByIdsFrom;
    /**
     * design/178 §3 (ruled 2026-08-08) — a projection of this backend for the session's RETRIEVAL
     * face (`memory_search` / `memory_get`): NO ADOPTION, ever.
     *
     * The ordinary read path carries the inbound-adoption channel: a disk revision that disagrees with
     * the ledger is judged an out-of-session change, gated, and — when it passes — adopted as the new
     * COMMITTED baseline (ledger + shadow rewritten). That is a deliberate lifecycle synchronization
     * point, and it belongs to the engine's own moments (materialize / harvest), not to every model
     * lookup: a retrieval landing between them would otherwise COMMIT the session's own in-flight edits
     * with no harvest gate and no pollution discipline in the way.
     *
     * Not running the inbound gate on this path is the point, not a concession: the view reports the
     * plane, and admitting anything stays the harvest's job. Stated exactly (v2-b trued this doc up —
     * the pre-v2b sentence "resolves entries from disk without touching the ledger" is no longer the
     * whole truth):
     *  · the v2-b RESURRECTION GUARD consults the delete-evidence digest (`transfers.jsonl` stat +
     *    parse) and — when any delete row exists — a PURE fresh read of the committed ledger, and it
     *    WITHHOLDS an on-plane file whose id carries a delete row with no live account row (an erased
     *    id's replanted bytes are not served through any read face; a host re-add serves). So the
     *    view is "the plane minus proven-erased ids", and it can throw {@link ControlPlaneCorruptError}
     *    from a fail-closed corrupt evidence log — states a pre-v2b view could not reach;
     *  · the reads do no healing and no quarantine, and the COMMITTED-ROWS read is cache-free; the
     *    digest read installs the instance fingerprint cache (a read cache, not a store write). The
     *    path's write-capable hooks (rescan r3 — do not present this list as shorter): `readScope`'s
     *    pre-existing opportunistic v1→v2 migration retry (v1-form/unbound-row stores only, design/186
     *    §3.1), the guard's fault-disclosure enqueue (a ledger read/parse fault mints an announcement
     *    row, once per fault code per mount), and `scopeDir`'s registry mint (listing a scope name the
     *    registry has never seen writes its row — predates this view and rides every scope read).
     *
     * WRITES ARE REFUSED, loudly — a view handed to a write path is a defect, not a fallback.
     */
    retrievalView(): MemoryBackend;
    /** #1076b — the lock-free retrieval face serves no recorded divergence either (side-effect-free, so
     *  no notice): the committed copy stands in at the entry's bound projection, anything else is
     *  withheld. The table is read per call; the account only when it names one of these ids. */
    private withoutObserved;
    /** The committed rows read afresh from disk (no cache install), or undefined when the account
     *  cannot be read — a fault, never folded into "no rows". */
    private committedRowsOrFault;
    /** Dedup keys for divergence findings the restricted view already recorded: one materialize reads
     *  a scope several times (listHeaders, then getByIds per scope), and a checkpoint-cadence harvest
     *  re-audits the same still-pending state — without the key, one untouched divergence would mint
     *  a finding per read. Per AUDITED MOUNT (design/186 §6): the keys live and die with the mount,
     *  same lifecycle as the id-less scope/baseline pair — one mount discloses a still-pending
     *  divergence once, and the NEXT audited mount (a later restricted session in this process, or a
     *  cross-process resume) re-discloses once, which is the desired "still pending" notice; the
     *  in-process and cross-process postures agree. A divergence whose bytes CHANGE mints a new key
     *  and is reported again within the mount. */
    private restrictedFindingKeys;
    private recordRestrictedFinding;
    /** Id-less audit state for an adoption-restricted session's WRITE scope. Armed only by the
     *  audited mount, which carries the caller's explicit session verdict together with the session's
     *  write scope — never inferred from plane structure (a writeScope-null layering has no write
     *  scope and is never given this surface: id-less files there are ordinary working files or
     *  external drops). The baseline is the set of id-less rel paths already under the scope at the
     *  first audited read after the mount (= materialize time): a pre-existing id-less file cannot be
     *  attributed to this session and stays unflagged, while a session under the restriction verdict
     *  has no sanctioned write channel into the store — so an id-less file that APPEARS after the
     *  baseline arrived out of band by construction and is worth naming. Per-process like the finding
     *  keys; when restricted sessions interleave on one instance the later mount re-baselines, which
     *  degrades toward silence, never toward a false alarm. */
    private restrictedIdlessScope?;
    private restrictedIdlessBaseline?;
    /**
     * The ADOPTION-RESTRICTED read of one scope: serve the COMMITTED account (ledger + control-plane
     * shadow) and never advance it. This is the tamper boundary for a session with no write intent:
     * the ordinary `readScope(sync=true)` ADOPTS disk divergence as the new committed baseline
     * regardless of session intent, so a byte landed by a side channel (e.g. a shell write under a
     * session whose persistence is declared unavailable, or into a read-only layering) would be
     * committed by the very session that was told it cannot persist — its own checkpoint-resume
     * materialize included. Here instead:
     *  - disk rev == committed rev ⇒ served as-is (it IS the committed content);
     *  - divergence whose bytes equal the committed SHADOW ⇒ served as-is: the shadow is written at
     *    the applyPatches commit point, so matching it is transactional proof that a concurrent
     *    writable session committed this state and only this call's ledger snapshot raced it —
     *    never treated as tampering;
     *  - other divergence of a shadow-backed entry ⇒ the SHADOW (committed content) is served; the
     *    divergent disk bytes stay in place UNLESS they fail the inbound gate (the existing
     *    quarantine door, unchanged under restriction). Scan-clean divergence is never quarantined,
     *    overwritten, or adopted — it stays on disk as deferred external state a later unrestricted
     *    session adjudicates normally;
     *  - a divergent entry with NO shadow (shadow-less legacy), and a never-committed id-bearing
     *    file, have no committed content to serve ⇒ WITHHELD with a loud finding, never served from
     *    disk, never adopted.
     * The ledger is reloaded from disk on every call (a stale cache would misread a concurrent commit
     * as divergence) and NEVER written here; the only writes this path may perform are the
     * gate-failing containment above and a shadow backfill for a rev-matching entry (recording
     * committed content — the account itself does not move).
     *
     * `audit` picks the mount: true = the engine's materialize/harvest boundary (findings recorded,
     * gate-failing divergence contained); false = the retrieval-tool face (a PURE committed read —
     * no findings, no containment, no writes: a model lookup must be side-effect-free).
     */
    private readScopeCommitted;
    /** Does `id` have a projection file anywhere in the store OTHER than `excludeAbsPath`? Returns
     *  that projection's rel path, or undefined. Early-exits on the first hit; only consulted on the
     *  rare divergent-with-shadow path (the scope-binding guard above) for UNBOUND rows — bound rows
     *  answer the same question in O(1) from the account binding (design/186 §2.4). RETIREMENT
     *  CONDITION: unbound rows are a migration transitional; when they are gone this scan form has no
     *  caller left and retires WITH them (delete the function, not just the call — a dead scan arm is
     *  the residue class the retirement registry exists for). */
    private idProjectionElsewhere;
    /**
     * The ADOPTION-RESTRICTED projection of this backend (see {@link readScopeCommitted}) — what a
     * session with no write intent materializes and retrieves through. The CALLER decides restriction
     * (an explicit verdict: a read-only layering, or persistence declared unavailable) and mounts this
     * view; the backend never guesses a session's intent from its own state.
     *  - `audit: true` (the engine's materialize/harvest mount): divergence findings are recorded for
     *    the harvest report + announcement queue, and gate-FAILING divergence is contained by the
     *    existing quarantine door; `writeScope` (when the session has one) additionally arms the
     *    id-less baseline for that scope — see {@link auditRestrictedDivergence};
     *  - `audit: false` (the retrieval-tool mount): a pure committed-view read — no findings, no
     *    containment, no writes, no baseline.
     * WRITES ARE REFUSED loudly — same posture as {@link retrievalView}.
     */
    restrictedAdoptionView(opts?: {
        audit?: boolean;
        writeScope?: string | null;
    }): MemoryBackend;
    /**
     * The harvest-boundary divergence sweep for an adoption-restricted session (the engine's
     * zero-admission harvest arm calls this BEFORE draining findings): re-run the committed-view audit
     * over the session's scopes — bytes landed AFTER materialize (a mid-session shell write) are
     * detected here, at the very boundary where an unrestricted harvest would have admitted them —
     * then check the committed account for entries MISSING from disk entirely (an unattributed delete
     * is divergence too). BOUND rows (design/186 §2.2) are judged AT THEIR COMMITTED PROJECTION: a
     * single-copy move / delete-then-plant empties the bound address and rings here even though the
     * id is still "somewhere" (the v1 any-scope presence test read exactly that as fine — the
     * registered residual this schema retires); byte-carriers of the id at other addresses are named
     * in the same finding. UNBOUND rows (migration transitional) keep the v1 store-wide presence
     * test — no false alarms while their address is underivable. Non-destructive throughout: nothing
     * is restored, forgotten, or adopted here.
     *
     * `opts.idlessWriteScope` is the CALLER's per-call session verdict (same posture as the mount:
     * the backend never guesses): when the audited session HAS a write scope, id-less files under it
     * that were not in the mount-time baseline are named too — the session has no sanctioned write
     * channel into the store, so a new arrival is out of band by construction. Left out (a
     * writeScope-null layering, or a caller that never mounted the audit), the sweep is exactly the
     * id-bearing audit above; a scope mismatch with the armed mount likewise degrades to silence
     * rather than judging against a foreign baseline.
     */
    auditRestrictedDivergence(scopes: readonly string[], opts?: {
        idlessWriteScope?: string;
    }): void;
    /** Lexical floor (design/138 §2.1): `1 - Jaccard` distance over name+description+body, ascending. */
    search(query: string, scopes: readonly string[], opts?: {
        limit?: number;
        exposureBands?: boolean;
    }): Promise<ScoredMemoryEntry[]>;
    private searchFrom;
    /**
     * H4 — the entry transaction, staged and journaled:
     *  1. RELOAD the ledger from disk (验收 M2: a concurrently-committed transaction from another
     *     process becomes a CAS conflict here, never a blind write over it) + finish any interrupted
     *     transaction (redo journal).
     *  2. PLAN every patch against current state (CAS/conflicts decided now; no disk writes yet).
     *  3. STAGE: write each new file content to a same-directory `.stage-*` tmp (fsync'd).
     *  4. JOURNAL: atomically write the full redo log (ops with content + the post-txn ledger snapshot).
     *     ← the commit point: a crash after this rolls FORWARD (recovery redoes the journal); a crash
     *     before it rolls BACK (staged tmps are swept, nothing was committed).
     *  5. EXECUTE: atomic renames / deletes / shadow updates.
     *  6. Persist the ledger (atomic, fail-closed) and drop the journal.
     */
    applyPatches(patches: readonly NotePatch[]): Promise<PatchReport>;
    private txnLockDir;
    /**
     * 证伪式复审 L1/L2/L3 — the mkdir mutex, hardened (same shape as layout.ts' acquireSidecarLock, but
     * fail-closed timings and an async wait):
     *  - L2: every acquisition writes an OWNER TOKEN (`<lock>/owner`, crypto-random — never a
     *    timestamp) immediately after mkdir. Release only removes the lock while the token is still
     *    ours; {@link assertTxnLockOwnership} re-verifies it at the commit point.
     *  - L1: the stale steal is SINGLE-WINNER — atomic rename of the stale lock dir to a unique
     *    tombstone (exactly one racer's rename succeeds; losers ENOENT back into the wait loop), then
     *    the winner removes the tombstone and re-acquires through a NORMAL mkdir. Never a bare
     *    rmSync+mkdir (two same-window stealers would force-rm each other's FRESH lock = double hold).
     *  - L2 (steal side): only a lock that is BOTH past its stale line AND carries an owner token is
     *    stealable (a tokenless dir is a mid-acquisition, not a crashed holder).
     *  - L3: the wait deadline is `max(waitMs, observed lock's stale line + grace)` — a holder that
     *    crashed with `remaining stale > waitMs` no longer strands every waiter in an all-throw window.
     */
    private acquireTxnLock;
    /** 证伪式复审 L2 — 提交点归属复验: called immediately before the JOURNAL write (the persistent commit
     *  point). A holder that stalled past the stale deadline (slow disk / GC pause / long txn) and was
     *  stolen from must ABORT — its staged tmps carry no committed state and the stealer's journal
     *  recovery is now authoritative; committing anyway would overwrite the stealer's transaction. */
    private assertTxnLockOwnership;
    private applyPatchesLocked;
    /** E-3 ② — the journal CLOSE guard: re-read the fixed-path journal and remove it only while its
     *  `.txn` names THIS transaction. A holder that stalled past the stale line and was stolen from
     *  may reach its close after the stealer already wrote a NEW journal at the same path — a bare rm
     *  there deletes the stealer's LIVE redo record (its crash then loses a committed transaction).
     *  An absent/foreign/unparseable journal is simply not ours to remove: leave it (the stealer, or
     *  the recovery leg, owns its fate). The remaining read→rm window is the same assert→act residue
     *  the commit-point assertion accepts, stated there. */
    private closeJournalIfOurs;
    /** Plan one patch: decide CAS/conflicts against CURRENT state, emit journal ops (no entry-file
     *  writes here — staging/execution happen in {@link applyPatches}). Every row write is BOUND
     *  (§1.1) at the coordinates the batch ACTUALLY lands — a slug collision's `-n` suffix binds the
     *  suffixed slug, the same one `applied[].slug` reports (v1's ledger had no landing awareness). */
    private planOne;
    private locateById;
    /**
     * design/178 v2-b §7 — request-driven erasure with evidence (capability face; probe with
     * `typeof backend.eraseWithEvidence === "function"`). Rides the SAME txn mutex, the SAME journal
     * shape, and the SAME recovery leg as `applyPatches` — never a second transaction protocol.
     * Everything runs INSIDE the lock (r1: resolving a selector outside it pins a set the store can
     * move under before the lock lands): recover → migrate → chain replay-judgment → resolution (a
     * scope selector consults the census for membership — the account alone cannot see an
     * unledgered-live inhabitant of the scope) → census → plan → journal (ops + ledger snapshot +
     * evidence rows in ONE commit) → EXECUTE → ledger → evidence append → guarded journal close →
     * lineage capture-and-clear.
     *
     * Replay protocol (§4.6): the first execution pins the resolved id set on an ANCHOR row; the same
     * requestId re-executes against THAT set (never re-resolves), deletes the un-deleted remainder,
     * and answers the cumulative view. A same-requestId call with a DIFFERENT selector is refused
     * (`memory.erasure_selector_mismatch`); two same-req anchors on the chain are a spliced chain —
     * refused for a human, never adjudicated by file order.
     *
     * This lane deletes by the ACCOUNT (ledger row), not by a physical locate: shadow-only rows
     * (projection deleted out of band), carrier-missing rows, and unbound rows (P=0 / P≥2) are all
     * erasable — the census sweep removes EVERY projection carrying the id (erasure needs no
     * adjudication of which copy was real: removing all of them IS the requested outcome). A ledger
     * key failing the entry-id contract takes the row-only arm (`legacyId` evidence; no ops — a
     * shadow op on it would fail this store's own journal validator). The NotePatch delete lane's
     * semantics are untouched (a shadow-only row still answers unknown-id there — pinned).
     *
     * Failure postures: an unreadable census refuses the WHOLE request pre-commit
     * (`memory.erasure_census_incomplete` — "deleted clean" must not be attested over a store that
     * cannot be fully read); per-id planning faults are non-fatal `conflicts` rows (a replay
     * converges them); everything before the journal write refuses cleanly (F-1); every crash window
     * after it converges through the shared recovery leg (F-2..F-5, F-13).
     */
    eraseWithEvidence(input: EraseMemoryEntriesInput): Promise<MemoryErasureAttestation>;
    /** Bounded journal-quiescence fence (attempt count × retry pause). Constants, not knobs: the
     *  contended window is the commit tail of one transaction (milliseconds); a store where it stays
     *  contended for the full bound has a crashed/wedged writer, and the honest answer is refusal,
     *  not a longer wait. */
    private static readonly AUDIT_FENCE_ATTEMPTS;
    private static readonly AUDIT_FENCE_RETRY_MS;
    /** Is a transaction journal present right now? ENOENT-only absence (§3.1 ⓪ — a probe FAILURE is
     *  not "no journal"; it throws the fail-closed corrupt error, never folds into quiescence). */
    private auditJournalPending;
    /** PURE ledger read for the audit faces: raw bytes → parsed rows, touching NOTHING on the
     *  instance (no cache install, no schema-version memo, no v1-compat announcement) and never the
     *  disk. A v1-form ledger is refused here — the migration belongs to the store's own entries
     *  (construction / adopting reads / locked mutations), never to an audit read. */
    private auditReadLedger;
    /** Run `read` over a STABLE committed account: journal absent before and after, ledger bytes
     *  identical across the read (closes both tear channels — the journal-first commit window and any
     *  ledger-advancing path, the journal-free no-move adoption save included), retried up to the
     *  bound. `read` may throw {@link AuditObservationUnstableError} on a mid-transition scene — the
     *  loop re-observes, and the FINAL attempt receives `final: true` to answer the steady-state
     *  reading instead. Throws {@link AuditSnapshotContendedError} when the bound exhausts.
     *
     *  CONSISTENCY CLAIM, stated precisely (adversarial-review wording pass): the answer is a
     *  CONSISTENT committed account as observed at the fenced read — never a torn mixture of two
     *  transactions' halves. FRESHNESS-AT-RETURN is deliberately not claimed: a lock-free reader
     *  cannot exclude a writer whose entire commit lands in the sub-window between the last two
     *  probes, and such an answer is simply the immediately-preceding committed state (the same
     *  answer the call would have produced had it returned a moment earlier). Holding the store's
     *  txn mutex would buy freshness at the cost of the audit read acquiring writer machinery (a
     *  write, and a contention seat) — refused: an audit must not perturb the store it audits. */
    private auditStable;
    /** The audit faces' committed-CONTENT resolution for one row (see {@link CommittedEntrySnapshot}
     *  for the state law). Reads only; the shadow read is fail-closed (a control-plane read FAILURE
     *  is not absence), the projection probe is the shared side-effect-free tri-state probe. */
    private resolveCommittedContent;
    /**
     * design/178 v2-a §1.3 — the per-id AUDIT SNAPSHOT (capability face; probe with
     * `typeof backend.committedSnapshotOf === "function"`). Pure committed read under the double
     * fence; unknown id ⇒ `{ state: "absent" }` (never a throw, never an empty guess); unbound row ⇒
     * the account half answers with `binding: { state: "unbound" }` (legal transitional state);
     * v1-compat store ⇒ loud refusal (migrate first); fence contention past the bound ⇒ loud refusal.
     */
    committedSnapshotOf(id: string): Promise<CommittedEntrySnapshot>;
    /** One row's public {@link CommittedBinding} (shared tagged form — see the type's own doc). */
    private publicBinding;
    /**
     * design/178 v2-a §1.3 — the LEDGER-DRIVEN scope enumeration (capability face). The account is
     * the enumeration authority: every bound row whose scope is requested is answered — including
     * shadow-only rows a disk scan would miss (`listHeaders` is an ADOPTING read and enumerates the
     * plane; it is not an audit face). Unbound rows land in the store-level `unbound` appendix.
     * Fence contention past the bound ⇒ `{ complete: false }` (NO data members — nothing to misread
     * as a truncated package): the caller fails closed. Corrupt control-plane bytes and the
     * v1-compat refusal still THROW (they are definite states, not contention).
     */
    committedSnapshotsOfScopes(scopes: readonly string[]): Promise<CommittedScopeSnapshots>;
    /**
     * design/178 v2-a §1.3 — the per-id CUSTODY read (capability face): every evidence row of
     * `transfers.jsonl` naming this id, in file (= time) order, as OPEN {@link TransferEvidence}.
     * PURE INSPECTION, never repair: a torn tail (crash mid-append) answers the sound prefix with
     * `state: "damaged"` and the file is NOT touched — quarantining the fragment is the recovery
     * leg's duty (the adopting read path), and an audit that rewrites its subject has destroyed the
     * evidence it was asked about. Non-tail corruption stays the fail-closed throw. Unknown channels
     * are TOLERATED and passed through (open form: a chain written by a newer engine still answers);
     * known channels are validated by the store's own rule set. Runs under the same journal fence as
     * the snapshot faces — a pending journal may hold committed-but-unappended evidence, and a chain
     * read across it would be completed differently by the very next recovery.
     */
    custodyOf(id: string): Promise<EntryCustodyReport>;
    /** The pure open-form evidence read behind {@link custodyOf} (no healing, no writes). `raw` is
     *  the caller's already-fenced read of the log (undefined = absent). */
    private custodyReadPure;
    /** design/178 v2-c §4 — the export/import refusal code (engine coded-error family, deliberately
     *  outside the governance registry: a store-state refusal, not a per-principal verdict). */
    private static readonly EXPORT_INCOMPLETE;
    /** The durable store identity (§6.2): read it, or mint it (`wx`) on first need — the txn mutex is
     *  held by every caller, so the exclusive create cannot race in-process; a cross-process EEXIST
     *  loser adopts the winner's identity. Corrupt/unreadable identity is fail-closed. */
    private storeIdentityLocked;
    /** One consistency fingerprint over the three ENGINE governance faces (challenge ledger, lineage
     *  ledger, pollution markers) — raw bytes, journal included. The two strict sidecars' JOURNAL
     *  reads are wrapped FAIL-CLOSED here (§4-6): the shared strict reader folds a journal read
     *  error into absence, and an export that read a stale main file over an unreadable journal
     *  would seal stale governance under an honest hash. Marker files that exist but cannot be read
     *  fingerprint their error code (a stable fault stays stable; a flapping one drifts the fence). */
    private exportGovernanceFingerprint;
    /**
     * The export chain read: PURE INSPECTION (never heals, never quarantines, never truncates — C-8
     * pins the file byte-identical across a refused export). A torn tail refuses the export
     * (`memory.export_incomplete`; the deployment heals it by running any locked mutation entry,
     * then re-exports); an unknown channel refuses too (T-14: a row whose slicing rules this build
     * does not know can neither be carried — possible cross-tenant leak — nor silently dropped —
     * lost mandatory governance); one ev under two different payloads refuses (spliced chain);
     * non-tail corruption stays the fail-closed {@link ControlPlaneCorruptError}. Byte-equal
     * duplicates collapse (append-idempotency residue).
     */
    private readExportChainRowsPure;
    /**
     * v2-c §2 — the tenant-slicing table over validated chain rows (T-6..T-11; T-9/T-10 refusals,
     * T-14 already refused by the reader). `storeId` is a thunk so the identity mints only when a
     * redaction actually needs the provenance stamp.
     */
    private sliceCustodyForScopes;
    /** T-12 — quarantine residual enumeration for an id set (names for attributable hits, an opaque
     *  count for the rest — same honest bound the erasure attestation uses). */
    private enumerateQuarantineFor;
    /**
     * design/178 v2-c §3 — the ONE-LOCK export composite (capability face; probe with
     * `typeof backend.exportSnapshotOf === "function"`). All five reads — entries (ledger-driven,
     * committed content), custody chain, challenge ledger, lineage ledger, pollution markers — happen
     * inside a single txn-mutex hold: the two backend faces are writer-serialized by the mutex
     * itself; the three engine faces (written under their own sidecar locks, not the mutex) are
     * fenced by a byte-level double fingerprint around the reads (≤3 rounds, then a loud refusal —
     * a store too hot to snapshot answers `memory.export_incomplete`, never a mixed-epoch bundle).
     * The terminal ownership assertion turns a stolen lock (stale-steal past the 30s line during a
     * long assembly) into a refusal: an unstolen lock means no writer entered, which IS the
     * consistency proof — no lease machinery needed. The chain read is pure inspection (a torn tail
     * refuses with the file byte-untouched); `complete:false`-class scenes all refuse loudly — there
     * is no degraded bundle shape, by design (§4).
     */
    exportSnapshotOf(scopes: readonly string[], timings?: TxnLockTimings): Promise<MemoryExportSnapshot>;
    /**
     * design/178 v2 §4.3 — the single-item governance slice face (parent-design form, kept for
     * NON-export consumers: erasure resolution, per-id inspection tooling). Answers
     * `complete: false` + reason instead of a bundle-shaped refusal (its callers fail closed on the
     * flag); definite corruption still throws. The export path deliberately does NOT use this face —
     * two independently-locked single faces are exactly the cross-face tear the composite closes.
     */
    governanceExport(scopes: readonly string[], timings?: TxnLockTimings): Promise<{
        custody: TransferEvidence[];
        unbound: string[];
        complete: boolean;
        unsliceableCustody: number;
        reason?: string;
    }>;
    /**
     * design/178 v2-c §5.4 — custody carriage onto THIS chain (capability face). Rows land under the
     * import namespace — `ev = imp:<bundleHash>:<source ev>` (full-hash salt: deterministic per
     * bundle, chain-deterministic across hops — a re-exported imported row salts its CURRENT ev
     * next hop), `origin` kept when present (multi-hop provenance) else stamped with the source
     * store's identity, `srcEv` kept else stamped with the source ev — so an imported row is ALWAYS
     * origin-bearing and the local replay/anchor judgments structurally never read it. Unknown
     * channels and rows failing this store's own validation are WITHHELD and reported (never
     * appended verbatim — an unvalidatable row would poison the fail-closed chain — and never a
     * whole-package refusal — a newer exporter must not brick an older importer). Appends are
     * idempotent by ev with canonical-payload comparison (same-bundle re-import converges; a
     * different payload under an existing ev is fail-closed corruption). Direct chain append, no
     * journal: custody carriage is not a ledger mutation, and origin-bearing rows are forbidden in
     * journals by the recovery leg's own validation.
     */
    custodyImport(rows: ReadonlyArray<Record<string, unknown>>, opts: {
        bundleHash: string;
        sourceStoreId: string;
    }, timings?: TxnLockTimings): Promise<{
        appended: number;
        withheld: Array<{
            srcEv: string;
            channel: string;
        }>;
    }>;
    /** The in-lock custody carriage body ({@link custodyImport}'s and the import composite's shared
     *  leg — the composite already holds the mutex and must not re-enter the acquisition). */
    private custodyImportLocked;
    /** Append import-namespace challenge events and VERIFY every key-idempotent replay by reading
     *  the full payload back (§5.2 defense ③): `appendChallengeEvents` is key-idempotent, not
     *  payload-idempotent — a pre-planted same-key event (a resolve, or a same-entry different-
     *  reason challenge) would silently swallow the imported challenge while the return echoes a
     *  plausible assignment. A mismatch ABORTS the import (the latch stays; the account is not
     *  silently short). */
    private appendImportChallengesVerified;
    /**
     * design/178 v2-c §1 — the IMPORT COMPOSITE (capability face; the engine's `importMemoryBundle`
     * is its validating membrane). One txn-mutex hold end to end:
     *  ①b destination-chain completeness precheck (torn/degraded ⇒ whole package refused with ZERO
     *     side effects — before the latch, "reject before landing" made literal);
     *  ②  the synthetic latch (durable pending lineage txn over every bundle entry id — from here
     *     to release, those ids are withheld on every model-visible read face; crash-safe);
     *  ③  authoritative in-lock judgment (the five import rules — a pre-lock classification would
     *     only be advisory, so there is none: the lock-held pass IS the classification);
     *  ④  entry landing through {@link applyPatchesLocked} (same skeleton, same lock, same recovery
     *     leg — never a second transaction protocol; `guard:"absent"` adjudicates the residual
     *     same-id arms);
     *  ⑤  governance legs, custody FIRST unconditionally (chain facts are exempt from the
     *     entry-eligibility rule — an absent-id delete row is exactly the anti-resurrection
     *     payload), then entry-attached lineage/challenges for landed ∪ already-present ids only,
     *     then pollution (retroactive sweep DURABLE BEFORE the marker — the crash between them
     *     leaves contributions challenged and the marker converging on re-import, never the
     *     reverse);
     *  ⑥  the durable completion receipt (`wx`, carrying the report) and the latch release —
     *     receipt-gated, the ONLY release channel.
     * Any failure past ② leaves the latch standing: re-importing the SAME bundle converges every
     * crash window (idempotent latch, idempotent judgments, ev/key-idempotent governance legs,
     * receipt short-circuit).
     */
    importBundleCommit(plan: MemoryBundleImportPlan, timings?: TxnLockTimings): Promise<MemoryImportReport>;
    getConsolidationCursor(scope: string): Promise<string | undefined>;
    setConsolidationCursor(scope: string, cursor: string): Promise<void>;
    private readCursors;
}
export { scopeDirName };