Store

tcmts.store.Store · #znwy43cs · class

import { sameSpace } from "#codebase/tcmts/core/types";
import type { DefKind, DefRecord, Dep, InitKind, RefPos, Span } from "#codebase/tcmts/core/types";
import {
  causalHash, contributionOf, deepFreeze, DEF_CACHE, EMPTY_BODY, EMPTY_NAMESPACE, entryText, jsxOf, LAYOUT,
  namespaceHash, readEntryText, renamedSignature, ROOT_CAUSAL, SCHEMA, stateHashOf, textHash,
} from "#codebase/tcmts/store";
import type {
  Causal, Comment, Contribution, LogEntry, NameRow, NamespaceBody, Release, Row, TestResult, TextVersion,
} from "#codebase/tcmts/store";
import { DatabaseSync } from "node:sqlite";
import type { StatementSync } from "node:sqlite";

export class Store {
  readonly db: DatabaseSync;
  private branchCache: { id: number; name: string } | undefined;
  /** one batch per Store instance (per CLI command), or per editor command (endBatch); allocated on the first logged change */
  private batch: number | undefined;
  /** branches whose names changed since their head causal was made */
  private dirty = new Set<number>();
  /** causals this batch made, by branch: a command's changes are one causal however many transactions it takes */
  private made = new Map<number, string>();
  /** extra parents for a branch's next causal (what a merge brought in) */
  private pendingParents = new Map<number, Set<string>>();
  private depth = 0;
  /** text versions this batch chose and hasn't recorded in history yet, by branch: definition → version (null: as first stored) */
  private pendingTexts = new Map<number, Map<string, string | null>>();
  /** a branch's text versions, replaced whole (a reset, a merge): definition → version */
  private replacedTexts = new Map<number, Map<string, string>>();
  /** the text versions this branch shows, read once (getDef asks for every definition it reads) */
  private shown: Map<string, string> | undefined;
  /** the last namespace whose text versions were read */
  private textsRead: { namespace: string; texts: Map<string, string> } | undefined;
  /** statements, prepared once: a command runs the same few queries over and over (a cached test run, millions of times) */
  private statements = new Map<string, StatementSync>();
  /** another branch this process only reads (`--branch`): what {@link branch} is, while no branch can change */
  reading: string | undefined;
  /** what this batch's history entry is for (`-m`): its message, kept with the entry the batch makes */
  message: string | undefined;
  /**
   * Asked before a name is changed one at a time (added, removed, pointed elsewhere, or shown with other text): it
   * throws to refuse. A branch's names replaced whole (a reset, a merge, a pull) aren't asked; nor is anything
   * {@link unguarded} changes. The codebase uses it to keep libraries read-only.
   */
  guard: ((path: string) => void) | undefined;
  private guarding = true;

  constructor(file: string) {
    this.db = new DatabaseSync(file);
    // several processes share a codebase (agents in sessions, an editor, a terminal): a write waits for another's.
    // Said first, so the statements after it wait too: setting WAL while another process checkpoints (closing the
    // codebase as the last one out) fails at once otherwise, and a session's server starting with it
    this.db.exec(
      "PRAGMA busy_timeout = 15000; PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON;",
    );
    // a codebase whose tables another tcm laid out differently can't be read (tcm is v0: there are no
    // migrations): say so, rather than fail on a missing column later
    const laidOut = !!this.sql(
      "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'definitions'",
    ).get();
    if (laidOut) {
      let was: string | undefined;
      try {
        was = (
          this.sql("SELECT value FROM settings WHERE key = 'layout'").get() as
            | { value: string }
            | undefined
        )?.value;
      } catch {}
      if (was !== LAYOUT) {
        this.db.close();
        throw new Error(
          `${file} was made by a tcm that stores codebases differently, and this one can't read it; make the codebase again with this tcm (\`tcm import\` its files, or \`tcm pull\` it)`,
        );
      }
    }
    // a codebase laid out already (as this tcm lays them out) is only read in opening it: a write would wait for
    // every other process's, a long one's too, before a command that only reads could start
    if (
      laidOut &&
      this.sql("SELECT 1 FROM branches WHERE name = 'main'").get() &&
      this.sql("SELECT 1 FROM settings WHERE key = 'branch'").get() &&
      this.sql("SELECT 1 FROM causals WHERE hash = ?").get(ROOT_CAUSAL) &&
      this.sql("SELECT 1 FROM namespaces WHERE hash = ?").get(EMPTY_NAMESPACE)
    )
      return;
    this.db.exec(SCHEMA);
    this.sql("INSERT OR REPLACE INTO settings (key, value) VALUES ('layout', ?)").run(LAYOUT);
    this.sql("INSERT OR IGNORE INTO namespaces (hash, body) VALUES (?, ?)").run(
      EMPTY_NAMESPACE,
      EMPTY_BODY,
    );
    this.sql(
      "INSERT OR IGNORE INTO causals (hash, namespace, ts, command) VALUES (?, ?, 0, 'init')",
    ).run(ROOT_CAUSAL, EMPTY_NAMESPACE);
    this.sql("INSERT OR IGNORE INTO branches (name, head) VALUES ('main', ?)").run(ROOT_CAUSAL);
    this.sql("INSERT OR IGNORE INTO settings (key, value) VALUES ('branch', 'main')").run();
  }

  close() {
    this.db.close();
  }

  private sql(text: string): StatementSync {
    let s = this.statements.get(text);
    if (!s) this.statements.set(text, (s = this.db.prepare(text)));
    return s;
  }

  /** Run `fn` with no {@link guard}: what changes names it guards, on purpose (adding a library, removing one). */
  unguarded<T>(fn: () => T): T {
    const was = this.guarding;
    this.guarding = false;
    try {
      return fn();
    } finally {
      this.guarding = was;
    }
  }

  private guarded(path: string) {
    if (this.guarding) this.guard?.(path);
  }

  /** Run `fn` in a transaction (joining the enclosing one, if any); branches it changed get a new head causal. */
  transaction<T>(fn: () => T): T {
    if (this.depth) {
      this.depth++;
      try {
        return fn();
      } finally {
        this.depth--;
      }
    }
    // IMMEDIATE: a transaction writes, so it takes the write lock first, waiting for another process's (busy_timeout).
    // One that read first and then wrote would fail at once if another process wrote in between (WAL can't move
    // a reader's snapshot forward): two sessions starting together, each making its branch, would lose one
    this.db.exec("BEGIN IMMEDIATE");
    this.depth = 1;
    try {
      const r = fn();
      this.commitHistory();
      this.db.exec("COMMIT");
      return r;
    } catch (e) {
      this.db.exec("ROLLBACK");
      this.dirty.clear();
      this.defs.clear();
      this.pendingTexts.clear();
      this.replacedTexts.clear();
      this.shown = undefined;
      throw e;
    } finally {
      this.depth = 0;
    }
  }

  /**
   * End this batch: the next change starts a new history entry. A CLI command is one batch; the language server,
   * which lives across many commands, ends one after each.
   */
  endBatch() {
    this.batch = undefined;
    this.made.clear();
    // (another process may have moved the branch since: its text versions are read again)
    this.shown = undefined;
  }

  // ---- content ---------------------------------------------------------------

