{"version":3,"sources":["../src/identity/rebuild.ts","../src/identity/types.ts","../src/backend/migrate-vectors.ts"],"names":["storeRuntime","ValidationError","DEFAULT_EMBEDDING_METRIC","DEFAULT_EMBEDDING_INDEX_TYPE","LEGACY_EMBEDDINGS_TABLE_NAME","createDataKeyedBag","isMissingTableError","embedding","EmbeddingDimensionChangedError","requireDefined","sql","asCompiledRowsSql"],"mappings":"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","file":"index.cjs","sourcesContent":["import { type GraphDef, type GraphIdentityConfig } from \"../core/define-graph\";\nimport { storeRuntime } from \"../store/runtime-port\";\nimport { type Store } from \"../store/store\";\n\n/**\n * Rebuilds the derived current identity closure for an identity-enabled store.\n * This is a repair operation: it does not change graph revision or assertions.\n */\nexport async function rebuildIdentityClosure<\n  G extends GraphDef & Readonly<{ identity: GraphIdentityConfig }>,\n>(store: Store<G>): Promise<void> {\n  await storeRuntime(store).rebuildIdentityClosure();\n}\n","import {\n  type AllNodeTypes,\n  type GraphDef,\n  type NodeKinds,\n} from \"../core/define-graph\";\nimport { ValidationError } from \"../errors\";\nimport {\n  type DynamicNode,\n  type DynamicNodeReference,\n  type GraphNodeReference,\n  type Node,\n  type NodeRef,\n} from \"../store/types\";\n\n/**\n * The accepted *input* form for every identity facade method: a whole node or\n * a `{ kind, id }` pair for any compile-time graph kind, plus a proof-bearing\n * node or reference produced through the runtime collection lane. Runtime\n * values stay nominal so accepting them does not make an arbitrary\n * `{ kind: string, id: string }` object type-safe.\n *\n * Identity results use {@link IdentityNodeReference}, which honestly includes\n * runtime kinds because an evolved kind can belong to a class reached from a\n * compile-time node.\n */\nexport type IdentityNodeRefInput<G extends GraphDef> =\n  NodeRef<AllNodeTypes<G>> | DynamicNode | DynamicNodeReference;\n\n/**\n * A node reference returned by Operational Identity.\n *\n * Identity classes can contain runtime-evolved kinds even when a read starts\n * from a compile-time node, so results honestly include both lanes.\n */\nexport type IdentityNodeReference<G extends GraphDef> =\n  GraphNodeReference<G> | DynamicNodeReference;\n\n/** A hydrated compile-time or runtime identity member. */\nexport type IdentityNode<G extends GraphDef> =\n  | {\n      [K in NodeKinds<G>]: Node<G[\"nodes\"][K][\"type\"]>;\n    }[NodeKinds<G>]\n  | DynamicNode;\n\ndeclare const __identityAssertionId: unique symbol;\n\nexport type IdentityAssertionId = string &\n  Readonly<{ [__identityAssertionId]: true }>;\n\n/**\n * Brands a non-empty string as an {@link IdentityAssertionId}.\n *\n * Use this when a persisted identity assertion id has round-tripped through\n * untyped storage or an external boundary and must be passed back to a\n * retraction surface such as `retractAssertion` or `bulkRetractAssertions`.\n * Mirrors the `asNodeId` / `asEdgeId` precedent.\n *\n * @throws {ValidationError} when `value` is empty.\n */\nexport function asIdentityAssertionId(value: string): IdentityAssertionId {\n  if (value.length === 0) {\n    throw new ValidationError(\n      \"asIdentityAssertionId must be a non-empty string.\",\n      {\n        issues: [\n          {\n            path: \"asIdentityAssertionId\",\n            message: \"Expected a non-empty string.\",\n          },\n        ],\n      },\n      {\n        suggestion: \"Use a persisted identity assertion id value.\",\n      },\n    );\n  }\n  return value as IdentityAssertionId;\n}\n\n/**\n * What one assertion claims about a pair of nodes: `\"same\"` merges them into\n * one equivalence set, `\"different\"` records a disjointness that a later\n * `\"same\"` claim must not contradict.\n */\nexport type IdentityRelation = \"same\" | \"different\";\n\n/**\n * One persisted identity claim about an ordered pair of nodes. Returned by the\n * assertion writers and by `identity.assertionsOf(...)`; `validTo` is set when\n * the assertion has been retracted, so a historical read can still see it.\n */\nexport type IdentityAssertion<G extends GraphDef> = Readonly<{\n  id: IdentityAssertionId;\n  relation: IdentityRelation;\n  a: IdentityNodeReference<G>;\n  b: IdentityNodeReference<G>;\n  validFrom: string;\n  validTo?: string;\n}>;\n\n/** Result of an idempotent assertion write. */\nexport type IdentityAssertionResult<G extends GraphDef> = Readonly<{\n  assertion: IdentityAssertion<G>;\n  action: \"created\" | \"existing\";\n}>;\n\n/** The half-open effective-time window of an identity assertion. */\nexport type IdentityValidityWindow = Readonly<{\n  validFrom?: string;\n  validTo?: string;\n}>;\n\n/** One ordered node pair handed to `bulkAssertSame` / `bulkAssertDifferent`. */\nexport type IdentityPair<G extends GraphDef> = Readonly<{\n  a: IdentityNodeRefInput<G>;\n  b: IdentityNodeRefInput<G>;\n}> &\n  IdentityValidityWindow;\n\n/**\n * The read half of the identity surface: equivalence-set membership,\n * representative selection, and the assertions behind them.\n *\n * Obtained from a read-only lens — `store.asOf(...).identity`,\n * `store.snapshot().identity` — where it answers at that view's coordinate.\n * The full read+write surface is {@link IdentityFacade}.\n */\nexport type IdentityReadFacade<G extends GraphDef> = Readonly<{\n  representativeOf: (\n    ref: IdentityNodeRefInput<G>,\n  ) => Promise<IdentityNodeReference<G> | undefined>;\n  membersOf: (\n    ref: IdentityNodeRefInput<G>,\n  ) => Promise<readonly IdentityNodeReference<G>[]>;\n  nodesOf: (\n    ref: IdentityNodeRefInput<G>,\n  ) => Promise<readonly IdentityNode<G>[]>;\n  areSame: (\n    a: IdentityNodeRefInput<G>,\n    b: IdentityNodeRefInput<G>,\n  ) => Promise<boolean>;\n  areDifferent: (\n    a: IdentityNodeRefInput<G>,\n    b: IdentityNodeRefInput<G>,\n  ) => Promise<boolean>;\n  assertionsOf: (\n    ref: IdentityNodeRefInput<G>,\n  ) => Promise<readonly IdentityAssertion<G>[]>;\n}>;\n\n/**\n * The full TypeGraph Identity Profile surface: {@link IdentityReadFacade} plus\n * the assertion writers and retractions.\n *\n * Obtained from `store.identity` or `tx.identity`, and present only on graphs\n * that declared `identity: { ... }` in `defineGraph`. Every writer is\n * idempotent — re-asserting an existing claim returns it with\n * `action: \"existing\"` rather than duplicating it.\n */\nexport type IdentityFacade<G extends GraphDef> = IdentityReadFacade<G> &\n  Readonly<{\n    assertSame: (\n      a: IdentityNodeRefInput<G>,\n      b: IdentityNodeRefInput<G>,\n      window?: IdentityValidityWindow,\n    ) => Promise<IdentityAssertionResult<G>>;\n    assertDifferent: (\n      a: IdentityNodeRefInput<G>,\n      b: IdentityNodeRefInput<G>,\n      window?: IdentityValidityWindow,\n    ) => Promise<IdentityAssertionResult<G>>;\n    bulkAssertSame: (\n      pairs: readonly IdentityPair<G>[],\n    ) => Promise<readonly IdentityAssertionResult<G>[]>;\n    bulkAssertDifferent: (\n      pairs: readonly IdentityPair<G>[],\n    ) => Promise<readonly IdentityAssertionResult<G>[]>;\n    retractAssertion: (\n      id: IdentityAssertionId,\n    ) => Promise<IdentityAssertion<G> | undefined>;\n    retractSameAssertion: (\n      a: IdentityNodeRefInput<G>,\n      b: IdentityNodeRefInput<G>,\n    ) => Promise<IdentityAssertion<G> | undefined>;\n    retractDifferentAssertion: (\n      a: IdentityNodeRefInput<G>,\n      b: IdentityNodeRefInput<G>,\n    ) => Promise<IdentityAssertion<G> | undefined>;\n    bulkRetractAssertions: (\n      ids: readonly IdentityAssertionId[],\n    ) => Promise<readonly IdentityAssertion<G>[]>;\n  }>;\n\n/**\n * The assertion-only write surface of the TypeGraph Identity Profile.\n *\n * This deliberately excludes reads and retractions so constrained staging\n * handles can accept incoming identity claims without exposing the broader\n * operational identity surface.\n */\nexport type IdentityAssertionWriteFacade<G extends GraphDef> = Pick<\n  IdentityFacade<G>,\n  \"assertSame\" | \"assertDifferent\" | \"bulkAssertSame\" | \"bulkAssertDifferent\"\n>;\n\n/**\n * Per-transaction identity write counts carried by a transaction receipt.\n * `total` is the sum of the three preceding counters.\n */\nexport type IdentityWriteSummary = Readonly<{\n  sameAssertions: number;\n  differentAssertions: number;\n  retractions: number;\n  total: number;\n}>;\n","/**\n * One-time offline migration: legacy shared embeddings table → per-field\n * strategy storage.\n *\n * ## What this is for\n *\n * The cross-backend vector cutover (#157) dropped the single shared\n * `typegraph_node_embeddings` table. Each\n * `VectorStrategy` now OWNS one typed, fixed-dimension structure per\n * `(nodeKind, fieldPath)` — the only layout that can be ANN-indexed on\n * libSQL and sqlite-vec, not just pgvector.\n *\n * Deployments that wrote embeddings under the old shared table before\n * upgrading still hold those rows. This utility drains them into the new\n * per-field storage **once**, during the upgrade, so existing embedded\n * data survives the cutover without re-embedding from source.\n *\n * It is an explicitly-run, offline step — not wired into `materializeIndexes`\n * or any boot path. Run it once after deploying the new version and before\n * (or alongside) the first `materializeIndexes()`, then drop the legacy\n * table at your leisure.\n *\n * ## How it works\n *\n * - Skips cleanly (returns a zero summary) when the legacy table is absent —\n *   a fresh install, or a re-run after the table was dropped.\n * - Reads rows in keyset-paginated batches (never the whole table at once),\n *   decoding the engine-native embedding column to a numeric array at the SQL\n *   level: sqlite-vec stored a `vec_f32` blob (`vec_to_json` decodes it);\n *   pgvector stored a native `vector` (`::text` yields a `[…]` literal).\n * - Re-inserts each row through `backend.upsertEmbedding`, which routes the\n *   active strategy's `buildUpsert` and idempotently provisions the per-field\n *   storage (the same `(nodeKind, fieldPath)` slot a fresh store write uses).\n *   The migration stays graph-agnostic and execution-correct on both the\n *   sync (better-sqlite3) and async (libsql / Postgres) backends — it does\n *   not re-implement the backend's statement-execution glue.\n *\n * Idempotent and resumable: a partial run followed by a full run converges\n * to the same per-field tables, because every write is an upsert keyed by\n * `(graph_id, node_id)` in the strategy's owned storage.\n */\nimport {\n  DEFAULT_EMBEDDING_INDEX_TYPE,\n  DEFAULT_EMBEDDING_METRIC,\n  type EmbeddingIndexType,\n  type EmbeddingMetric,\n} from \"../core/embedding\";\nimport { EmbeddingDimensionChangedError } from \"../errors\";\nimport { sql, type SqlFragment } from \"../query/sql-fragment\";\nimport { asCompiledRowsSql } from \"../query/sql-intent\";\nimport { createDataKeyedBag } from \"../utils/object\";\nimport { requireDefined } from \"../utils/presence\";\nimport { isMissingTableError } from \"../utils/sql-errors\";\nimport { LEGACY_EMBEDDINGS_TABLE_NAME } from \"./table-names\";\nimport { type GraphBackend } from \"./types\";\n\n/**\n * Default rows read per round-trip. Large enough to amortize round-trip\n * latency, small enough to bound peak memory regardless of table size.\n */\nconst DEFAULT_BATCH_SIZE = 500;\n\n/**\n * Metric / index type applied to a migrated field when no\n * {@link MigrateLegacyEmbeddingsOptions.resolveSlotConfig} is supplied (or\n * it returns `undefined`). Matches the defaults `embedding()` and\n * `resolveEmbeddingFields` apply, so a migrated field lands in storage shaped\n * identically to one written fresh through the store. Migrated data is only\n * ever brute-forceable until `materializeIndexes()` builds the ANN index, so\n * the index-type default governs only which ANN structure the per-field\n * storage DDL provisions, not the migration's correctness.\n */\nconst DEFAULT_SLOT_METRIC: EmbeddingMetric = DEFAULT_EMBEDDING_METRIC;\nconst DEFAULT_SLOT_INDEX_TYPE: EmbeddingIndexType =\n  DEFAULT_EMBEDDING_INDEX_TYPE;\n\n/**\n * The resolved storage config for one `(nodeKind, fieldPath)` slot that the\n * legacy table did not record. The legacy shared table stored only the\n * vector and its dimension count — not the metric or index type — so a\n * caller that wants migrated fields to match a specific graph's `embedding()`\n * declarations supplies this resolver.\n */\nexport type LegacyEmbeddingSlotConfig = Readonly<{\n  metric: EmbeddingMetric;\n  indexType: EmbeddingIndexType;\n}>;\n\n/**\n * Options for {@link migrateLegacyEmbeddings}.\n */\nexport type MigrateLegacyEmbeddingsOptions = Readonly<{\n  /**\n   * The backend to migrate, wired with the destination `VectorStrategy`\n   * (i.e. the post-cutover backend). Its `vectorStrategy` owns the per-field\n   * storage rows are written into and its `upsertEmbedding` performs the\n   * writes.\n   */\n  backend: GraphBackend;\n\n  /**\n   * Restrict the migration to a single graph. When omitted, every graph's\n   * rows in the legacy table are migrated (the table is graph-scoped by its\n   * `graph_id` column).\n   */\n  graphId?: string;\n\n  /**\n   * Resolves the metric / index type for a `(nodeKind, fieldPath)` slot the\n   * legacy table didn't persist. Return `undefined` to fall back to the\n   * cosine / hnsw defaults. Supply this (typically reading the graph's\n   * `embedding()` declarations) when migrated fields must match a specific\n   * metric — e.g. an `l2` field, which scores incorrectly under the cosine\n   * default.\n   */\n  resolveSlotConfig?: (\n    nodeKind: string,\n    fieldPath: string,\n  ) => LegacyEmbeddingSlotConfig | undefined;\n\n  /** Rows read per round-trip. Defaults to {@link DEFAULT_BATCH_SIZE}. */\n  batchSize?: number;\n\n  /**\n   * Physical name of the legacy table to drain. Defaults to\n   * {@link LEGACY_EMBEDDINGS_TABLE_NAME}. Override only for a non-default\n   * deployment that renamed the embeddings table.\n   */\n  legacyTableName?: string;\n}>;\n\n/**\n * Summary of a {@link migrateLegacyEmbeddings} run.\n */\nexport type MigrateLegacyEmbeddingsResult = Readonly<{\n  /** Total embedding rows re-inserted into per-field storage. */\n  migrated: number;\n  /**\n   * Per-`(nodeKind, fieldPath)` counts, keyed `\"<nodeKind>.<fieldPath>\"`\n   * (the strategy's logical-slot key form). Empty when the legacy table was\n   * absent or held no rows for the requested scope.\n   */\n  perField: Readonly<Record<string, number>>;\n  /**\n   * Rows skipped because their vector length didn't match the per-field\n   * table's fixed dimension. The legacy shared column allowed mixed\n   * dimensions for one `(nodeKind, fieldPath)` (e.g. an unmigrated model\n   * change); the first migrated row fixes the typed table's dimension and\n   * any differently-sized rows for that slot are skipped rather than aborting\n   * the whole migration. Keyed `\"<nodeKind>.<fieldPath>\"`. Non-empty here\n   * means those fields need a deliberate re-embed at a single dimension.\n   */\n  skippedDimensionMismatch: Readonly<Record<string, number>>;\n  /**\n   * Rows skipped because their legacy embedding could not be decoded into a\n   * finite-number array — a corrupt value (e.g. a pgvector `vector` column\n   * permits `NaN`/`Infinity`, which is not valid JSON). Keyed\n   * `\"<nodeKind>.<fieldPath>\"`. Skipping + reporting keeps one bad row from\n   * aborting the whole migration; non-empty here means those rows need manual\n   * repair.\n   */\n  skippedDecodeError: Readonly<Record<string, number>>;\n  /**\n   * Whether the legacy table existed. `false` means the run was a clean\n   * no-op (fresh install, or already-dropped table on a re-run).\n   */\n  legacyTablePresent: boolean;\n}>;\n\n/**\n * A single legacy embedding row, with the engine-native vector already\n * decoded to a JSON array string by the dialect-specific SELECT. The\n * legacy `dimensions` column is intentionally not read: the decoded array's\n * length is the authoritative fixed dimension for the destination slot, and\n * relying on it sidesteps the per-driver string/number coercion the legacy\n * integer column would otherwise need.\n */\ntype LegacyEmbeddingRow = Readonly<{\n  graph_id: string;\n  node_kind: string;\n  node_id: string;\n  field_path: string;\n  /** JSON array text, e.g. `\"[0.1,0.2,0.3]\"` — decoded per dialect. */\n  embedding_json: string;\n}>;\n\n/**\n * Re-inserts every embedding from the legacy shared\n * `typegraph_node_embeddings` table into the active\n * `backend.vectorStrategy`'s per-`(nodeKind, fieldPath)` storage.\n *\n * One-time, explicitly-run upgrade step for the shared-table → per-field\n * cutover (#157). Idempotent, batched, and a clean no-op when the legacy\n * table is absent. See the module doc comment for the full contract.\n *\n * @throws Error when the backend exposes no `vectorStrategy` /\n *         `upsertEmbedding` (nothing to migrate *into*) — a configuration\n *         error the caller must fix by wiring the destination strategy\n *         before migrating.\n */\nexport async function migrateLegacyEmbeddings(\n  options: MigrateLegacyEmbeddingsOptions,\n): Promise<MigrateLegacyEmbeddingsResult> {\n  const {\n    backend,\n    graphId,\n    resolveSlotConfig,\n    batchSize = DEFAULT_BATCH_SIZE,\n    legacyTableName = LEGACY_EMBEDDINGS_TABLE_NAME,\n  } = options;\n\n  if (\n    backend.vectorStrategy === undefined ||\n    backend.upsertEmbedding === undefined\n  ) {\n    throw new Error(\n      \"migrateLegacyEmbeddings requires a backend wired with a vectorStrategy \" +\n        \"(the destination per-field storage). Pass the post-cutover backend.\",\n    );\n  }\n  if (!Number.isInteger(batchSize) || batchSize <= 0) {\n    throw new RangeError(\n      `batchSize must be a positive integer, got: ${batchSize}`,\n    );\n  }\n  const upsertEmbedding = backend.upsertEmbedding;\n  const ensureVectorSlotContribution = backend.ensureVectorSlotContribution;\n\n  // Data-keyed: slot keys are built from a row's `node_kind` / `field_path`.\n  const perField = createDataKeyedBag<number>();\n  const skippedDimensionMismatch = createDataKeyedBag<number>();\n  const skippedDecodeError = createDataKeyedBag<number>();\n  // (kind, field) slots whose per-field table + durable marker this run has\n  // already provisioned, mapped to the dimension the table was fixed at.\n  // `upsertEmbedding` asserts the marker (#135) and no longer self-creates\n  // the table, so this offline migration (privileged) provisions each slot\n  // once — at the first row's dimension, which fixes the typed table just as\n  // the original lazy write did. Differently-sized later rows for the same\n  // slot are detected against this map and skipped BEFORE the upsert: the\n  // marker assert would otherwise read the differently-dimensioned slot as\n  // stale and abort the whole migration, and the ensure is deliberately not\n  // re-run per row (that would trip the marker drift-guard).\n  const ensuredSlots = new Map<string, number>();\n  let migrated = 0;\n\n  let cursor: LegacyRowCursor | undefined;\n  for (;;) {\n    let batch: readonly LegacyEmbeddingRow[];\n    try {\n      batch = await readLegacyBatch(backend, {\n        legacyTableName,\n        graphId,\n        batchSize,\n        after: cursor,\n      });\n    } catch (error) {\n      // A missing legacy table is the expected \"nothing to migrate\" path on\n      // the first read — surface it as a clean no-op, not a failure.\n      if (cursor === undefined && isMissingTableError(error)) {\n        return {\n          migrated: 0,\n          perField: {},\n          skippedDimensionMismatch: {},\n          skippedDecodeError: {},\n          legacyTablePresent: false,\n        };\n      }\n      throw error;\n    }\n\n    if (batch.length === 0) break;\n\n    for (const row of batch) {\n      const slotKey = `${row.node_kind}.${row.field_path}`;\n\n      let embedding: readonly number[];\n      try {\n        embedding = decodeEmbeddingJson(row);\n      } catch {\n        // A corrupt legacy value (e.g. a pgvector `vector` column permits\n        // NaN/Infinity, which is not valid JSON) is skipped + reported rather\n        // than aborting the whole migration on one bad row.\n        skippedDecodeError[slotKey] = (skippedDecodeError[slotKey] ?? 0) + 1;\n        continue;\n      }\n      const config = resolveSlotConfig?.(row.node_kind, row.field_path);\n      // The decoded length is the authoritative fixed dimension — the\n      // strategy provisions its column type (e.g. `F32_BLOB(N)`) from it.\n      const slot = {\n        graphId: row.graph_id,\n        nodeKind: row.node_kind,\n        fieldPath: row.field_path,\n        dimensions: embedding.length,\n        metric: config?.metric ?? DEFAULT_SLOT_METRIC,\n        indexType: config?.indexType ?? DEFAULT_SLOT_INDEX_TYPE,\n      };\n\n      // Provision the per-field table + durable marker before the first\n      // write into this slot. Once per (graph, kind, field): the first row's\n      // dimension fixes the table. Keyed by graph id too — the legacy table\n      // is graph-scoped, so the same `kind.field` can recur across graphs.\n      const ensuredKey = JSON.stringify([\n        row.graph_id,\n        row.node_kind,\n        row.field_path,\n      ]);\n      if (\n        ensureVectorSlotContribution !== undefined &&\n        !ensuredSlots.has(ensuredKey)\n      ) {\n        await ensureVectorSlotContribution(slot);\n        ensuredSlots.set(ensuredKey, slot.dimensions);\n      }\n\n      // The legacy shared column allowed mixed dimensions for one\n      // (nodeKind, fieldPath); the per-field table is fixed at the first\n      // migrated row's dimension. Skip a differently-sized row up front —\n      // its slot would fail the marker assert as stale (different\n      // signature), aborting the migration instead of skipping the row.\n      const provisionedDimensions = ensuredSlots.get(ensuredKey);\n      if (\n        provisionedDimensions !== undefined &&\n        provisionedDimensions !== embedding.length\n      ) {\n        skippedDimensionMismatch[slotKey] =\n          (skippedDimensionMismatch[slotKey] ?? 0) + 1;\n        continue;\n      }\n\n      try {\n        await upsertEmbedding({ ...slot, nodeId: row.node_id, embedding });\n      } catch (error) {\n        // The legacy shared column allowed mixed dimensions for one\n        // (nodeKind, fieldPath); the per-field table is fixed at the first\n        // migrated row's dimension, so a differently-sized row is skipped and\n        // reported rather than aborting the whole migration.\n        if (error instanceof EmbeddingDimensionChangedError) {\n          skippedDimensionMismatch[slotKey] =\n            (skippedDimensionMismatch[slotKey] ?? 0) + 1;\n          continue;\n        }\n        throw error;\n      }\n\n      migrated += 1;\n      perField[slotKey] = (perField[slotKey] ?? 0) + 1;\n    }\n\n    if (batch.length < batchSize) break;\n    const last = requireDefined(batch.at(-1));\n    cursor = {\n      graphId: last.graph_id,\n      nodeKind: last.node_kind,\n      nodeId: last.node_id,\n      fieldPath: last.field_path,\n    };\n  }\n\n  // SPREAD at the boundary. The three maps are null-prototype accumulators\n  // because their keys are slot names built from row data; that protection is\n  // internal, and returning the bags as-is handed a caller three maps with no\n  // `toString` and `instanceof Object === false` — while the early\n  // \"nothing to migrate\" return above hands back `{}` literals, so the same\n  // exported function answered with two different kinds of object depending on\n  // whether the legacy table existed. Spread copies own properties with\n  // CreateDataProperty, so a `__proto__` slot key survives as an own key.\n  return {\n    migrated,\n    perField: { ...perField },\n    skippedDimensionMismatch: { ...skippedDimensionMismatch },\n    skippedDecodeError: { ...skippedDecodeError },\n    legacyTablePresent: true,\n  };\n}\n\n/**\n * Composite keyset cursor over the legacy table's primary key\n * `(graph_id, node_kind, node_id, field_path)`. Stable under concurrent\n * writes and immune to OFFSET drift, so a batched walk reads every row once.\n */\ntype LegacyRowCursor = Readonly<{\n  graphId: string;\n  nodeKind: string;\n  nodeId: string;\n  fieldPath: string;\n}>;\n\ntype ReadLegacyBatchParams = Readonly<{\n  legacyTableName: string;\n  graphId: string | undefined;\n  batchSize: number;\n  after: LegacyRowCursor | undefined;\n}>;\n\n/**\n * Reads one keyset-paginated batch of legacy rows, ordered by the primary\n * key, with the engine-native embedding column decoded to a JSON array\n * string by the dialect.\n */\nasync function readLegacyBatch(\n  backend: GraphBackend,\n  params: ReadLegacyBatchParams,\n): Promise<readonly LegacyEmbeddingRow[]> {\n  const table = sql.identifier(params.legacyTableName);\n  const embeddingJson = legacyEmbeddingDecodeExpression(backend);\n\n  const conditions: SqlFragment[] = [];\n  if (params.graphId !== undefined) {\n    conditions.push(sql`\"graph_id\" = ${params.graphId}`);\n  }\n  if (params.after !== undefined) {\n    conditions.push(keysetAfterCondition(params.after));\n  }\n\n  const whereClause =\n    conditions.length === 0 ?\n      sql``\n    : sql` WHERE ${sql.join(conditions, sql` AND `)}`;\n\n  const query = sql`\n    SELECT\n      \"graph_id\" AS graph_id,\n      \"node_kind\" AS node_kind,\n      \"node_id\" AS node_id,\n      \"field_path\" AS field_path,\n      ${embeddingJson} AS embedding_json\n    FROM ${table}${whereClause}\n    ORDER BY \"graph_id\" ASC, \"node_kind\" ASC, \"node_id\" ASC, \"field_path\" ASC\n    LIMIT ${params.batchSize}\n  `;\n\n  return backend.execute<LegacyEmbeddingRow>(asCompiledRowsSql(query));\n}\n\n/**\n * The strict `>` comparison over the composite primary key, expressed as\n * the standard lexicographic OR-chain so it works on every dialect (SQLite\n * supports row-value comparison, but Postgres + SQLite both accept this\n * portable form).\n */\nfunction keysetAfterCondition(after: LegacyRowCursor): SqlFragment {\n  return sql`\n    (\n        \"graph_id\" > ${after.graphId}\n        OR (\"graph_id\" = ${after.graphId} AND \"node_kind\" > ${after.nodeKind})\n        OR (\"graph_id\" = ${after.graphId} AND \"node_kind\" = ${after.nodeKind} AND \"node_id\" > ${after.nodeId})\n        OR (\n          \"graph_id\" = ${after.graphId}\n          AND \"node_kind\" = ${after.nodeKind}\n          AND \"node_id\" = ${after.nodeId}\n          AND \"field_path\" > ${after.fieldPath}\n        )\n      )\n  `;\n}\n\n/**\n * The SQL expression that decodes the legacy engine-native embedding column\n * into a JSON array string `embedding_json`. The single dialect branch in\n * this whole module: the legacy storage formats are genuinely engine-\n * specific, and there is no longer a strategy abstraction over the *legacy*\n * table to delegate to.\n */\nfunction legacyEmbeddingDecodeExpression(backend: GraphBackend): SqlFragment {\n  switch (backend.dialect) {\n    case \"sqlite\": {\n      // Legacy SQLite embeddings were written via `vec_f32('[…]')`\n      // (sqlite-vec binary). `vec_to_json` is its inverse, yielding the\n      // JSON array text. Requires sqlite-vec on the connection — which the\n      // legacy write path also required, so any deployment with legacy rows\n      // has it loaded.\n      return sql`vec_to_json(\"embedding\")`;\n    }\n    case \"postgres\": {\n      // pgvector's native `vector` casts to its `[…]` text literal, which is\n      // valid JSON for `JSON.parse`.\n      return sql`\"embedding\"::text`;\n    }\n    default: {\n      const _exhaustive: never = backend.dialect;\n      throw new Error(\n        `migrateLegacyEmbeddings does not support dialect: ${String(_exhaustive)}`,\n      );\n    }\n  }\n}\n\n/**\n * Parses a decoded `embedding_json` array, asserting it is a non-empty array\n * of finite numbers. A bad decode (wrong type, NaN) names the offending row\n * loudly instead of writing a corrupt vector into per-field storage.\n */\nfunction decodeEmbeddingJson(row: LegacyEmbeddingRow): readonly number[] {\n  let parsed: unknown;\n  try {\n    parsed = JSON.parse(row.embedding_json) as unknown;\n  } catch (error) {\n    throw new Error(\n      `Failed to decode legacy embedding for ${row.node_kind}.${row.field_path} ` +\n        `(graph ${row.graph_id}, node ${row.node_id}): not valid JSON.`,\n      { cause: error },\n    );\n  }\n  if (!Array.isArray(parsed) || parsed.length === 0) {\n    throw new Error(\n      `Legacy embedding for ${row.node_kind}.${row.field_path} ` +\n        `(graph ${row.graph_id}, node ${row.node_id}) decoded to a non-array ` +\n        `or empty value.`,\n    );\n  }\n  for (const [index, value] of parsed.entries()) {\n    if (typeof value !== \"number\" || !Number.isFinite(value)) {\n      throw new TypeError(\n        `Legacy embedding for ${row.node_kind}.${row.field_path} ` +\n          `(graph ${row.graph_id}, node ${row.node_id}) has a non-finite ` +\n          `value at index ${index}: ${String(value)}.`,\n      );\n    }\n  }\n  return parsed as readonly number[];\n}\n"]}