{"version":3,"sources":["../../protocol/src/helpers.ts","../src/import.ts"],"names":["MAX_TRANSACTION_OPS"],"mappings":"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","file":"import.cjs","sourcesContent":["import { PROTOCOL_VERSION, type EntityId } from \"./ids\";\nimport type { ClientMsg, ServerMsg } from \"./messages\";\n\n/**\n * Generate a UUIDv7 (time-ordered). Uses the Web Crypto API available in both\n * the browser, Node >=20, and the Workers runtime.\n */\nexport function uuidv7(): EntityId {\n  const bytes = new Uint8Array(16);\n  crypto.getRandomValues(bytes);\n\n  const ts = BigInt(Date.now());\n  bytes[0] = Number((ts >> 40n) & 0xffn);\n  bytes[1] = Number((ts >> 32n) & 0xffn);\n  bytes[2] = Number((ts >> 24n) & 0xffn);\n  bytes[3] = Number((ts >> 16n) & 0xffn);\n  bytes[4] = Number((ts >> 8n) & 0xffn);\n  bytes[5] = Number(ts & 0xffn);\n\n  // version 7\n  bytes[6] = (bytes[6]! & 0x0f) | 0x70;\n  // variant (RFC 4122)\n  bytes[8] = (bytes[8]! & 0x3f) | 0x80;\n\n  const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n}\n\nconst CLIENT_OPS = new Set([\n  \"init\", \"subscribe\", \"subscribe-batch\", \"unsubscribe\", \"transact\",\n  \"presence-set\", \"presence-clear\", \"room-publish\", \"ping\",\n]);\nconst SERVER_OPS = new Set([\n  \"init-ok\", \"query-result\", \"query-result-batch\", \"query-diff\", \"tx-ack\",\n  \"mutation-reject\", \"tx\", \"presence\", \"room-event\", \"error\", \"pong\", \"echo\",\n]);\nexport const MAX_WS_MESSAGE_BYTES = 1_100_000;\nexport const MAX_TRANSACTION_OPS = 1_000;\n/** Durable Object sockets retain at most this many live query registrations. */\nexport const MAX_ACTIVE_SUBSCRIPTIONS = 32;\n\n/** Cheap structural guard for an inbound client message. */\nexport function isClientMsg(x: unknown): x is ClientMsg {\n  if (!isEnvelope(x, CLIENT_OPS)) return false;\n  const m = x as Record<string, unknown>;\n  switch (m.op) {\n    case \"init\":\n      return isShortString(m.clientId) && optionalString(m.auth) && optionalNonNegativeNumber(m.lastSeenTx) &&\n        (m.caps === undefined || (Array.isArray(m.caps) && m.caps.length <= 32 && m.caps.every(isShortString))) &&\n        (m.resume === undefined || (Array.isArray(m.resume) && m.resume.length <= MAX_ACTIVE_SUBSCRIPTIONS && m.resume.every(isResume)));\n    case \"subscribe\": return isShortString(m.subId) && isRecord(m.query);\n    case \"subscribe-batch\":\n      return Array.isArray(m.subs) && m.subs.length >= 1 && m.subs.length <= MAX_ACTIVE_SUBSCRIPTIONS && m.subs.every(isResume);\n    case \"unsubscribe\": return isShortString(m.subId);\n    case \"transact\": return isShortString(m.mutationId) && Array.isArray(m.ops) && m.ops.length <= MAX_TRANSACTION_OPS && m.ops.every(isOp);\n    case \"presence-set\": return isRoom(m.room) && isJson(m.presence);\n    case \"presence-clear\": return isRoom(m.room);\n    case \"room-publish\": return isRoom(m.room) && isJson(m.event);\n    case \"ping\": return true;\n    default: return false;\n  }\n}\n\n/** Cheap structural guard for an inbound server message. */\nexport function isServerMsg(x: unknown): x is ServerMsg {\n  if (!isEnvelope(x, SERVER_OPS)) return false;\n  const m = x as Record<string, unknown>;\n  switch (m.op) {\n    case \"init-ok\":\n      return isFiniteNumber(m.serverTime) && isFiniteNumber(m.currentTx) && optionalNonNegativeNumber(m.epoch) &&\n        (m.caps === undefined || (Array.isArray(m.caps) && m.caps.length <= 32 && m.caps.every(isShortString)));\n    case \"query-result\": return isShortString(m.subId) && isShortString(m.queryHash) && isFiniteNumber(m.txId) && isRecord(m.result);\n    case \"query-result-batch\":\n      return isFiniteNumber(m.txId) && Array.isArray(m.results) && m.results.length >= 1 &&\n        m.results.length <= MAX_ACTIVE_SUBSCRIPTIONS && m.results.every(isBatchResult);\n    case \"query-diff\": return isShortString(m.subId) && isShortString(m.queryHash) && isFiniteNumber(m.txId) && isRecord(m.diff);\n    case \"tx-ack\": return isShortString(m.mutationId) && isFiniteNumber(m.txId);\n    case \"mutation-reject\": return isShortString(m.mutationId) && isErr(m.error);\n    case \"tx\": return isFiniteNumber(m.txId);\n    case \"presence\": return isRoom(m.room) && Array.isArray(m.peers) && m.peers.every(isPeer);\n    case \"room-event\": return isRoom(m.room) && isShortString(m.from) && isJson(m.event);\n    case \"error\": return isErr(m.error);\n    case \"pong\": return true;\n    case \"echo\": return typeof m.data === \"string\";\n    default: return false;\n  }\n}\n\nexport type WireParse<T> =\n  | { ok: true; value: T }\n  | { ok: false; code: \"invalid_json\" | \"protocol_version\" | \"bad_message\" | \"message_too_large\"; receivedVersion?: unknown };\n\n/** Parse with enough detail for a peer to report an actionable version error. */\nexport function parseClientMsgDetailed(data: string): WireParse<ClientMsg> {\n  if (new TextEncoder().encode(data).byteLength > MAX_WS_MESSAGE_BYTES) return { ok: false, code: \"message_too_large\" };\n  let parsed: unknown;\n  try {\n    parsed = JSON.parse(data);\n  } catch {\n    return { ok: false, code: \"invalid_json\" };\n  }\n  if (typeof parsed === \"object\" && parsed !== null && (parsed as { v?: unknown }).v !== PROTOCOL_VERSION) {\n    return { ok: false, code: \"protocol_version\", receivedVersion: (parsed as { v?: unknown }).v };\n  }\n  return isClientMsg(parsed) ? { ok: true, value: parsed } : { ok: false, code: \"bad_message\" };\n}\n\n/** Parse a raw WebSocket text frame into a typed client message, or null. */\nexport function parseClientMsg(data: string): ClientMsg | null {\n  const result = parseClientMsgDetailed(data);\n  return result.ok ? result.value : null;\n}\n\n/** Parse a server WebSocket frame, rejecting unknown ops and versions. */\nexport function parseServerMsg(data: string): WireParse<ServerMsg> {\n  if (new TextEncoder().encode(data).byteLength > MAX_WS_MESSAGE_BYTES) return { ok: false, code: \"message_too_large\" };\n  let parsed: unknown;\n  try {\n    parsed = JSON.parse(data);\n  } catch {\n    return { ok: false, code: \"invalid_json\" };\n  }\n  if (typeof parsed === \"object\" && parsed !== null && (parsed as { v?: unknown }).v !== PROTOCOL_VERSION) {\n    return { ok: false, code: \"protocol_version\", receivedVersion: (parsed as { v?: unknown }).v };\n  }\n  return isServerMsg(parsed) ? { ok: true, value: parsed } : { ok: false, code: \"bad_message\" };\n}\n\n/** Serialize a server message for the wire. */\nexport function encodeServerMsg(msg: ServerMsg): string {\n  return JSON.stringify(msg);\n}\n\nfunction isEnvelope(value: unknown, ops: ReadonlySet<string>): value is Record<string, unknown> {\n  return isRecord(value) && value.v === PROTOCOL_VERSION && typeof value.op === \"string\" && ops.has(value.op);\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n  return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction isShortString(value: unknown): value is string {\n  return typeof value === \"string\" && value.length > 0 && value.length <= 512;\n}\n\nfunction optionalString(value: unknown): boolean {\n  return value === undefined || typeof value === \"string\";\n}\n\nfunction isFiniteNumber(value: unknown): value is number {\n  return typeof value === \"number\" && Number.isFinite(value);\n}\n\nfunction optionalNonNegativeNumber(value: unknown): boolean {\n  return value === undefined || (isFiniteNumber(value) && value >= 0);\n}\n\nfunction isRoom(value: unknown): value is string {\n  return typeof value === \"string\" && value.length > 0 && value.length <= 256;\n}\n\nfunction isResume(value: unknown): boolean {\n  return isRecord(value) && isShortString(value.subId) && isRecord(value.query);\n}\n\nfunction isBatchResult(value: unknown): boolean {\n  return isRecord(value) && isShortString(value.subId) && isShortString(value.queryHash) && isRecord(value.result);\n}\n\nfunction isEntityRef(value: unknown): boolean {\n  if (isShortString(value)) return true;\n  if (!isRecord(value)) return false;\n  if (isShortString(value.id)) return value.graph === undefined || isShortString(value.graph);\n  return isShortString(value.ns) && isShortString(value.attr) && isScalar(value.value) &&\n    (value.graph === undefined || isShortString(value.graph));\n}\n\nfunction isOp(value: unknown): boolean {\n  if (!isRecord(value) || !isShortString(value.ns) || !isEntityRef(value.id)) return false;\n  switch (value.t) {\n    case \"update\": case \"merge\": return isRecord(value.attrs);\n    case \"increment\": return isShortString(value.attr) && isFiniteNumber(value.by);\n    case \"delete\": return true;\n    case \"retract\": return Array.isArray(value.attrs) && value.attrs.every(isShortString);\n    case \"link\": case \"unlink\": return isShortString(value.label) && isEntityRef(value.target);\n    default: return false;\n  }\n}\n\nfunction isErr(value: unknown): boolean {\n  return isRecord(value) && isShortString(value.code) && typeof value.message === \"string\" &&\n    (value.detail === undefined || isJson(value.detail));\n}\n\nfunction isPeer(value: unknown): boolean {\n  return isRecord(value) && isShortString(value.peerId) && isJson(value.presence);\n}\n\nfunction isScalar(value: unknown): boolean {\n  return value === null || typeof value === \"string\" || typeof value === \"boolean\" || isFiniteNumber(value);\n}\n\nfunction isJson(value: unknown, depth = 0): boolean {\n  if (isScalar(value)) return true;\n  if (depth >= 32) return false;\n  if (Array.isArray(value)) return value.every((item) => isJson(item, depth + 1));\n  return isRecord(value) && Object.values(value).every((item) => isJson(item, depth + 1));\n}\n","/**\n * `@odla-ai/db/import` — plan a bulk upsert from a file or a paste.\n *\n * Turning \"here are 50,000 rows\" into transactions the server will accept is\n * fiddly in ways that are easy to get subtly wrong: a transaction caps at\n * {@link MAX_TRANSACTION_OPS} ops AND at a message-size limit, an oversized\n * single row can never be sent at all, and retrying a partially-applied import\n * must skip what already committed rather than double-apply it. This module is\n * the whole of that logic, pure and deterministic, so it can be unit-tested and\n * shared by every surface that imports data (Studio's panel, `odla-ai app\n * import`, and your own tooling).\n *\n *   import { parseImport, buildImportOps, planImportChunks, importMutationId } from \"@odla-ai/db/import\";\n *\n *   const { sources } = parseImport(text, \"todos\");\n *   const { ops, rejected } = buildImportOps(sources, { idMode: \"field\" });\n *   const runId = crypto.randomUUID();          // once per import, reused on retry\n *   for (const chunk of planImportChunks(ops)) {\n *     await db.transact(chunk.ops, { mutationId: importMutationId(runId, chunk.index) });\n *   }\n *\n * Every op produced is an `update`, which is odla-db's upsert: named attributes\n * are set and unnamed ones are left alone, so re-importing a row edits it in\n * place instead of replacing it.\n */\nimport { MAX_TRANSACTION_OPS as PROTOCOL_MAX_TRANSACTION_OPS, type EntityRef, type Op, type Value } from \"@odla/protocol\";\n\n/**\n * The most operations a single transaction may carry.\n *\n * {@link planImportChunks} packs to this bound so an import never presents the\n * server with a transaction it is required to reject. Re-exported here because\n * an importer needs it to reason about chunk counts without depending on the\n * protocol package directly.\n */\nexport const MAX_TRANSACTION_OPS = PROTOCOL_MAX_TRANSACTION_OPS;\n\n/**\n * Per-transaction byte budget for {@link planImportChunks}.\n *\n * Deliberately below the protocol's 1,100,000-byte message cap: that cap covers\n * the entire request body, while chunk accounting measures only the ops. The\n * ~9% headroom absorbs the surrounding envelope, separators, and any UTF-8\n * expansion — without it a chunk that fits by our arithmetic is still rejected\n * on the wire.\n */\nexport const IMPORT_CHUNK_MAX_BYTES = 1_000_000;\n\n/** An import only ever upserts, so its ops are narrower than the full `Op` union. */\nexport type UpdateOp = Extract<Op, { t: \"update\" }>;\n\n/**\n * How an input row is matched to an entity.\n *\n * - `field` — take the entity id from a field on the row (`id` by default).\n * - `key` — match an existing row by a **unique** attribute, via a lookup ref.\n * - `generate` — mint a fresh id per row. **Not idempotent**: re-running an\n *   import in this mode creates duplicates rather than updating in place.\n */\nexport type ImportIdMode = \"field\" | \"key\" | \"generate\";\n\n/** Input shapes {@link parseImport} recognises. */\nexport type ImportFormat = \"array\" | \"jsonl\" | \"namespace-map\" | \"object\";\n\n/** One input row, the namespace it lands in, and its 1-based position in the input. */\nexport interface ImportSource {\n  ns: string;\n  row: Record<string, unknown>;\n  index: number;\n}\n\n/** A row that cannot be imported, identified by its position in the input. */\nexport interface ImportRejection {\n  index: number;\n  reason: string;\n}\n\n/** Detected format plus every row flattened out of it. */\nexport interface ImportParseResult {\n  format: ImportFormat;\n  sources: ImportSource[];\n}\n\n/** Ops to send, rows that can't be sent, and fields that were dropped. */\nexport interface ImportBuildResult {\n  ops: UpdateOp[];\n  rejected: ImportRejection[];\n  /** Server-maintained `$` fields stripped from the input, sorted and unique. */\n  ignored: string[];\n}\n\n/** One transaction's worth of ops. */\nexport interface ImportChunk {\n  index: number;\n  ops: UpdateOp[];\n  bytes: number;\n}\n\n/** Options for {@link buildImportOps}. */\nexport interface ImportOptions {\n  idMode: ImportIdMode;\n  /**\n   * Which field carries the entity id in `field` mode. Defaults to `id`.\n   * Only a literal `id` is stripped from the written attributes (it is already\n   * the entity id); a custom field is kept so it stays queryable.\n   */\n  idField?: string;\n  /**\n   * The attribute to match on in `key` mode. Must be declared **unique** in the\n   * schema — the server rejects a lookup on a non-unique attribute with\n   * `lookup_not_unique`.\n   */\n  keyAttr?: string;\n  /**\n   * Whether a namespace declares its own `id` attribute (the \"mirror the entity\n   * id so `where: { id }` resolves\" pattern). When it does, `generate` mode\n   * fills that attribute from the newly minted id.\n   */\n  declaresIdAttr?: (ns: string) => boolean;\n  /** Id factory for `generate` mode. Defaults to `crypto.randomUUID`. */\n  newId?: () => string;\n  /** Overrides {@link IMPORT_CHUNK_MAX_BYTES} when sizing individual rows. */\n  maxBytes?: number;\n}\n\n/** Thrown for input that cannot be parsed or planned at all. */\nexport class ImportError extends Error {\n  constructor(message: string) {\n    super(message);\n    this.name = \"ImportError\";\n  }\n}\n\nconst encoder = new TextEncoder();\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n  return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\n/**\n * Detect the input format and flatten it to rows.\n *\n * Accepts a JSON array of objects, one JSON object per line (JSONL), a single\n * JSON object, or a `{ namespace: rows[] }` map — the last being the shape a\n * query result already has, so query output can be pasted straight back in.\n *\n * The whole text is parsed as JSON **first**, so JSONL whose opening line is\n * itself a complete JSON object resolves deterministically as JSONL rather than\n * depending on which check happens to run first.\n *\n * @param ns Target namespace for row-shaped input. A `{ namespace: rows }` map\n *   carries its own namespaces and ignores this.\n * @throws {ImportError} for a non-object document or a malformed JSONL line\n *   (the message names the line number).\n */\nexport function parseImport(text: string, ns?: string): ImportParseResult {\n  const trimmed = text.trim();\n  if (!trimmed) return { format: \"array\", sources: [] };\n  let whole: unknown;\n  try {\n    whole = JSON.parse(trimmed);\n  } catch {\n    return { format: \"jsonl\", sources: parseLines(trimmed, ns) };\n  }\n  if (Array.isArray(whole)) return { format: \"array\", sources: rowsOf(whole, ns) };\n  if (!isRecord(whole)) throw new ImportError(\"input must be an object, an array of objects, or one object per line\");\n  const values = Object.values(whole);\n  if (values.length > 0 && values.every(Array.isArray)) {\n    const sources: ImportSource[] = [];\n    let index = 1;\n    for (const [key, rows] of Object.entries(whole)) {\n      for (const row of rows as unknown[]) sources.push({ ns: key, row: row as Record<string, unknown>, index: index++ });\n    }\n    return { format: \"namespace-map\", sources };\n  }\n  return { format: \"object\", sources: rowsOf([whole], ns) };\n}\n\nfunction parseLines(text: string, ns?: string): ImportSource[] {\n  const sources: ImportSource[] = [];\n  const lines = text.split(\"\\n\");\n  for (let i = 0; i < lines.length; i++) {\n    const line = lines[i]!.trim();\n    if (!line) continue;\n    try {\n      sources.push({ ns: ns ?? \"\", row: JSON.parse(line) as Record<string, unknown>, index: i + 1 });\n    } catch {\n      throw new ImportError(`line ${i + 1} is not valid JSON — the input parsed as neither a JSON document nor JSONL`);\n    }\n  }\n  return sources;\n}\n\nfunction rowsOf(rows: unknown[], ns?: string): ImportSource[] {\n  return rows.map((row, i) => ({ ns: ns ?? \"\", row: row as Record<string, unknown>, index: i + 1 }));\n}\n\ntype IdResult = { ok: true; id: EntityRef } | { ok: false; reason: string };\n\n/**\n * Build one upsert op per row.\n *\n * Problems are reported per row rather than failing the whole input, so a caller\n * can drop the bad rows and import the rest. Rows are rejected when they carry\n * no usable id, are not objects, target a platform-owned (`$`-prefixed)\n * namespace, would write nothing, or are individually too large for any single\n * transaction.\n *\n * Server-maintained `$` fields (`$createdAt`, `$updatedBy`, …) are stripped\n * rather than rejected — query output carries them, and re-importing it is the\n * common case — and reported in {@link ImportBuildResult.ignored}.\n */\nexport function buildImportOps(sources: ImportSource[], opts: ImportOptions): ImportBuildResult {\n  const maxBytes = opts.maxBytes ?? IMPORT_CHUNK_MAX_BYTES;\n  const newId = opts.newId ?? (() => crypto.randomUUID());\n  const ops: UpdateOp[] = [];\n  const rejected: ImportRejection[] = [];\n  const ignored = new Set<string>();\n\n  for (const source of sources) {\n    const { ns, row, index } = source;\n    if (!ns) {\n      rejected.push({ index, reason: \"no namespace — name one, or use the {namespace: rows} form\" });\n      continue;\n    }\n    if (ns.startsWith(\"$\")) {\n      // The engine would ACCEPT this: `$` namespaces are writable, so an import\n      // could mint identity rows with no authenticated user behind them.\n      rejected.push({ index, reason: `${ns} is platform-owned — odla-db maintains it, imports must not` });\n      continue;\n    }\n    if (!isRecord(row)) {\n      rejected.push({ index, reason: \"not an object\" });\n      continue;\n    }\n\n    const attrs: Record<string, Value> = {};\n    let attrCount = 0;\n    for (const [key, value] of Object.entries(row)) {\n      if (key.startsWith(\"$\")) {\n        ignored.add(key);\n        continue;\n      }\n      if (key === \"id\" && opts.idMode !== \"generate\" && !opts.declaresIdAttr?.(ns)) continue;\n      attrs[key] = value as Value;\n      attrCount++;\n    }\n\n    const resolved = resolveId(source, opts, newId);\n    if (!resolved.ok) {\n      rejected.push({ index, reason: resolved.reason });\n      continue;\n    }\n    if (attrCount === 0) {\n      rejected.push({ index, reason: \"no writable fields — only server-maintained `$` values\" });\n      continue;\n    }\n    if (opts.idMode === \"generate\" && opts.declaresIdAttr?.(ns)) attrs.id = resolved.id as string;\n\n    const op: UpdateOp = { t: \"update\", ns, id: resolved.id, attrs };\n    const bytes = sizeOf(op);\n    if (bytes > maxBytes) {\n      rejected.push({ index, reason: `${bytes} bytes on its own, larger than one transaction can carry — store large values as files` });\n      continue;\n    }\n    ops.push(op);\n  }\n  return { ops, rejected, ignored: [...ignored].sort() };\n}\n\n/** Discriminated rather than sentinel-encoded: an entity id is an arbitrary\n * string, so no in-band error marker would be safe. */\nfunction resolveId(source: ImportSource, opts: ImportOptions, newId: () => string): IdResult {\n  const { ns, row } = source;\n  if (opts.idMode === \"generate\") return { ok: true, id: newId() };\n  if (opts.idMode === \"key\") {\n    const attr = opts.keyAttr;\n    if (!attr) return { ok: false, reason: \"no key attribute chosen\" };\n    const value = row[attr];\n    if (value === undefined || value === null || typeof value === \"object\") {\n      return { ok: false, reason: `missing or non-scalar \\`${attr}\\`` };\n    }\n    return { ok: true, id: { ns, attr, value: value as string | number | boolean } };\n  }\n  const idField = opts.idField ?? \"id\";\n  const id = row[idField];\n  if (typeof id !== \"string\" || !id) return { ok: false, reason: `missing or non-string \\`${idField}\\`` };\n  return { ok: true, id };\n}\n\nfunction sizeOf(op: UpdateOp): number {\n  return encoder.encode(JSON.stringify(op)).length + 1; // +1 for the separating comma\n}\n\n/**\n * Pack ops into transaction-sized chunks, respecting both the op-count cap and\n * the byte budget. Each op is measured once — re-costing a candidate chunk on\n * every addition would be quadratic on a large import.\n *\n * @throws {ImportError} if a single op exceeds the byte budget and so could\n *   never be sent. {@link buildImportOps} already rejects such rows, so this\n *   guards direct callers, where emitting an empty chunk would loop forever.\n */\nexport function planImportChunks(ops: UpdateOp[], limits: { maxOps?: number; maxBytes?: number } = {}): ImportChunk[] {\n  const maxOps = limits.maxOps ?? MAX_TRANSACTION_OPS;\n  const maxBytes = limits.maxBytes ?? IMPORT_CHUNK_MAX_BYTES;\n  const chunks: ImportChunk[] = [];\n  let current: UpdateOp[] = [];\n  let bytes = 0;\n  for (const op of ops) {\n    const size = sizeOf(op);\n    if (size > maxBytes) throw new ImportError(`a single op is ${size} bytes, larger than one transaction can carry`);\n    if (current.length > 0 && (current.length >= maxOps || bytes + size > maxBytes)) {\n      chunks.push({ index: chunks.length, ops: current, bytes });\n      current = [];\n      bytes = 0;\n    }\n    current.push(op);\n    bytes += size;\n  }\n  if (current.length > 0) chunks.push({ index: chunks.length, ops: current, bytes });\n  return chunks;\n}\n\n/**\n * The mutation id for one chunk of an import run.\n *\n * Mutation ids are unique per writer, so a bare `import:0` would collide with —\n * and be silently swallowed as a duplicate of — the *previous* import's first\n * chunk, writing nothing. Scoping to a run id fixes that, and makes retries\n * safe: re-sending a run's chunks skips whatever already committed.\n *\n * Mint `runId` once when the import starts and reuse it across retries. If the\n * input changes, mint a **new** one: chunk boundaries move, so the same id would\n * name different ops than the chunk already committed under it.\n */\nexport function importMutationId(runId: string, chunkIndex: number): string {\n  return `import:${runId}:${chunkIndex}`;\n}\n"]}