  putDefs(records: DefRecord[], tsVersion: string, irVersion: number) {
    for (const r of records) this.defs.delete(r.hash);
    const now = Date.now();
    const comp = this.sql(
      "INSERT OR IGNORE INTO components (hash, size, ir_version, created_at) VALUES (?, ?, ?, ?)",
    );
    const def = this.sql(
      `INSERT OR IGNORE INTO definitions (hash, kind, component_hash, component_index, ir, source_text, ref_spans, reserved_names, init_kind, name_hint, nominal)
       VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
    );
    const dep = this.sql(
      "INSERT OR IGNORE INTO deps (from_hash, to_ref, ref_kind, strict) VALUES (?, ?, ?, ?)",
    );
    const ext = this.sql(
      "INSERT OR IGNORE INTO externals (ref, module, subpath, export_name) VALUES (?, ?, ?, ?)",
    );
    const sig = this.sql(
      "INSERT OR IGNORE INTO signatures (hash, display, ts_version) VALUES (?, ?, ?)",
    );
    const meta = this.sql("INSERT OR IGNORE INTO metadata (hash, key, value) VALUES (?, ?, ?)");
    this.transaction(() => {
      for (const r of records) {
        comp.run(r.componentHash, r.componentSize, irVersion, now);
        const inserted = def.run(
          r.hash,
          r.kind,
          r.componentHash,
          r.componentIndex,
          r.ir,
          r.sourceText,
          JSON.stringify(r.spans),
          JSON.stringify(r.reservedNames),
          r.initKind,
          r.nameHint,
          r.nominal ? 1 : 0,
        );
        if (!inserted.changes) continue; // identical definition already stored: first text wins
        // (whether it's called is the second bit: the column stays as it was)
        for (const d of r.deps)
          dep.run(r.hash, d.ref, d.pos, (d.strict ? 1 : 0) | (d.called ? 2 : 0));
        for (const s of r.spans)
          if (s.ext)
            ext.run(
              `${s.ext.module}${s.ext.subpath ? "/" + s.ext.subpath : ""}#${s.ext.export}`,
              s.ext.module,
              s.ext.subpath,
              s.ext.export,
            );
        sig.run(r.hash, r.signature, tsVersion);
        if (r.doc) meta.run(r.hash, "doc", r.doc);
        if (r.params) meta.run(r.hash, "params", JSON.stringify(r.params));
        if (r.shape) meta.run(r.hash, "shape", JSON.stringify(r.shape));
        if (r.docRefs?.length) meta.run(r.hash, "docRefs", JSON.stringify(r.docRefs));
        if (r.table) meta.run(r.hash, "table", JSON.stringify(r.table));
        if (r.transform) meta.run(r.hash, "transform", "1");
        if (r.checked) meta.run(r.hash, "checked", JSON.stringify(r.checked));
        if (r.globalNames) meta.run(r.hash, "globals", JSON.stringify(r.globalNames));
      }
    });
  }

  /**
   * The components `hashes` need: their own, and those of everything they use, from the stored dependencies alone (no
   * definition is read: a sync asks this of tens of thousands).
   */
  closureComponents(hashes: Iterable<string>): Set<string> {
    const compOf = this.sql("SELECT component_hash AS c FROM definitions WHERE hash = ?");
    const uses = this.sql(
      "SELECT DISTINCT substr(d.to_ref, 2) AS h FROM definitions m JOIN deps d ON d.from_hash = m.hash WHERE m.component_hash = ? AND d.to_ref LIKE '#%'",
    );
    const comps = new Set<string>();
    const queue = [...hashes];
    while (queue.length) {
      const row = compOf.get(queue.pop()!) as { c: string } | undefined;
      if (!row || comps.has(row.c)) continue;
      comps.add(row.c);
      for (const { h } of uses.all(row.c) as { h: string }[]) queue.push(h);
    }
    return comps;
  }

  /** Hash format versions of stored definitions. */
  irVersions(): number[] {
    return (
      this.sql("SELECT DISTINCT ir_version AS v FROM components").all() as { v: number }[]
    ).map((r) => r.v);
  }

  getMetadata(hash: string, key: string): string | undefined {
    return (
      this.sql("SELECT value FROM metadata WHERE hash = ? AND key = ?").get(hash, key) as
        | { value: string }
        | undefined
    )?.value;
  }

  setMetadata(hash: string, key: string, value: string) {
    this.sql("INSERT OR REPLACE INTO metadata (hash, key, value) VALUES (?, ?, ?)").run(
      hash,
      key,
      value,
    );
    this.defs.delete(hash);
  }

  hasDef(hash: string): boolean {
    return !!this.sql("SELECT 1 FROM definitions WHERE hash = ?").get(hash);
  }

  /**
   * Definitions read so far. A definition never changes under its hash (only its metadata, and its text when
   * reformatted, which drop it from here), and a large codebase reads the same ones many times over in one command.
   * They're frozen: callers share them. By hash, then by the text version read ("": as first stored). Only the
   * DEF_CACHE most recently read are kept: a sync or a long-running server reads tens of thousands, whose IR held in
   * memory made every garbage collection slower.
   */
  private defs = new Map<string, Map<string, DefRecord | undefined>>();

  /** A definition, as this branch shows it (its text version, if it has one). */
  getDef(hash: string): DefRecord | undefined {
    return this.defWithText(hash, this.shownTexts().get(hash));
  }

  /** A definition with one of its text versions (none: as first stored). */
  defWithText(hash: string, text: string | undefined): DefRecord | undefined {
    let versions = this.defs.get(hash);
    // (the most recently read last: what's read least recently goes first)
    if (versions) this.defs.delete(hash);
    else {
      versions = new Map();
      if (this.defs.size >= DEF_CACHE) this.defs.delete(this.defs.keys().next().value!);
    }
    this.defs.set(hash, versions);
    const key = text ?? "";
    if (versions.has(key)) return versions.get(key);
    const rec = this.readDef(hash, text);
    versions.set(key, rec && deepFreeze(rec));
    return rec;
  }

  private readDef(hash: string, text?: string): DefRecord | undefined {
    const rec = this.readStoredDef(hash);
    const version = rec && text !== undefined ? this.textVersion(hash, text) : undefined;
    if (!rec || !version) return rec;
    // (the version's doc, or none: a doc removed is a version too)
    const out: DefRecord = { ...rec, ...version };
    if (version.doc === undefined) delete out.doc;
    if (version.docRefs === undefined) delete out.docRefs;
    return out;
  }

  private readStoredDef(hash: string): DefRecord | undefined {
    const row = this.sql(
      `SELECT d.*, c.size, s.display, m.value AS doc, p.value AS params, t.value AS table_schema, x.value AS transform, k.value AS checked, g.value AS globals, sh.value AS shape, dr.value AS doc_refs FROM definitions d
         JOIN components c ON c.hash = d.component_hash
         LEFT JOIN signatures s ON s.hash = d.hash
         LEFT JOIN metadata m ON m.hash = d.hash AND m.key = 'doc'
         LEFT JOIN metadata p ON p.hash = d.hash AND p.key = 'params'
         LEFT JOIN metadata t ON t.hash = d.hash AND t.key = 'table'
         LEFT JOIN metadata x ON x.hash = d.hash AND x.key = 'transform'
         LEFT JOIN metadata k ON k.hash = d.hash AND k.key = 'checked'
         LEFT JOIN metadata g ON g.hash = d.hash AND g.key = 'globals'
         LEFT JOIN metadata sh ON sh.hash = d.hash AND sh.key = 'shape'
         LEFT JOIN metadata dr ON dr.hash = d.hash AND dr.key = 'docRefs'
         WHERE d.hash = ?`,
    ).get(hash) as Row | undefined;
    return row && this.toRecord(row);
  }

  componentMembers(componentHash: string): DefRecord[] {
    const hashes = this.sql(
      "SELECT hash FROM definitions WHERE component_hash = ? ORDER BY component_index",
    ).all(componentHash) as { hash: string }[];
    return hashes.map((h) => this.getDef(h.hash)!);
  }

  private toRecord(row: Row): DefRecord {
    const deps = (
      this.sql("SELECT to_ref, ref_kind, strict FROM deps WHERE from_hash = ?").all(
        row.hash,
      ) as Row[]
    ).map((d): Dep => ({
      ref: d.to_ref as string,
      pos: d.ref_kind as RefPos,
      strict: (Number(d.strict) & 1) === 1,
      ...(Number(d.strict) & 2 ? { called: true } : {}),
    }));
    return {
      hash: row.hash as string,
      kind: row.kind as DefKind,
      componentHash: row.component_hash as string,
      componentIndex: row.component_index as number,
      componentSize: row.size as number,
      ir: row.ir as string,
      sourceText: row.source_text as string,
      spans: JSON.parse(row.ref_spans as string) as Span[],
      reservedNames: JSON.parse(row.reserved_names as string) as string[],
      initKind: row.init_kind as InitKind,
      nameHint: row.name_hint as string,
      nominal: row.nominal === 1,
      deps,
      signature: (row.display as string | null) ?? "?",
      doc: (row.doc as string | null) ?? undefined,
      params: row.params ? JSON.parse(row.params as string) : undefined,
      ...(row.shape ? { shape: JSON.parse(row.shape as string) } : {}),
      ...(row.doc_refs ? { docRefs: JSON.parse(row.doc_refs as string) } : {}),
      table: row.table_schema ? JSON.parse(row.table_schema as string) : undefined,
      ...(row.transform ? { transform: true } : {}),
      ...jsxOf(row.ir as string),
      ...(row.checked ? { checked: JSON.parse(row.checked as string) } : {}),
      ...(row.globals ? { globalNames: JSON.parse(row.globals as string) } : {}),
    };
  }

  /** Full hashes starting with a prefix. */
  hashesWithPrefix(prefix: string): string[] {
    return (
      this.sql("SELECT hash FROM definitions WHERE hash LIKE ? LIMIT 10").all(`${prefix}%`) as {
        hash: string;
      }[]
    ).map((r) => r.hash);
  }

  /** Hashes of stored definitions that reference `hash`. */
  dependents(hash: string): string[] {
    return (
      this.sql("SELECT DISTINCT from_hash FROM deps WHERE to_ref = ?").all(`#${hash}`) as {
        from_hash: string;
      }[]
    ).map((r) => r.from_hash);
  }

  /**
   * The dependents named on this branch, but `hash` itself: what uses it now, not every version that ever did (a
   * codebase keeps them all, and a much-used definition has thousands).
   */
  namedDependents(hash: string): string[] {
    // read for every named definition at once, and kept until names change: asked of each definition in turn (a
    // code browser's pages), one query each would sift every past version of a much-used one
    const at = `${this.branch().id}:${this.logHead()}`;
    if (this.dependentsNamed?.at !== at) {
      const of = new Map<string, string[]>();
      const rows = this.sql(
        "SELECT DISTINCT d.to_ref AS t, d.from_hash AS h FROM names n JOIN deps d ON d.from_hash = n.hash WHERE n.branch_id = ?",
      ).all(this.branch().id) as { t: string; h: string }[];
      for (const r of rows) {
        if (!r.t.startsWith("#") || r.t.slice(1) === r.h) continue;
        const users = of.get(r.t.slice(1));
        if (users) users.push(r.h);
        else of.set(r.t.slice(1), [r.h]);
      }
      this.dependentsNamed = { at, of };
    }
    return this.dependentsNamed.of.get(hash) ?? [];
  }
  private dependentsNamed: { at: string; of: Map<string, string[]> } | undefined;

  // ---- text versions -----------------------------------------------------------
  //
  // A definition's text (its comments, its layout, its doc) isn't part of its hash, so the same code can read
  // differently on different branches. A definition keeps the text it was first stored with; other versions are kept
  // beside it, each by its own hash (metadata `text <hash>`), and a branch's namespace says which one each name shows
  // (none: the first). Changing only the text is then a change like any: on its branch alone, an entry in its
  // history that reset and revert undo, carried by merges.

  /** One of a definition's text versions. */
  textVersion(hash: string, text: string): TextVersion | undefined {
    const v = this.getMetadata(hash, `text ${text}`);
    return v === undefined ? undefined : (JSON.parse(v) as TextVersion);
  }

  /** The text versions kept for a definition, besides the one it was first stored with. */
  textVersions(hash: string): string[] {
    return (
      this.sql("SELECT key FROM metadata WHERE hash = ? AND key LIKE 'text %' ORDER BY key").all(
        hash,
      ) as { key: string }[]
    ).map((r) => r.key.slice(5));
  }

  /**
   * Keep a text version of a stored definition (the caller has checked it's the same code); its hash, or none if it's
   * the text the definition was first stored with.
   */
  addText(hash: string, version: TextVersion): string | undefined {
    const stored = this.readStoredDef(hash);
    if (!stored || (stored.sourceText === version.sourceText && stored.doc === version.doc))
      return undefined;
    const text = textHash(version);
    const kept: TextVersion = {
      sourceText: version.sourceText,
      spans: version.spans,
      reservedNames: version.reservedNames,
      ...(version.doc !== undefined ? { doc: version.doc } : {}),
      ...(version.docRefs?.length ? { docRefs: version.docRefs } : {}),
    };
    this.sql("INSERT OR IGNORE INTO metadata (hash, key, value) VALUES (?, ?, ?)").run(
      hash,
      `text ${text}`,
      JSON.stringify(kept),
    );
    return text;
  }

  /** The text versions a namespace's names show: definition → version. */
  textsAt(namespace: string): Map<string, string> {
    if (this.textsRead?.namespace === namespace) return this.textsRead.texts;
    const texts = new Map<string, string>();
    const node = this.sql("SELECT body FROM namespaces WHERE hash = ?");
    const walk = (h: string) => {
      const row = node.get(h) as { body: string } | undefined;
      if (!row) return;
      const [, , children, shown] = JSON.parse(row.body) as NamespaceBody;
      for (const [, d, t] of shown ?? []) texts.set(d, t);
      for (const [, c] of children) walk(c);
    };
    walk(namespace);
    this.textsRead = { namespace, texts };
    return texts;
  }

  /** The text versions a branch (this one by default) shows, with what this batch changed and hasn't recorded yet. */
  texts(branch?: string): Map<string, string> {
    return this.textsOf(branch === undefined ? this.branch().id : this.branchId(branch));
  }

  private textsOf(id: number): Map<string, string> {
    const head = (this.sql("SELECT head FROM branches WHERE id = ?").get(id) as { head: string })
      .head;
    const base = this.replacedTexts.get(id) ?? this.textsAt(this.causal(head)!.namespace);
    const pending = this.pendingTexts.get(id);
    if (!pending?.size) return base;
    const out = new Map(base);
    for (const [d, t] of pending) {
      if (t === null) out.delete(d);
      else out.set(d, t);
    }
    return out;
  }

  private shownTexts(): Map<string, string> {
    return (this.shown ??= this.textsOf(this.branch().id));
  }

  /**
   * Show this text for a definition on this branch: kept as a version of its own, and recorded in the branch's
   * history (an entry of `command`'s) where the branch names it. The definition's hash, and every other branch, are
   * untouched. Text the definition was first stored with is no version: the branch shows it as stored.
   */
  setText(rec: TextVersion & { hash: string }, command: string) {
    this.transaction(() => {
      const text = this.addText(rec.hash, rec) ?? null;
      if ((this.shownTexts().get(rec.hash) ?? null) === text) return;
      const b = this.branch().id;
      if (!this.pendingTexts.has(b)) this.pendingTexts.set(b, new Map());
      this.pendingTexts.get(b)!.set(rec.hash, text);
      this.shown = undefined;
      const named = this.sql("SELECT path, kind FROM names WHERE branch_id = ? AND hash = ?").all(
        b,
        rec.hash,
      ) as { path: string; kind: DefKind }[];
      for (const n of named) this.guarded(n.path);
      for (const n of named) this.log("update", n.path, rec.hash, rec.hash, n.kind, command);
    });
  }

  // ---- branches --------------------------------------------------------------

  /**
   * The branch this process works on: a session's (`TCM_BRANCH`), which no other process can move, or else the
   * codebase's own (`tcm switch`).
   */
  branch(): { id: number; name: string } {
    if (!this.branchCache) {
      const session = this.reading ?? process.env.TCM_BRANCH;
      const name =
        session ||
        (this.sql("SELECT value FROM settings WHERE key = 'branch'").get() as { value: string })
          .value;
      const row = this.sql("SELECT id FROM branches WHERE name = ?").get(name) as
        | { id: number }
        | undefined;
      if (!row) throw new Error(`no branch named '${name}'${session ? " (TCM_BRANCH)" : ""}`);
      this.branchCache = { id: row.id, name };
    }
    return this.branchCache;
  }

  /**
   * Read another branch rather than this process's (`tcm view --branch <b>`), and change none: whatever would change
   * a branch's names, history, patches or todo throws instead. The process's own branch (a session's) is untouched.
   */
  readOnly(name: string) {
    if (!this.hasBranch(name)) throw new Error(`no branch named '${name}' (--branch)`);
    this.reading = name;
    this.branchCache = undefined;
    this.shown = undefined;
  }

  /** Refuse to change a branch while reading one (`--branch`). */
  private changing() {
    if (this.reading !== undefined)
      throw new Error(
        `${this.reading} is only read here (--branch): nothing changes it, or any branch (to change it, work on it: \`tcm switch\`, or a session's tcm_cli ["switch", "<branch>"])`,
      );
  }

  /** Re-read the current branch on next use (another process may have switched it). */
  forgetBranch() {
    this.branchCache = undefined;
    this.shown = undefined;
  }

  branches(): string[] {
    return (this.sql("SELECT name FROM branches ORDER BY id").all() as { name: string }[]).map(
      (r) => r.name,
    );
  }

  createBranch(name: string) {
    this.changing();
    const from = this.branch();
    this.transaction(() => {
      const { lastInsertRowid } = this.sql("INSERT INTO branches (name, head) VALUES (?, ?)").run(
        name,
        this.head(from.name),
      );
      this.sql(
        "INSERT INTO names (branch_id, path, hash, kind) SELECT ?, path, hash, kind FROM names WHERE branch_id = ?",
      ).run(lastInsertRowid, from.id);
      this.sql(
        "INSERT INTO patches (branch_id, old_hash, new_hash, ts) SELECT ?, old_hash, new_hash, ts FROM patches WHERE branch_id = ?",
      ).run(lastInsertRowid, from.id);
    });
  }

  /**
   * Delete a branch: its names, patches, todo and log. Not the branch this process works on, nor `main`; the history
   * entries it made stay (other branches may have them).
   */
  deleteBranch(name: string) {
    this.changing();
    if (name === "main") throw new Error("main can't be deleted");
    if (name === this.branch().name)
      throw new Error(`${name} is the branch this works on: switch to another first`);
    const id = this.branchId(name);
    this.transaction(() => {
      for (const table of ["names", "patches", "todo", "name_log"])
        this.sql(`DELETE FROM ${table} WHERE branch_id = ?`).run(id);
      this.sql("DELETE FROM branches WHERE id = ?").run(id);
    });
  }

  switchBranch(name: string) {
    this.changing();
    if (process.env.TCM_BRANCH)
      throw new Error(
        `this is a session on ${process.env.TCM_BRANCH} (TCM_BRANCH): it stays on its branch, and \`tcm switch\` would move the codebase's own, which it doesn't use (an agent's MCP session moves onto another branch with tcm_cli ["switch", "<branch>"])`,
      );
    if (!this.sql("SELECT 1 FROM branches WHERE name = ?").get(name))
      throw new Error(`no branch named '${name}'`);
    this.sql("UPDATE settings SET value = ? WHERE key = 'branch'").run(name);
    this.branchCache = undefined;
    this.shown = undefined;
  }

  // ---- names -----------------------------------------------------------------

  /** A branch's names (this one's by default): its code's, and with `files`, the files kept with it too. */
  names(branch?: string, files = false): NameRow[] {
    const id = branch === undefined ? this.branch().id : this.branchId(branch);
    return this.sql(
      `SELECT path, hash, kind FROM names WHERE branch_id = ?${files ? "" : " AND kind != 'file'"} ORDER BY path, hash`,
    ).all(id) as unknown as NameRow[];
  }

  /** Every hash reachable from a name on this branch (files' included), following references: one query. */
  liveHashes(): Set<string> {
    const rows = this.sql(
      "WITH RECURSIVE live(h) AS (SELECT hash FROM names WHERE branch_id = ? UNION SELECT substr(d.to_ref, 2) FROM deps d JOIN live ON d.from_hash = live.h WHERE d.to_ref LIKE '#%') SELECT h FROM live",
    ).all(this.branch().id) as { h: string }[];
    return new Set(rows.map((r) => r.h));
  }

  /**
   * This branch's names (with `files`, the files' too) and each definition's signature as shown (`shownSignature`):
   * one query, not a record each.
   */
  signedNames(files = false): (NameRow & { signature: string })[] {
    const rows = this.sql(
      `SELECT n.path, n.hash, n.kind, coalesce(s.display, '?') AS signature FROM names n LEFT JOIN signatures s ON s.hash = n.hash WHERE n.branch_id = ?${files ? "" : " AND n.kind != 'file'"} ORDER BY n.path, n.hash`,
    ).all(this.branch().id) as unknown as (NameRow & { signature: string })[];
    // (only a signature with a word some definition was called, and isn't now, needs a closer look)
    const now = new Map<string, Set<string>>();
    const hints = new Map<string, string>();
    for (const r of this.sql(
      "SELECT n.path, n.hash, d.name_hint AS hint FROM names n JOIN definitions d ON d.hash = n.hash WHERE n.branch_id = ? AND n.kind != 'file'",
    ).all(this.branch().id) as { path: string; hash: string; hint: string }[]) {
      now.set(
        r.hash,
        (now.get(r.hash) ?? new Set()).add(r.path.slice(r.path.lastIndexOf(".") + 1)),
      );
      hints.set(r.hash, r.hint);
    }
    const known = this.namesKnown();
    const renamed = new Set<string>();
    for (const [h, leaves] of now)
      for (const was of [hints.get(h)!, ...(known.get(h) ?? [])])
        if (!leaves.has(was)) renamed.add(was);
    if (!renamed.size) return rows;
    for (const r of rows)
      if (
        r.kind !== "file" &&
        r.kind !== "type" &&
        r.signature.match(/[A-Za-z_$][\w$]*/g)?.some((w) => renamed.has(w))
      )
        r.signature = this.shownSignature(this.getDef(r.hash)!);
    return rows;
  }

  /**
   * A definition's signature as it's shown: naming the codebase's types as they're named on this branch now. The
   * signature stored is the type as the checker printed it when the definition was made, which names a codebase type
   * as it was called then (as its module declares it, the name it was made as; or as the file being saved did), so
   * after `tcm mv` it would name it the old way. The types are those of its shape, by hash (or, where that stops,
   * those its code mentions), and a class's own. A name one of them was made as or had is shown as what it's called
   * now, unless that's still a name of one of them, or two of them have had it.
   */
  shownSignature(rec: DefRecord): string {
    if (rec.kind === "type" || rec.kind === "file") return rec.signature;
    const hashes = new Set<string>(rec.kind === "class" ? [rec.hash] : []);
    let whole = !!rec.shape;
    const visit = (s: unknown): void => {
      if (Array.isArray(s)) return s.forEach(visit);
      if (!s || typeof s !== "object") return;
      const o = s as { k?: string; hash?: string };
      if (o.k === "ref" && o.hash) hashes.add(o.hash);
      if (o.k === "opaque") whole = false;
      Object.values(o).forEach(visit);
    };
    visit(rec.shape);
    if (!whole)
      for (const d of rec.deps)
        if (d.pos === "type" && d.ref.startsWith("#")) hashes.add(d.ref.slice(1));
    const now = new Map(
      [...hashes].map((h) => [
        h,
        [...new Set(this.pathsOf(h).map((p) => p.slice(p.lastIndexOf(".") + 1)))],
      ]),
    );
    const current = new Set([...now.values()].flat());
    const known = this.namesKnown();
    const renames = new Map<string, string>();
    const clashes = new Set<string>();
    for (const [h, leaves] of now) {
      // (one nothing names now, a past version say, keeps the name it's printed with; one with several, which?)
      if (leaves.length !== 1) continue;
      const hint = h === rec.hash ? rec.nameHint : this.getDef(h)?.nameHint;
      for (const was of new Set([...(hint ? [hint] : []), ...(known.get(h) ?? [])])) {
        if (current.has(was)) continue;
        if (renames.has(was) && renames.get(was) !== leaves[0]) clashes.add(was);
        renames.set(was, leaves[0]!);
      }
    }
    for (const c of clashes) renames.delete(c);
    return renamedSignature(rec.signature, renames);
  }

  /**
   * The last segment of every name each definition named on this branch has had, on any branch (from the name log):
   * how a signature made then may name it. Read once, and kept until names change.
   */
  private namesKnown(): Map<string, Set<string>> {
    const at = `${this.branch().id}:${this.logHead()}`;
    if (this.known?.at !== at) {
      const of = new Map<string, Set<string>>();
      // (only those named on this branch, the ones asked about: the whole log is every version ever named, tens of
      // thousands, read on each command that shows a signature)
      for (const r of this.sql(
        "SELECT DISTINCT new_hash AS hash, path FROM name_log WHERE new_hash IN (SELECT hash FROM names WHERE branch_id = ?)",
      ).all(this.branch().id) as { hash: string; path: string }[])
        of.set(
          r.hash,
          (of.get(r.hash) ?? new Set()).add(r.path.slice(r.path.lastIndexOf(".") + 1)),
        );
      this.known = { at, of };
    }
    return this.known.of;
  }
  private known: { at: string; of: Map<string, Set<string>> } | undefined;

  private branchId(name: string): number {
    const row = this.sql("SELECT id FROM branches WHERE name = ?").get(name) as
      | { id: number }
      | undefined;
    if (!row) throw new Error(`no branch named '${name}'`);
    return row.id;
  }

  ensureBranch(name: string) {
    this.changing();
    this.sql("INSERT OR IGNORE INTO branches (name, head) VALUES (?, ?)").run(name, ROOT_CAUSAL);
  }

  /** Fingerprint of a branch's namespace, for detecting concurrent changes during sync. */
  stateHash(branch?: string): string {
    return stateHashOf(this.names(branch, true));
  }

  /**
   * Make `branch` name exactly `rows`, showing the text versions they say, logging each change on that branch (a new
   * causal follows).
   */
  setBranchNames(
    branch: string,
    rows: NameRow[],
    command: string,
  ): { added: number; removed: number; texts: number } {
    this.ensureBranch(branch);
    const id = this.branchId(branch);
    return this.transaction(() => this.replaceBranchNames(id, rows, command));
  }

  hasBranch(name: string): boolean {
    return !!this.sql("SELECT 1 FROM branches WHERE name = ?").get(name);
  }

  /** This branch's files whose own name is `leaf` (`libraries`: `lib.libraries`, `a.lib.libraries`, ...): one query. */
  filesNamed(leaf: string): NameRow[] {
    return this.sql(
      "SELECT path, hash, kind FROM names WHERE branch_id = ? AND kind = 'file' AND (path = ? OR path LIKE ?) ORDER BY path",
    ).all(this.branch().id, leaf, `%.${leaf}`) as unknown as NameRow[];
  }

  /** Hashes named `path`; with `kind`, only those in the same space (a type and a value can share a path). */
  hashesAt(path: string, kind?: DefKind): string[] {
    const rows = this.sql(
      "SELECT hash, kind FROM names WHERE branch_id = ? AND path = ? ORDER BY hash",
    ).all(this.branch().id, path) as { hash: string; kind: DefKind }[];
    return rows.filter((r) => kind === undefined || sameSpace(r.kind, kind)).map((r) => r.hash);
  }

  /** Point `path` at `newHash` instead of `oldHashes`, leaving other hashes on it (the other space) alone. */
  replaceNames(path: string, oldHashes: string[], newHash: string, kind: DefKind, command: string) {
    this.guarded(path);
    this.transaction(() => {
      const b = this.branch().id;
      for (const o of oldHashes)
        this.sql("DELETE FROM names WHERE branch_id = ? AND path = ? AND hash = ?").run(b, path, o);
      this.sql("INSERT OR IGNORE INTO names (branch_id, path, hash, kind) VALUES (?, ?, ?, ?)").run(
        b,
        path,
        newHash,
        kind,
      );
      for (const o of oldHashes.length ? oldHashes : [null])
        this.log("update", path, o, newHash, kind, command);
    });
  }

  pathsOf(hash: string): string[] {
    // (through names_by_hash, said outright: left to choose, SQLite reads every name of the branch through the primary
    // key, which holds the path, rather than the few with this hash; rendering asks this for every reference)
    return (
      this.sql(
        "SELECT path FROM names INDEXED BY names_by_hash WHERE branch_id = ? AND hash = ?",
      ).all(this.branch().id, hash) as { path: string }[]
    )
      .map((r) => r.path)
      .sort();
  }

  addName(path: string, hash: string, kind: DefKind, command: string) {
    this.guarded(path);
    this.transaction(() => {
      const b = this.branch().id;
      const r = this.sql(
        "INSERT OR IGNORE INTO names (branch_id, path, hash, kind) VALUES (?, ?, ?, ?)",
      ).run(b, path, hash, kind);
      if (r.changes) this.log("add", path, null, hash, kind, command);
    });
  }

  removeName(path: string, hash: string, command: string) {
    this.guarded(path);
    this.transaction(() => {
      const b = this.branch().id;
      const row = this.sql(
        "SELECT kind FROM names WHERE branch_id = ? AND path = ? AND hash = ?",
      ).get(b, path, hash) as { kind: DefKind } | undefined;
      if (!row) return;
      this.sql("DELETE FROM names WHERE branch_id = ? AND path = ? AND hash = ?").run(
        b,
        path,
        hash,
      );
      this.log("delete", path, hash, null, row.kind, command);
    });
  }

  /** Point `path` at `newHash`, replacing whatever it pointed to. */
  updateName(path: string, newHash: string, kind: DefKind, command: string) {
    this.guarded(path);
    this.transaction(() => {
      const b = this.branch().id;
      const old = this.hashesAt(path);
      this.sql("DELETE FROM names WHERE branch_id = ? AND path = ?").run(b, path);
      this.sql("INSERT INTO names (branch_id, path, hash, kind) VALUES (?, ?, ?, ?)").run(
        b,
        path,
        newHash,
        kind,
      );
      for (const o of old.length ? old : [null])
        this.log("update", path, o, newHash, kind, command);
    });
  }

  /** Swap one hash on `path` for another, leaving any other hashes there (a conflict) alone. */
  replaceName(path: string, oldHash: string, newHash: string, kind: DefKind, command: string) {
    this.guarded(path);
    this.transaction(() => {
      const b = this.branch().id;
      const r = this.sql("DELETE FROM names WHERE branch_id = ? AND path = ? AND hash = ?").run(
        b,
        path,
        oldHash,
      );
      if (!r.changes) return;
      this.sql("INSERT OR IGNORE INTO names (branch_id, path, hash, kind) VALUES (?, ?, ?, ?)").run(
        b,
        path,
        newHash,
        kind,
      );
      this.log("update", path, oldHash, newHash, kind, command);
    });
  }

  private log(
    op: LogEntry["op"],
    path: string,
    oldHash: string | null,
    newHash: string | null,
    kind: DefKind,
    command: string,
    branchId = this.branch().id,
  ) {
    this.changing();
    this.dirty.add(branchId);
    this.batch ??= (
      this.sql("SELECT coalesce(max(batch), 0) + 1 AS b FROM name_log").get() as { b: number }
    ).b;
    this.sql(
      "INSERT INTO name_log (branch_id, ts, op, path, old_hash, new_hash, kind, command, batch) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
    ).run(branchId, Date.now(), op, path, oldHash, newHash, kind, command, this.batch);
  }

  // ---- history ---------------------------------------------------------------

  /** A branch's head causal (default: the current branch's). */
  head(branch?: string): string {
    const id = branch === undefined ? this.branch().id : this.branchId(branch);
    return (this.sql("SELECT head FROM branches WHERE id = ?").get(id) as { head: string }).head;
  }

  causal(hash: string): Causal | undefined {
    const row = this.sql("SELECT * FROM causals WHERE hash = ?").get(hash) as Row | undefined;
    if (!row) return undefined;
    // in the order they were recorded: a merge's own branch first
    const parents = (
      this.sql("SELECT parent FROM causal_parents WHERE child = ? ORDER BY rowid").all(hash) as {
        parent: string;
      }[]
    ).map((r) => r.parent);
    return {
      hash,
      namespace: row.namespace as string,
      parents,
      ts: row.ts as number,
      ...readEntryText(row.command as string),
    };
  }

  hasCausal(hash: string): boolean {
    return !!this.sql("SELECT 1 FROM causals WHERE hash = ?").get(hash);
  }

  /** Causals whose hash starts with `prefix`. */
  causalsWithPrefix(prefix: string): string[] {
    return (
      this.sql("SELECT hash FROM causals WHERE hash LIKE ? LIMIT 10").all(`${prefix}%`) as {
        hash: string;
      }[]
    ).map((r) => r.hash);
  }

  /** Store a namespace for these names (and the text versions they show), node by node; its hash. */
  namespaceOf(rows: { path: string; hash: string; text?: string }[]): string {
    interface Tree {
      names: Map<string, Set<string>>;
      texts: [string, string, string][];
      children: Map<string, Tree>;
    }
    const tree = (): Tree => ({ names: new Map(), texts: [], children: new Map() });
    const root = tree();
    for (const r of rows) {
      const segs = r.path.split(".");
      let t = root;
      for (const seg of segs.slice(0, -1)) {
        if (!t.children.has(seg)) t.children.set(seg, tree());
        t = t.children.get(seg)!;
      }
      const last = segs.at(-1)!;
      t.names.set(last, (t.names.get(last) ?? new Set()).add(r.hash));
      if (r.text && !t.texts.some(([n, h]) => n === last && h === r.hash))
        t.texts.push([last, r.hash, r.text]);
    }
    const put = this.sql("INSERT OR IGNORE INTO namespaces (hash, body) VALUES (?, ?)");
    const write = (t: Tree): string => {
      const names = [...t.names]
        .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
        .map(([n, hs]): [string, string[]] => [n, [...hs].sort()]);
      const children = [...t.children]
        .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
        .map(([n, c]): [string, string] => [n, write(c)]);
      const texts = t.texts.sort(([a, x], [b, y]) =>
        a < b ? -1 : a > b ? 1 : x < y ? -1 : x > y ? 1 : 0,
      );
      // (a node showing no text versions is written as it was before there were any: its hash is the same)
      const body = JSON.stringify(
        (texts.length
          ? ["ns", names, children, texts]
          : ["ns", names, children]) satisfies NamespaceBody,
      );
      const h = namespaceHash(body);
      put.run(h, body);
      return h;
    };
    return write(root);
  }

  /** The names a stored namespace holds, with each definition's kind (and the text version it shows, if any). */
  namespaceRows(namespace: string): NameRow[] {
    const kind = this.sql("SELECT kind FROM definitions WHERE hash = ?");
    const out: NameRow[] = [];
    const walk = (h: string, prefix: string) => {
      const row = this.sql("SELECT body FROM namespaces WHERE hash = ?").get(h) as
        | { body: string }
        | undefined;
      if (!row) throw new Error(`namespace ${h.slice(0, 8)} is missing from this codebase`);
      const [, names, children, texts] = JSON.parse(row.body) as NamespaceBody;
      const shown = new Map((texts ?? []).map(([n, d, t]) => [`${n} ${d}`, t]));
      for (const [n, hs] of names)
        for (const d of hs) {
          const text = shown.get(`${n} ${d}`);
          out.push({
            path: prefix + n,
            hash: d,
            kind: ((kind.get(d) as { kind: DefKind } | undefined)?.kind ?? "term") as DefKind,
            ...(text ? { text } : {}),
          });
        }
      for (const [n, c] of children) walk(c, `${prefix}${n}.`);
    };
    walk(namespace, "");
    return out.sort((a, b) =>
      a.path < b.path ? -1 : a.path > b.path ? 1 : a.hash < b.hash ? -1 : 1,
    );
  }

  /** Record a causal (its hash follows from namespace and parents); returns the hash. */
  putCausal(
    namespace: string,
    parents: string[],
    ts: number,
    command: string,
    message?: string,
  ): string {
    const h = causalHash(namespace, parents);
    const r = this.sql(
      "INSERT OR IGNORE INTO causals (hash, namespace, ts, command) VALUES (?, ?, ?, ?)",
    ).run(h, namespace, ts, entryText(command, message));
    if (r.changes)
      for (const p of new Set(parents))
        this.sql("INSERT OR IGNORE INTO causal_parents (child, parent) VALUES (?, ?)").run(h, p);
    return h;
  }

  /**
   * Say what a history entry is for: its message (none takes it away). The message is in no hash, so the entry, and
   * all history after it, stay as they are; a codebase that has the entry already (a remote it was pushed to) keeps
   * the message it had.
   */
  describe(hash: string, message: string | undefined) {
    const c = this.causal(hash);
    if (!c) throw new Error(`no history entry ${hash.slice(0, 8)} in this codebase`);
    this.sql("UPDATE causals SET command = ? WHERE hash = ?").run(
      entryText(c.command, message),
      hash,
    );
  }

  /** Make the next causal on the current branch also descend from `causal` (a merge brings its history in). */
  addParent(causal: string) {
    this.changing();
    const b = this.branch().id;
    this.pendingParents.set(b, (this.pendingParents.get(b) ?? new Set()).add(causal));
    this.dirty.add(b);
  }

  /**
   * A new head causal for each branch whose names changed. Within a batch (a command) there is one: a later
   * transaction replaces the causal an earlier one made, keeping its parents.
   */
  private commitHistory() {
    for (const id of this.dirty) {
      const name = (
        this.sql("SELECT name FROM branches WHERE id = ?").get(id) as { name: string } | undefined
      )?.name;
      if (name === undefined) continue;
      const head = this.head(name);
      const current = this.causal(head)!;
      // (files kept as written are history like code: they travel with it; so do the text versions names show)
      const texts = this.textsOf(id);
      const namespace = this.namespaceOf(
        this.names(name, true).map((r) => {
          const text = texts.get(r.hash);
          return text ? { ...r, text } : r;
        }),
      );
      const extra = [...(this.pendingParents.get(id) ?? [])].filter((p) => p !== head);
      if (namespace === current.namespace && !extra.length) continue;
      const mine = this.made.get(id) === head;
      const parents = [...new Set([...(mine ? current.parents : [head]), ...extra])];
      const command = mine ? current.command : this.batchCommand(id);
      const message = this.message ?? (mine ? current.message : undefined);
      const next = this.putCausal(namespace, parents, Date.now(), command, message);
      // (the same change from the same point is an entry here already, as a branch's one step landed squashed is:
      // told what it's for, it says so)
      if (this.message && this.causal(next)!.message !== this.message)
        this.describe(next, this.message);
      this.sql("UPDATE branches SET head = ? WHERE id = ?").run(next, id);
      // the causal this one replaces was only ever this batch's: drop it unless something else points at it
      if (
        mine &&
        next !== head &&
        !this.sql("SELECT 1 FROM branches WHERE head = ?").get(head) &&
        !this.sql("SELECT 1 FROM causal_parents WHERE parent = ?").get(head)
      ) {
        this.sql("DELETE FROM causal_parents WHERE child = ?").run(head);
        this.sql("DELETE FROM causals WHERE hash = ?").run(head);
      }
      this.made.set(id, next);
      this.pendingParents.delete(id);
    }
    this.dirty.clear();
    // (what's recorded is read from the heads now; a version chosen for something no branch names is no change)
    this.pendingTexts.clear();
    this.replacedTexts.clear();
    this.shown = undefined;
  }

  /** The command that made this batch's changes on a branch: the first logged. */
  private batchCommand(branchId: number): string {
    const row = this.sql(
      "SELECT command FROM name_log WHERE branch_id = ? AND batch = ? ORDER BY id LIMIT 1",
    ).get(branchId, this.batch ?? -1) as { command: string } | undefined;
    return row?.command ?? "change";
  }

  /**
   * Point a branch at a causal this codebase has: its names become that causal's namespace (logged as changes,
   * like any), and its head becomes that causal, with no new history entry.
   */
  setHead(branch: string, causal: string, command: string, quiet = false) {
    const c = this.causal(causal);
    if (!c) throw new Error(`no history entry ${causal.slice(0, 8)} in this codebase`);
    this.ensureBranch(branch);
    const id = this.branchId(branch);
    this.transaction(() => {
      this.replaceBranchNames(id, this.namespaceRows(c.namespace), command, quiet);
      this.sql("UPDATE branches SET head = ? WHERE id = ?").run(causal, id);
      this.dirty.delete(id);
      this.made.delete(id);
      // (the text versions it shows are the causal's)
      this.replacedTexts.delete(id);
      this.pendingTexts.delete(id);
      this.shown = undefined;
    });
  }

  /** Whether `ancestor` is `of` or in its history. */
  isAncestor(ancestor: string, of: string): boolean {
    if (ancestor === of || ancestor === ROOT_CAUSAL) return true;
    const parents = this.sql("SELECT parent FROM causal_parents WHERE child = ?");
    const seen = new Set<string>();
    const queue = [of];
    while (queue.length) {
      const h = queue.pop()!;
      if (h === ancestor) return true;
      if (seen.has(h)) continue;
      seen.add(h);
      for (const r of parents.all(h) as { parent: string }[]) queue.push(r.parent);
    }
    return false;
  }

  /** Every causal in a causal's history, itself included (newest first, by time). */
  ancestors(of: string): string[] {
    const parents = this.sql("SELECT parent FROM causal_parents WHERE child = ?");
    const seen = new Set<string>();
    const queue = [of];
    while (queue.length) {
      const h = queue.pop()!;
      if (seen.has(h) || !this.hasCausal(h)) continue;
      seen.add(h);
      for (const r of parents.all(h) as { parent: string }[]) queue.push(r.parent);
    }
    return [...seen];
  }

  /**
   * The best common ancestors of two histories (each one causal or several), which a three-way merge starts from:
   * the causals both contain that aren't behind another such one. Usually one; criss-cross merges (two branches
   * that merged each other) leave several. Oldest first; with nothing in common, the root.
   */
  mergeBases(a: string | string[], b: string | string[]): string[] {
    const inA = new Set([a].flat().flatMap((h) => this.ancestors(h)));
    const parents = this.sql("SELECT parent FROM causal_parents WHERE child = ?");
    const candidates: string[] = [];
    const seen = new Set<string>();
    const queue = [b].flat();
    while (queue.length) {
      const h = queue.shift()!;
      if (seen.has(h)) continue;
      seen.add(h);
      if (inA.has(h)) {
        candidates.push(h);
        continue;
      }
      for (const r of parents.all(h) as { parent: string }[]) queue.push(r.parent);
    }
    const best = candidates.filter(
      (c) => !candidates.some((o) => o !== c && this.isAncestor(c, o)),
    );
    if (!best.length) return [ROOT_CAUSAL];
    return best
      .map((h) => this.causal(h)!)
      .sort((x, y) => x.ts - y.ts || (x.hash < y.hash ? -1 : 1))
      .map((c) => c.hash);
  }

  /** The newest of two causals' best common ancestors ({@link mergeBases}): where one branch left the other. */
  mergeBase(a: string, b: string): string {
    return this.mergeBases(a, b).at(-1)!;
  }

  /**
   * A branch's history from its head back, first parents only: what happened on it, with each merge as one step.
   * A merge's first parent is the head of the branch it was made on.
   */
  causalLog(limit = 50, branch?: string): Causal[] {
    const out: Causal[] = [];
    for (
      let h: string | undefined = this.head(branch);
      h && out.length < limit;
      h = out.at(-1)!.parents[0]
    ) {
      const c = this.causal(h);
      if (!c) break;
      out.push(c);
    }
    return out;
  }

  // ---- history, between codebases -------------------------------------------

  missingCausals(hashes: string[]): string[] {
    return hashes.filter((h) => !this.hasCausal(h));
  }

  missingNamespaces(hashes: string[]): string[] {
    const q = this.sql("SELECT 1 FROM namespaces WHERE hash = ?");
    return hashes.filter((h) => !q.get(h));
  }

  /** Causals to send another codebase: each message in its command, as the column holds it, so any tcm keeps it. */
  getCausals(hashes: string[]): Causal[] {
    return hashes
      .map((h) => this.causal(h))
      .filter((c): c is Causal => !!c)
      .map(({ message, ...c }) => ({ ...c, command: entryText(c.command, message) }));
  }

  getNamespaces(hashes: string[]): { hash: string; body: string }[] {
    const q = this.sql("SELECT body FROM namespaces WHERE hash = ?");
    return hashes.flatMap((h) => {
      const r = q.get(h) as { body: string } | undefined;
      return r ? [{ hash: h, body: r.body }] : [];
    });
  }

  /** The nodes a namespace node points at. */
  namespaceChildren(hash: string): string[] {
    const r = this.sql("SELECT body FROM namespaces WHERE hash = ?").get(hash) as
      | { body: string }
      | undefined;
    return r ? (JSON.parse(r.body) as NamespaceBody)[2].map(([, c]) => c) : [];
  }

  /** The definitions a namespace node names directly. */
  namespaceDefinitions(hash: string): string[] {
    const r = this.sql("SELECT body FROM namespaces WHERE hash = ?").get(hash) as
      | { body: string }
      | undefined;
    return r ? (JSON.parse(r.body) as NamespaceBody)[1].flatMap(([, hs]) => hs) : [];
  }

  /** Namespace nodes from another codebase: each must hash to what it claims, and be well formed. */
  putNamespaces(rows: { hash: string; body: string }[]) {
    this.transaction(() => {
      for (const r of rows) {
        if (namespaceHash(r.body) !== r.hash)
          throw new Error(
            `namespace ${String(r.hash).slice(0, 8)} doesn't hash to its claimed hash`,
          );
        const b = JSON.parse(r.body) as NamespaceBody;
        if (b[0] !== "ns" || !Array.isArray(b[1]) || !Array.isArray(b[2]))
          throw new Error(`namespace ${r.hash.slice(0, 8)} is malformed`);
        this.sql("INSERT OR IGNORE INTO namespaces (hash, body) VALUES (?, ?)").run(r.hash, r.body);
      }
    });
  }

  /** Causals from another codebase, oldest first: each must hash to what it claims, with its namespace and parents here. */
  putCausals(rows: Causal[]) {
    this.transaction(() => {
      for (const c of rows) {
        if (causalHash(c.namespace, c.parents) !== c.hash)
          throw new Error(
            `history entry ${String(c.hash).slice(0, 8)} doesn't hash to its claimed hash`,
          );
        if (this.missingNamespaces([c.namespace]).length)
          throw new Error(`history entry ${c.hash.slice(0, 8)}: its namespace wasn't sent`);
        for (const p of c.parents)
          if (!this.hasCausal(p))
            throw new Error(
              `history entry ${c.hash.slice(0, 8)}: its parent ${p.slice(0, 8)} wasn't sent`,
            );
        this.putCausal(
          c.namespace,
          c.parents,
          Number(c.ts) || 0,
          String(c.command ?? ""),
          typeof c.message === "string" ? c.message : undefined,
        );
      }
    });
  }

  /**
   * Make a branch's names exactly `rows`, and the text versions they show exactly theirs, logging each change on that
   * branch. `texts`: the definitions whose text changed, that are still named.
   */
  private replaceBranchNames(
    id: number,
    rows: NameRow[],
    command: string,
    // (moving a head and back, as a merge not finished is stepped into for a command: no names changed for good)
    quiet = false,
  ): { added: number; removed: number; texts: number } {
    this.changing();
    const key = (r: { path: string; hash: string }) => `${r.path} ${r.hash}`;
    const current = new Map(
      (
        this.sql("SELECT path, hash, kind FROM names WHERE branch_id = ?").all(
          id,
        ) as unknown as NameRow[]
      ).map((r) => [key(r), r]),
    );
    const wanted = new Map(rows.map((r) => [key(r), r]));
    let added = 0;
    let removed = 0;
    const was = this.textsOf(id);
    const shown = new Map(rows.flatMap((r) => (r.text ? [[r.hash, r.text] as const] : [])));
    const retexted = new Set<string>();
    for (const [k, r] of wanted)
      if (current.has(k) && was.get(r.hash) !== shown.get(r.hash)) {
        if (!quiet) this.log("update", r.path, r.hash, r.hash, r.kind, command, id);
        retexted.add(r.hash);
      }
    this.replacedTexts.set(id, shown);
    this.pendingTexts.delete(id);
    this.shown = undefined;
    for (const [k, r] of current) {
      if (wanted.has(k)) continue;
      this.sql("DELETE FROM names WHERE branch_id = ? AND path = ? AND hash = ?").run(
        id,
        r.path,
        r.hash,
      );
      if (!quiet) this.log("delete", r.path, r.hash, null, r.kind, command, id);
      removed++;
    }
    for (const [k, r] of wanted) {
      if (current.has(k)) continue;
      this.sql("INSERT INTO names (branch_id, path, hash, kind) VALUES (?, ?, ?, ?)").run(
        id,
        r.path,
        r.hash,
        r.kind,
      );
      if (!quiet) this.log("add", r.path, null, r.hash, r.kind, command, id);
      added++;
    }
    return { added, removed, texts: retexted.size };
  }

  // ---- releases ------------------------------------------------------------

  release(pkg: string, version: string): Release | undefined {
    return this.sql("SELECT * FROM releases WHERE package = ? AND version = ?").get(
      pkg,
      version,
    ) as unknown as Release | undefined;
  }

  /** Releases (of one package, or all), newest first. */
  releases(pkg?: string): Release[] {
    const q =
      "SELECT * FROM releases" +
      (pkg === undefined ? "" : " WHERE package = ?") +
      " ORDER BY ts DESC, version DESC";
    return (pkg === undefined ? this.sql(q).all() : this.sql(q).all(pkg)) as unknown as Release[];
  }

  /** Record a release; one that exists must be identical (a version names one thing, for good). */
  putRelease(r: Release) {
    const had = this.release(r.package, r.version);
    if (had) {
      if (had.causal !== r.causal || had.notes !== r.notes)
        throw new Error(
          `${r.package}@${r.version} is already released here as ${had.causal.slice(0, 8)}, differently; a version can't change`,
        );
      return;
    }
    if (!this.hasCausal(r.causal))
      throw new Error(
        `release ${r.package}@${r.version}: its history entry ${r.causal.slice(0, 8)} isn't here`,
      );
    this.sql(
      "INSERT INTO releases (package, version, namespace, causal, notes, author, ts) VALUES (?, ?, ?, ?, ?, ?, ?)",
    ).run(r.package, r.version, r.namespace, r.causal, r.notes, r.author, r.ts);
  }

  // ---- contributions -------------------------------------------------------

  createContribution(c: {
    title: string;
    description: string;
    source: string;
    target: string;
    author: string;
  }): number {
    const now = Date.now();
    return Number(
      this.sql(
        "INSERT INTO contributions (title, description, source, target, author, status, created, updated) VALUES (?, ?, ?, ?, ?, 'open', ?, ?)",
      ).run(c.title, c.description, c.source, c.target, c.author, now, now).lastInsertRowid,
    );
  }

  contribution(id: number): Contribution | undefined {
    const r = this.sql("SELECT * FROM contributions WHERE id = ?").get(id) as Row | undefined;
    return r && contributionOf(r);
  }

  /** Contributions, newest first; by status if given. */
  contributions(status?: Contribution["status"]): Contribution[] {
    const rows = (
      status
        ? this.sql("SELECT * FROM contributions WHERE status = ? ORDER BY id DESC").all(status)
        : this.sql("SELECT * FROM contributions ORDER BY id DESC").all()
    ) as Row[];
    return rows.map(contributionOf);
  }

  /** The open contribution of `source` into `target`, if there is one. */
  openContribution(source: string, target: string): Contribution | undefined {
    const r = this.sql(
      "SELECT * FROM contributions WHERE source = ? AND target = ? AND status = 'open'",
    ).get(source, target) as Row | undefined;
    return r && contributionOf(r);
  }

  setContributionStatus(
    id: number,
    status: Contribution["status"],
    merged?: { from: string; as: string },
  ) {
    this.sql(
      "UPDATE contributions SET status = ?, merged_from = ?, merged_as = ?, updated = ? WHERE id = ?",
    ).run(status, merged?.from ?? null, merged?.as ?? null, Date.now(), id);
  }

  touchContribution(id: number) {
    this.sql("UPDATE contributions SET updated = ? WHERE id = ?").run(Date.now(), id);
  }

  addComment(
    contribution: number,
    c: { author: string; body: string; about?: string | null },
  ): number {
    this.touchContribution(contribution);
    return Number(
      this.sql(
        "INSERT INTO contribution_comments (contribution, author, body, about, ts) VALUES (?, ?, ?, ?, ?)",
      ).run(contribution, c.author, c.body, c.about ?? null, Date.now()).lastInsertRowid,
    );
  }

  comments(contribution: number): Comment[] {
    return this.sql(
      "SELECT id, author, body, about, ts FROM contribution_comments WHERE contribution = ? ORDER BY id",
    ).all(contribution) as unknown as Comment[];
  }

  /** The most recent batch of changes on this branch: its first entry and the command that made it. */
  lastBatch(): { firstId: number; command: string } | undefined {
    const b = this.branch().id;
    const { first } = this.sql(
      "SELECT min(id) AS first FROM name_log WHERE branch_id = ? AND batch = (SELECT max(batch) FROM name_log WHERE branch_id = ?)",
    ).get(b, b) as { first: number | null };
    if (first === null) return undefined;
    const { command } = this.sql("SELECT command FROM name_log WHERE id = ?").get(first) as {
      command: string;
    };
    return { firstId: first, command };
  }

  logHead(): number {
    return ((this.sql("SELECT max(id) AS id FROM name_log").get() as { id: number | null }).id ??
      0) as number;
  }

  /** This branch's names whose definitions have docs, with them: one query, not a record each. */
  documentedNames(): { path: string; hash: string; doc: string }[] {
    const rows = this.sql(
      "SELECT n.path, n.hash, m.value AS doc FROM names n JOIN metadata m ON m.hash = n.hash AND m.key = 'doc' WHERE n.branch_id = ? AND n.kind != 'file' ORDER BY n.path, n.hash",
    ).all(this.branch().id) as { path: string; hash: string; doc: string }[];
    // (a definition showing another text version has that version's doc, or none)
    const texts = this.shownTexts();
    if (!texts.size) return rows;
    const out = rows.filter((r) => !texts.has(r.hash));
    for (const n of this.names())
      if (texts.has(n.hash)) {
        const doc = this.getDef(n.hash)?.doc;
        if (doc) out.push({ path: n.path, hash: n.hash, doc });
      }
    return out.sort((a, b) =>
      a.path < b.path ? -1 : a.path > b.path ? 1 : a.hash < b.hash ? -1 : 1,
    );
  }

  /**
   * Named definitions that use a definition nothing names on this branch (an old version, or one removed with
   * `--force`): each such use, with the name the used one was made as.
   */
  unnamedUses(): { path: string; hash: string; uses: string; madeAs: string }[] {
    return this.sql(
      "SELECT DISTINCT n.path, n.hash, substr(d.to_ref, 2) AS uses, coalesce(u.name_hint, '?') AS madeAs FROM names n JOIN deps d ON d.from_hash = n.hash AND d.to_ref LIKE '#%' LEFT JOIN definitions u ON u.hash = substr(d.to_ref, 2) WHERE n.branch_id = ? AND NOT EXISTS (SELECT 1 FROM names m WHERE m.branch_id = n.branch_id AND m.hash = substr(d.to_ref, 2)) ORDER BY n.path",
    ).all(this.branch().id) as { path: string; hash: string; uses: string; madeAs: string }[];
  }

  /** The names that changed on this branch after log entry `afterId`: each once. */
  pathsChangedSince(afterId: number): string[] {
    return (
      this.sql("SELECT DISTINCT path FROM name_log WHERE branch_id = ? AND id > ?").all(
        this.branch().id,
        afterId,
      ) as { path: string }[]
    ).map((r) => r.path);
  }

  /** Id of the latest name change on a branch, 0 if none. */
  branchLogHead(branch: string): number {
    return ((
      this.sql("SELECT max(id) AS id FROM name_log WHERE branch_id = ?").get(
        this.branchId(branch),
      ) as { id: number | null }
    ).id ?? 0) as number;
  }

  /** Name changes on a branch (default: the current one), newest first. */
  history(limit = 50, afterId = 0, branch?: string): LogEntry[] {
    const id = branch === undefined ? this.branch().id : this.branchId(branch);
    return (
      this.sql(
        "SELECT * FROM name_log WHERE branch_id = ? AND id > ? ORDER BY id DESC LIMIT ?",
      ).all(id, afterId, limit) as Row[]
    ).map((r) => ({
      id: r.id as number,
      batch: r.batch as number,
      ts: r.ts as number,
      op: r.op as LogEntry["op"],
      path: r.path as string,
      oldHash: r.old_hash as string | null,
      newHash: r.new_hash as string | null,
      kind: r.kind as DefKind,
      command: r.command as string,
    }));
  }

  getSetting(key: string): string | undefined {
    return (
      this.sql("SELECT value FROM settings WHERE key = ?").get(key) as { value: string } | undefined
    )?.value;
  }

  settings(prefix: string): [string, string][] {
    return (
      this.sql("SELECT key, value FROM settings WHERE key LIKE ? ORDER BY key").all(
        `${prefix}%`,
      ) as { key: string; value: string }[]
    ).map((r) => [r.key, r.value]);
  }

  deleteSetting(key: string) {
    this.sql("DELETE FROM settings WHERE key = ?").run(key);
  }

  setSetting(key: string, value: string) {
    this.sql("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)").run(key, value);
  }

  // ---- lint results --------------------------------------------------------------------------------------------

  lintResult(hash: string, key: string): string | undefined {
    return (
      this.sql("SELECT issues FROM lint_results WHERE hash = ? AND key = ?").get(hash, key) as
        | { issues: string }
        | undefined
    )?.issues;
  }

  /** The keys a definition has lint findings under (the linter's version and configuration): what another codebase could use. */
  lintKeysOf(hash: string): string[] {
    return (
      this.sql("SELECT key FROM lint_results WHERE hash = ? ORDER BY key").all(hash) as {
        key: string;
      }[]
    ).map((r) => r.key);
  }

  putLintResult(hash: string, key: string, issues: string) {
    this.sql("INSERT OR REPLACE INTO lint_results (hash, key, issues) VALUES (?, ?, ?)").run(
      hash,
      key,
      issues,
    );
  }

  // ---- the package lock ----------------------------------------------------------------------------------------

  /** The version the codebase runs for package `name` within compatible range `range`. */
  lockedVersion(name: string, range: string): string | undefined {
    return (
      this.sql("SELECT version FROM packages WHERE name = ? AND range = ?").get(name, range) as
        | { version: string }
        | undefined
    )?.version;
  }

  /** Lock `name` within `range` to `version`; the integrity recorded for a previous version doesn't carry over. */
  lockVersion(name: string, range: string, version: string, integrity?: string) {
    const was = this.sql(
      "SELECT version, integrity FROM packages WHERE name = ? AND range = ?",
    ).get(name, range) as { version: string; integrity: string | null } | undefined;
    const keep = was?.version === version ? (was.integrity ?? undefined) : undefined;
    this.sql(
      "INSERT OR REPLACE INTO packages (name, range, version, integrity) VALUES (?, ?, ?, ?)",
    ).run(name, range, version, integrity ?? keep ?? null);
  }

  /** The integrity recorded for the locked version of `name` within `range`. */
  lockedIntegrity(name: string, range: string): string | undefined {
    return (
      (
        this.sql("SELECT integrity FROM packages WHERE name = ? AND range = ?").get(name, range) as
          | { integrity: string | null }
          | undefined
      )?.integrity ?? undefined
    );
  }

  /**
   * The ranges of package `name` that named definitions use (all, or those at `prefix` and below), with how many
   * definitions use each.
   */
  packageRanges(name: string, prefix = ""): Map<string, number> {
    const rows = this.sql(
      `SELECT d.to_ref AS ref, COUNT(DISTINCT n.hash) AS n FROM names n JOIN deps d ON d.from_hash = n.hash
         WHERE n.branch_id = ? AND (? = '' OR n.path = ? OR n.path LIKE ?) AND d.to_ref LIKE ? GROUP BY d.to_ref`,
    ).all(this.branch().id, prefix, prefix, `${prefix}.%`, `npm:${name}@%`) as {
      ref: string;
      n: number;
    }[];
    const out = new Map<string, number>();
    for (const r of rows) {
      const range = /^[^/#]+/.exec(r.ref.slice(`npm:${name}@`.length))![0];
      out.set(range, (out.get(range) ?? 0) + r.n);
    }
    return out;
  }

  /** Every locked package, by name then range. */
  lockedPackages(): { name: string; range: string; version: string; integrity?: string }[] {
    return (
      this.sql(
        "SELECT name, range, version, integrity FROM packages ORDER BY name, range",
      ).all() as { name: string; range: string; version: string; integrity: string | null }[]
    ).map((r) => ({
      name: r.name,
      range: r.range,
      version: r.version,
      ...(r.integrity ? { integrity: r.integrity } : {}),
    }));
  }

  // ---- test results ----------------------------------------------------------

  /** A cached result. A test's hash covers everything it depends on, so a result stays valid for that hash. */
  testResult(hash: string, nodeVersion: string): TestResult | undefined {
    const r = this.sql("SELECT * FROM test_results WHERE hash = ? AND node_version = ?").get(
      hash,
      nodeVersion,
    ) as Row | undefined;
    return (
      r && {
        hash,
        ok: r.ok === 1,
        message: r.message as string,
        output: r.output as string,
        ms: r.duration_ms as number,
      }
    );
  }

  putTestResult(r: TestResult, nodeVersion: string) {
    this.sql(
      "INSERT OR REPLACE INTO test_results (hash, node_version, ok, message, output, duration_ms, ts) VALUES (?, ?, ?, ?, ?, ?, ?)",
    ).run(r.hash, nodeVersion, r.ok ? 1 : 0, r.message, r.output, r.ms, Date.now());
  }

  /** The keys a test has results under (runtimes, test frameworks, platforms): what another codebase could use. */
  resultKeysOf(hash: string): string[] {
    return (
      this.sql(
        "SELECT node_version AS key FROM test_results WHERE hash = ? ORDER BY node_version",
      ).all(hash) as {
        key: string;
      }[]
    ).map((r) => r.key);
  }

  /** A watch expression's cached value: like a test, its hash covers everything it evaluates. */
  watchResult(hash: string, nodeVersion: string): { ok: boolean; value: string } | undefined {
    const r = this.sql(
      "SELECT ok, value FROM watch_results WHERE hash = ? AND node_version = ?",
    ).get(hash, nodeVersion) as Row | undefined;
    return r && { ok: r.ok === 1, value: r.value as string };
  }

  putWatchResult(hash: string, nodeVersion: string, r: { ok: boolean; value: string }) {
    this.sql(
      "INSERT OR REPLACE INTO watch_results (hash, node_version, ok, value, ts) VALUES (?, ?, ?, ?, ?)",
    ).run(hash, nodeVersion, r.ok ? 1 : 0, r.value, Date.now());
  }

  // ---- patches & todo --------------------------------------------------------

  /**
   * Record that `newHash` replaces `oldHash`. A version can come back (a change undone hashes as the code before it
   * did): what replaced it then doesn't now, so that patch goes.
   */
  addPatch(oldHash: string, newHash: string) {
    this.changing();
    const branch = this.branch().id;
    this.transaction(() => {
      if (newHash !== oldHash)
        this.sql("DELETE FROM patches WHERE branch_id = ? AND old_hash = ?").run(branch, newHash);
      this.sql(
        "INSERT OR REPLACE INTO patches (branch_id, old_hash, new_hash, ts) VALUES (?, ?, ?, ?)",
      ).run(branch, oldHash, newHash, Date.now());
    });
  }

  /**
   * Follow this branch's patches from `hash` to the newest replacement: as far as one the branch names, which is
   * current (a patch out of it is from before it came back, or a reset named it again).
   */
  patched(hash: string): string {
    const stmt = this.sql("SELECT new_hash FROM patches WHERE branch_id = ? AND old_hash = ?");
    const named = this.sql("SELECT 1 FROM names WHERE branch_id = ? AND hash = ? LIMIT 1");
    const seen = new Set<string>();
    for (
      let row;
      !seen.has(hash) &&
      !named.get(this.branch().id, hash) &&
      (row = stmt.get(this.branch().id, hash) as { new_hash: string } | undefined);
    ) {
      seen.add(hash);
      hash = row.new_hash;
    }
    return hash;
  }

  setTodo(hash: string, message: string) {
    this.changing();
    this.sql("INSERT OR REPLACE INTO todo (branch_id, hash, message) VALUES (?, ?, ?)").run(
      this.branch().id,
      hash,
      message,
    );
  }

  clearTodo(hash: string) {
    this.changing();
    this.sql("DELETE FROM todo WHERE branch_id = ? AND hash = ?").run(this.branch().id, hash);
  }

  todos(): { hash: string; message: string }[] {
    return this.sql("SELECT hash, message FROM todo WHERE branch_id = ?").all(this.branch().id) as {
      hash: string;
      message: string;
    }[];
  }

  /**
   * A branch's patches and what it has to fix, as they are, and put back: what a merge not finished keeps apart from
   * the branch it's into ({@link setHead} moves only names and history).
   */
  workOf(branch: string): {
    patches: { old: string; new: string; ts: number }[];
    todo: { hash: string; message: string }[];
  } {
    const id = this.branchId(branch);
    return {
      patches: (
        this.sql("SELECT old_hash, new_hash, ts FROM patches WHERE branch_id = ?").all(id) as {
          old_hash: string;
          new_hash: string;
          ts: number;
        }[]
      ).map((r) => ({ old: r.old_hash, new: r.new_hash, ts: r.ts })),
      todo: this.sql("SELECT hash, message FROM todo WHERE branch_id = ?").all(id) as {
        hash: string;
        message: string;
      }[],
    };
  }

  /** Put back what {@link workOf} said of a branch. */
  setWorkOf(branch: string, work: ReturnType<Store["workOf"]>) {
    this.changing();
    const id = this.branchId(branch);
    this.transaction(() => {
      this.sql("DELETE FROM patches WHERE branch_id = ?").run(id);
      for (const p of work.patches)
        this.sql("INSERT INTO patches (branch_id, old_hash, new_hash, ts) VALUES (?, ?, ?, ?)").run(
          id,
          p.old,
          p.new,
          p.ts,
        );
      this.sql("DELETE FROM todo WHERE branch_id = ?").run(id);
      for (const t of work.todo)
        this.sql("INSERT INTO todo (branch_id, hash, message) VALUES (?, ?, ?)").run(
          id,
          t.hash,
          t.message,
        );
    });
  }
}

uses

used by