@ai-matrx/alchemy 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/core.cjs +7 -2
  3. package/dist/core.cjs.map +1 -1
  4. package/dist/core.d.cts +43 -2
  5. package/dist/core.d.ts +43 -2
  6. package/dist/core.js +8 -3
  7. package/dist/core.js.map +1 -1
  8. package/dist/index.cjs +7 -2
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +43 -2
  11. package/dist/index.d.ts +43 -2
  12. package/dist/index.js +8 -3
  13. package/dist/index.js.map +1 -1
  14. package/dist/operate-capture.cjs +2 -0
  15. package/dist/operate-capture.cjs.map +1 -0
  16. package/dist/operate-capture.d.cts +76 -0
  17. package/dist/operate-capture.d.ts +76 -0
  18. package/dist/operate-capture.js +2 -0
  19. package/dist/operate-capture.js.map +1 -0
  20. package/dist/operate-document.cjs +1 -1
  21. package/dist/operate-document.cjs.map +1 -1
  22. package/dist/operate-document.js +1 -1
  23. package/dist/operate-document.js.map +1 -1
  24. package/dist/operate-read.cjs +6 -0
  25. package/dist/operate-read.cjs.map +1 -0
  26. package/dist/operate-read.d.cts +174 -0
  27. package/dist/operate-read.d.ts +174 -0
  28. package/dist/operate-read.js +6 -0
  29. package/dist/operate-read.js.map +1 -0
  30. package/dist/operate-rich-html.cjs +1 -1
  31. package/dist/operate-rich-html.cjs.map +1 -1
  32. package/dist/operate-rich-html.js +1 -1
  33. package/dist/operate-rich-html.js.map +1 -1
  34. package/dist/operate-server.cjs +1 -1
  35. package/dist/operate-server.cjs.map +1 -1
  36. package/dist/operate-server.js +1 -1
  37. package/dist/operate-server.js.map +1 -1
  38. package/dist/operate-xlsx.cjs +3 -2
  39. package/dist/operate-xlsx.cjs.map +1 -1
  40. package/dist/operate-xlsx.d.cts +1227 -6
  41. package/dist/operate-xlsx.d.ts +1227 -6
  42. package/dist/operate-xlsx.js +3 -2
  43. package/dist/operate-xlsx.js.map +1 -1
  44. package/dist/operate.cjs +7 -2
  45. package/dist/operate.cjs.map +1 -1
  46. package/dist/operate.d.cts +43 -2
  47. package/dist/operate.d.ts +43 -2
  48. package/dist/operate.js +8 -3
  49. package/dist/operate.js.map +1 -1
  50. package/dist/react-workspace.cjs +2 -2
  51. package/dist/react-workspace.cjs.map +1 -1
  52. package/dist/react-workspace.d.cts +30 -1
  53. package/dist/react-workspace.d.ts +30 -1
  54. package/dist/react-workspace.js +1 -1
  55. package/dist/react-workspace.js.map +1 -1
  56. package/package.json +27 -3
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/operate/core.ts","../src/operate/fenced-code.ts","../src/operate/formats/rich-html.ts","../src/operate/formats/document.ts"],"sourcesContent":["import { stringifyJson } from \"@ai-matrx/kit/json-format\";\nimport { escapeHtml } from \"@ai-matrx/kit/html-escape\";\nimport {\n ContentTransferError,\n cloneTransferJson,\n normalizeTransferJson,\n normalizeTransferJsonAtPath,\n} from \"@ai-matrx/kit/transfer-json\";\nexport {\n ContentTransferError,\n normalizeTransferJson,\n type NormalizeTransferJsonOptions,\n} from \"@ai-matrx/kit/transfer-json\";\nimport type {\n Artifact,\n CapturedAllMatching,\n CapturedSource,\n Column,\n Draft,\n ExportProjection,\n Json,\n Omission,\n Payload,\n RawSnapshot,\n Snapshot,\n Source,\n Transport,\n TransferScope,\n TransferLimits,\n TransferPreferences,\n SurfaceHandle,\n} from \"./types\";\nexport * from \"./types\";\nexport * from \"./agent-payload\";\n\n/** A realm-wide brand (Symbol.for), so a binding made by one loaded copy is recognised by another. */\nconst bound: unique symbol = Symbol.for(\"ai-matrx.alchemy.operate.bound-all-matching\");\nexport type BoundAllMatching = {\n readonly [bound]: true;\n readonly source: CapturedAllMatching;\n readonly projection: ExportProjection;\n readonly columns: readonly Column[];\n readonly limits?: TransferLimits;\n};\nconst stop = (s: AbortSignal) => {\n if (s.aborted) throw new DOMException(\"Cancelled\", \"AbortError\");\n};\nconst enc = (x: string) => x.replace(/~/g, \"~0\").replace(/\\//g, \"~1\");\nconst dec = (x: string) => x.replace(/~1/g, \"/\").replace(/~0/g, \"~\");\nconst maxSurfacePointerSegmentLength = 64;\nconst maxSurfacePointerLength = 192;\nconst truncateSurfacePointer = (value: string, maxLength: number) => {\n const characters = Array.from(value);\n return characters.length > maxLength\n ? `${characters.slice(0, maxLength - 1).join(\"\")}…`\n : value;\n};\nfunction displaySurfacePointer(path: string | undefined): string {\n if (!path) return \"/\";\n const safe = path\n .split(\"/\")\n .slice(1)\n .map((segment) => {\n const controlSafe = segment.replace(/[\\u0000-\\u001F\\u007F-\\u009F]/g, (character) =>\n `\\\\u${character.charCodeAt(0).toString(16).padStart(4, \"0\")}`,\n );\n return truncateSurfacePointer(controlSafe, maxSurfacePointerSegmentLength);\n })\n .join(\"/\");\n const pointer = `/${safe}`;\n return truncateSurfacePointer(pointer, maxSurfacePointerLength);\n}\nfunction cloneSurfaceValue(value: unknown, path: string): Json {\n try {\n return normalizeTransferJsonAtPath(value, path, {\n omitUndefinedObjectProperties: true,\n });\n } catch (error) {\n if (error instanceof ContentTransferError)\n throw new ContentTransferError(\n error.code,\n `Surface capture rejected value at ${displaySurfacePointer(error.path)}: ${error.message}`,\n error.path,\n );\n throw error;\n }\n}\nfunction deepFreeze<T>(v: T): T {\n if (ArrayBuffer.isView(v)) return v;\n if (v && typeof v === \"object\" && !Object.isFrozen(v)) {\n Object.freeze(v);\n for (const x of Object.values(v as object)) deepFreeze(x);\n }\n return v;\n}\nfunction at(v: Json, path: string): Json | undefined {\n validatePointer(path);\n if (path === \"\") return v;\n if (!path.startsWith(\"/\"))\n throw new ContentTransferError(\n \"invalid-path\",\n \"Expected RFC 6901 pointer\",\n path,\n );\n let cur: Json | undefined = v;\n for (const part of path.slice(1).split(\"/\").map(dec)) {\n if (Array.isArray(cur)) cur = cur[Number(part)];\n else if (cur && typeof cur === \"object\")\n cur = Object.hasOwn(cur, part) ? cur[part] : undefined;\n else return undefined;\n }\n return cur;\n}\nfunction validatePointer(path: string): void {\n if ((path !== \"\" && !path.startsWith(\"/\")) || /~(?![01])/u.test(path))\n throw new ContentTransferError(\n \"invalid-path\",\n \"Expected an RFC 6901 JSON pointer\",\n path,\n );\n}\n/** Column ids are identities, never an alternative data address. */\nexport function readTransferCell(\n row: Record<string, Json>,\n column: Column,\n): Json | undefined {\n return at(row, column.path);\n}\nfunction without(\n v: Json,\n paths: readonly string[],\n omit: Omission[],\n path = \"\",\n): Json | undefined {\n if (paths.includes(path)) {\n omit.push({ path, reason: \"excluded\" });\n return undefined;\n }\n if (Array.isArray(v))\n return v\n .map((x, i) => without(x, paths, omit, `${path}/${i}`))\n .filter((x): x is Json => x !== undefined);\n if (v && typeof v === \"object\") {\n const out: Record<string, Json> = Object.create(null);\n for (const [k, x] of Object.entries(v)) {\n const y = without(x, paths, omit, `${path}/${enc(k)}`);\n if (y !== undefined) out[k] = y;\n }\n return out;\n }\n return v;\n}\nfunction projectValue(\n v: Json,\n rules: ExportProjection[\"rules\"],\n target: \"payload\" | \"row\",\n omit: Omission[],\n): Json {\n rules.forEach((rule) => validatePointer(rule.path));\n return (\n without(\n cloneTransferJson(v),\n rules\n .filter(\n (r) =>\n r.target === target &&\n (!r.exportable || r.classification !== \"ordinary\"),\n )\n .map((r) => r.path),\n omit,\n ) ?? null\n );\n}\nfunction excludedColumn(column: Column): boolean {\n return (\n column.exportable === false ||\n column.classification === \"secret\" ||\n column.classification === \"credential\"\n );\n}\nfunction rowProjectionExcludesColumnSchema(\n column: Column,\n rules: ExportProjection[\"rules\"],\n): boolean {\n return rules.some(\n (rule) =>\n rule.target === \"row\" &&\n (!rule.exportable || rule.classification !== \"ordinary\") &&\n (rule.path === column.path || column.path.startsWith(`${rule.path}/`)),\n );\n}\n/** Project the schema with the same payload rules that govern column metadata. */\nfunction projectColumnSchema(\n column: Column,\n rules: ExportProjection[\"rules\"],\n omit: Omission[],\n): Column[\"schema\"] | undefined {\n if (column.schema === undefined) return undefined;\n const root = `/columns/${enc(column.id)}/schema`;\n const schemaRules = rules\n .filter((rule) => rule.target === \"payload\" && (rule.path === root || rule.path.startsWith(`${root}/`)))\n .map((rule) => ({ ...rule, path: rule.path.slice(root.length) }));\n const projected = projectValue(column.schema, schemaRules, \"payload\", omit);\n if (schemaRules.some((rule) => rule.path === \"\" && (!rule.exportable || rule.classification !== \"ordinary\")))\n return undefined;\n return projected as Column[\"schema\"];\n}\nfunction normalize(\n p: Payload,\n projection: ExportProjection,\n omit: Omission[],\n): Payload {\n if (p.kind === \"text\" || p.kind === \"markdown\") {\n if (typeof p.text !== \"string\")\n throw new ContentTransferError(\n \"unsupported-value\",\n \"Text payload must contain a string\",\n \"/text\",\n );\n const value = projectValue(p.text, projection.rules, \"payload\", omit);\n const text = typeof value === \"string\" ? value : \"\";\n // A code block keeps its language (downloads take its extension; Markdown fences it).\n return p.kind === \"text\" && typeof p.language === \"string\" && p.language.trim()\n ? { kind: \"text\", text, language: p.language.trim() }\n : { kind: p.kind, text };\n }\n if (p.kind === \"json\")\n return {\n kind: \"json\",\n value: projectValue(p.value, projection.rules, \"payload\", omit),\n };\n if (p.kind === \"registered\")\n return {\n kind: \"registered\",\n format: p.format,\n value: projectValue(p.value, projection.rules, \"payload\", omit),\n };\n const rowsPayload = p as Extract<Payload, { kind: \"rows\" }>;\n if (p.kind !== \"rows\")\n throw new ContentTransferError(\n \"invalid-payload\",\n \"Unsupported payload kind\",\n );\n const sourceColumns = rowsPayload.columns.map((column) => ({ ...column }));\n const columns = sourceColumns\n .filter((column) => !excludedColumn(column))\n .map((column) => {\n // Do not spread source schema back after an explicit schema or row exclusion.\n const { schema: _sourceSchema, ...withoutSchema } = column;\n const schema = rowProjectionExcludesColumnSchema(column, projection.rules)\n ? undefined\n : projectColumnSchema(column, projection.rules, omit);\n return {\n ...withoutSchema,\n ...(schema === undefined ? {} : { schema }),\n };\n });\n resolveTransferRegistry(columns);\n columns.forEach((column) => validatePointer(column.path));\n const rules = [\n ...projection.rules,\n ...sourceColumns\n .filter(excludedColumn)\n .map((c) => ({\n target: \"row\" as const,\n path: c.path,\n exportable: c.exportable ?? true,\n classification: c.classification ?? (\"ordinary\" as const),\n })),\n ];\n return {\n kind: \"rows\",\n columns,\n rows: rowsPayload.rows.map((row, index) => {\n if (!row || typeof row !== \"object\" || Array.isArray(row))\n throw new ContentTransferError(\n \"invalid-row\",\n \"A table row must be a JSON object\",\n `/rows/${index}`,\n );\n const projected = projectValue(row, rules, \"row\", omit);\n return projected === null ? {} : (projected as Record<string, Json>);\n }),\n };\n}\nexport function projectSnapshot(\n raw: RawSnapshot,\n projection: ExportProjection,\n): Snapshot {\n const omissions = raw.omissions.map((o) => ({ ...o }));\n const normalized = normalize(raw.payload, projection, omissions);\n const payload = raw.limits\n ? limitPayload(normalized, raw.limits, omissions)\n : normalized;\n const included =\n payload.kind === \"rows\" ? payload.rows.length : raw.coverage.included;\n return deepFreeze({\n ...raw,\n payload,\n coverage: {\n ...raw.coverage,\n included,\n ...(included < raw.coverage.included\n ? { status: \"partial\" as const, reason: \"Source export limit applied\" }\n : {}),\n },\n omissions,\n sections: raw.sections\n .filter(\n (section) =>\n !projection.rules.some(\n (rule) =>\n rule.target === \"payload\" &&\n (!rule.exportable || rule.classification !== \"ordinary\") &&\n (section.path === rule.path ||\n section.path.startsWith(`${rule.path}/`)),\n ),\n )\n .map((x) => ({ ...x })),\n ...(raw.limits ? { limits: { ...raw.limits } } : {}),\n } as Snapshot);\n}\nconst uid = () =>\n globalThis.crypto?.randomUUID?.() ?? `capture-${Date.now()}-${Math.random()}`;\nexport function directSource(\n payload: Payload,\n options: Partial<\n Pick<\n RawSnapshot,\n | \"id\"\n | \"sourceId\"\n | \"revision\"\n | \"label\"\n | \"coverage\"\n | \"unsavedChanges\"\n | \"sections\"\n | \"limits\"\n > & { projection: ExportProjection }\n > = {},\n): CapturedSource {\n const id = options.id ?? uid();\n return {\n projection: options.projection ?? { rules: [] },\n snapshot: {\n id,\n sourceId: options.sourceId ?? id,\n revision: options.revision ?? uid(),\n capturedAt: new Date().toISOString(),\n label: options.label ?? \"Content\",\n payload,\n coverage: options.coverage ?? {\n status: \"complete\",\n included: payload.kind === \"rows\" ? payload.rows.length : 1,\n total: payload.kind === \"rows\" ? payload.rows.length : 1,\n },\n unsavedChanges: options.unsavedChanges ?? false,\n omissions: [],\n sections: options.sections ?? [],\n ...(options.limits ? { limits: { ...options.limits } } : {}),\n },\n };\n}\nexport async function capture(\n source: Source | Payload,\n signal: AbortSignal,\n scope: TransferScope = \"target\",\n): Promise<{ snapshot: Snapshot; allMatching?: BoundAllMatching }> {\n stop(signal);\n const captured =\n \"kind\" in source\n ? directSource(source)\n : await source.capture({ scope, signal });\n stop(signal);\n const all = captured.allMatching;\n if (\n all &&\n (all.query.sourceId !== captured.snapshot.sourceId ||\n all.query.revision !== captured.snapshot.revision ||\n all.query.order.length === 0)\n )\n throw new ContentTransferError(\n \"query-mismatch\",\n \"All-matching query must be captured with stable ordering\",\n );\n const snapshot = projectSnapshot(captured.snapshot, captured.projection);\n if (!all) return { snapshot };\n return {\n snapshot,\n allMatching: deepFreeze({\n [bound]: true as const,\n source: {\n ...all,\n query: {\n ...all.query,\n query: cloneTransferJson(all.query.query),\n order: all.query.order.map((x) => ({ ...x })),\n },\n },\n projection: { rules: captured.projection.rules.map((x) => ({ ...x })) },\n // Page rows must be normalized against the original declaration so excluded\n // column values are removed before stable IDs or any caller can observe them.\n columns:\n captured.snapshot.payload.kind === \"rows\"\n ? captured.snapshot.payload.columns.map((column) => ({ ...column }))\n : [],\n ...(snapshot.limits ? { limits: { ...snapshot.limits } } : {}),\n }),\n };\n}\nexport function createDraft(s: Snapshot): Draft {\n return deepFreeze({\n snapshotId: s.id,\n sourceId: s.sourceId,\n sourceRevision: s.revision,\n revision: 0,\n payload: normalize(s.payload, { rules: [] }, []),\n omissions: [...s.omissions],\n manuallyEdited: false,\n provenance: [{ kind: \"original\", description: \"Captured source snapshot\" }],\n });\n}\nfunction revise(\n d: Draft,\n p: Payload,\n o: Omission[],\n description: string,\n manual = d.manuallyEdited,\n kind: Draft[\"provenance\"][number][\"kind\"] = \"deterministic\",\n baseOmissions: readonly Omission[] = d.omissions,\n): Draft {\n const { history: _history, ...previous } = d;\n return deepFreeze({\n ...d,\n revision: d.revision + 1,\n payload: normalize(p, { rules: [] }, []),\n omissions: [...baseOmissions, ...o],\n manuallyEdited: manual,\n provenance: [...d.provenance, { kind, description }],\n history: [...(d.history ?? []), previous],\n });\n}\nexport const editDraft = (d: Draft, p: Payload) =>\n revise(d, p, [], \"Manual edit\", true, \"manual\");\n/**\n * A deterministic revision of `d` (the structure trees, PP-06/07): same snapshot, next revision,\n * the given omissions added. `baseOmissions` replaces the draft's own when a step re-projects from\n * the snapshot (a section selection), exactly as `applySectionSelection` does.\n */\nexport const deriveDraft = (\n d: Draft,\n p: Payload,\n o: Omission[],\n description: string,\n baseOmissions: readonly Omission[] = d.omissions,\n) => revise(d, p, o, description, d.manuallyEdited, \"deterministic\", baseOmissions);\nexport const resetDraft = (s: Snapshot) => createDraft(s);\nexport const sourceChanged = (d: Draft, s: Snapshot) =>\n d.sourceId !== s.sourceId || d.sourceRevision !== s.revision;\nexport function undoDraft(d: Draft): Draft {\n const previous = d.history?.at(-1);\n if (!previous) return d;\n return deepFreeze({\n ...previous,\n revision: d.revision + 1,\n history: d.history!.slice(0, -1),\n });\n}\n/** Keep and regenerate are explicit choices; observing fresh data never overwrites edits. */\nexport function refreshDraft(\n draft: Draft,\n snapshot: Snapshot,\n choice: \"keep\" | \"regenerate\",\n): Draft {\n return choice === \"keep\" ? draft : createDraft(snapshot);\n}\nexport function applySectionSelection(\n d: Draft,\n s: Snapshot,\n ids: readonly string[],\n): Draft {\n if (\n d.snapshotId !== s.id ||\n d.sourceId !== s.sourceId ||\n d.sourceRevision !== s.revision\n )\n throw new ContentTransferError(\n \"snapshot-mismatch\",\n \"Section selection requires the draft's captured source snapshot\",\n );\n const selected = new Set(ids),\n omit: Omission[] = [],\n paths = s.sections\n .filter((x) => !selected.has(x.id))\n .map((x) => {\n omit.push({ path: x.path, reason: \"excluded\" });\n return x.path;\n }),\n // Selection is a replacement projection from the snapshot, so only the\n // snapshot's omissions remain authoritative. Retaining draft omissions\n // would preserve stale transforms after the snapshot payload is rebuilt.\n baseOmissions = s.omissions;\n if (s.payload.kind === \"json\")\n return revise(\n d,\n { kind: \"json\", value: without(s.payload.value, paths, []) ?? null },\n omit,\n \"Selected sections\",\n d.manuallyEdited,\n \"deterministic\",\n baseOmissions,\n );\n if (s.payload.kind === \"registered\")\n return revise(\n d,\n {\n kind: \"registered\",\n format: s.payload.format,\n value: without(s.payload.value, paths, []) ?? null,\n },\n omit,\n \"Selected sections\",\n d.manuallyEdited,\n \"deterministic\",\n baseOmissions,\n );\n if (s.payload.kind === \"markdown\" || s.payload.kind === \"text\") {\n // A prose section is a character range of the text: `/text/<start>-<end>`.\n const text = s.payload.text;\n const ranges = paths\n .map((path) => {\n const m = /^\\/text\\/(\\d+)-(\\d+)$/.exec(path);\n const start = m ? Number(m[1]) : NaN,\n end = m ? Number(m[2]) : NaN;\n if (!m || start > end || end > text.length)\n throw new ContentTransferError(\n \"invalid-section\",\n `Section \"${path}\" is not a range of this text. Declare a prose section as /text/<start>-<end> within the text.`,\n );\n return [start, end] as const;\n })\n .sort((a, b) => b[0] - a[0]);\n let kept = text;\n for (const [start, end] of ranges) kept = kept.slice(0, start) + kept.slice(end);\n return revise(\n d,\n { ...s.payload, text: kept },\n omit,\n \"Selected sections\",\n d.manuallyEdited,\n \"deterministic\",\n baseOmissions,\n );\n }\n return revise(\n d,\n s.payload,\n omit,\n \"Selected sections\",\n d.manuallyEdited,\n \"deterministic\",\n baseOmissions,\n );\n}\nexport function transformRows(\n d: Draft,\n o: {\n search?: string;\n sort?: { column: string; direction: \"asc\" | \"desc\" };\n filters?: Readonly<Record<string, string>>;\n /** Positions in this call's input draft rows; unlike ids, no guessed key is used. */\n selectedIndices?: readonly number[];\n selectedIds?: readonly string[];\n idColumn?: string;\n visibleColumns?: readonly string[];\n structuredProjection?: \"full\" | \"visible\";\n top?: number | null;\n },\n): Draft {\n if (d.payload.kind !== \"rows\") return d;\n // Validate before deciding whether truncation is necessary. A negative/NaN\n // top is an invalid request even for an empty result set.\n if (o.top != null) assertLimit(o.top, \"maxRows\");\n const cols = o.visibleColumns\n ? o.visibleColumns\n .map((id) =>\n d.payload.kind === \"rows\"\n ? d.payload.columns.find((c) => c.id === id)\n : undefined,\n )\n .filter((c): c is Column => Boolean(c))\n : d.payload.columns.filter((c) => c.visible);\n const get = readTransferCell;\n let rows = [...d.payload.rows],\n omit: Omission[] = [];\n const before = rows.length;\n if (o.selectedIndices !== undefined) {\n const selected = new Set<number>();\n for (const index of o.selectedIndices) {\n if (!Number.isSafeInteger(index) || index < 0 || index >= d.payload.rows.length)\n throw new ContentTransferError(\n \"invalid-selection-index\",\n \"Selected row indexes must be non-negative positions in the input draft rows\",\n );\n selected.add(index);\n }\n rows = d.payload.rows.filter((_, index) => selected.has(index));\n }\n if (o.selectedIds && o.idColumn) {\n const set = new Set(o.selectedIds);\n const idColumn = d.payload.columns.find((c) => c.id === o.idColumn);\n rows = rows.filter((r) =>\n set.has(String(idColumn ? get(r, idColumn) : r[o.idColumn!])),\n );\n }\n if (o.search) {\n const q = o.search.toLowerCase();\n rows = rows.filter((r) =>\n cols.some((c) =>\n String(get(r, c) ?? \"\")\n .toLowerCase()\n .includes(q),\n ),\n );\n }\n for (const [id, q] of Object.entries(o.filters ?? {})) {\n const c = cols.find((x) => x.id === id);\n if (c)\n rows = rows.filter((r) =>\n String(get(r, c) ?? \"\")\n .toLowerCase()\n .includes(q.toLowerCase()),\n );\n }\n if (rows.length !== before)\n omit.push({\n path: \"/rows\",\n reason: \"filtered\",\n count: before - rows.length,\n });\n if (o.sort) {\n const c = cols.find((x) => x.id === o.sort!.column);\n if (c)\n rows.sort((a, b) => {\n const left = get(a, c);\n const right = get(b, c);\n const compare =\n typeof left === \"number\" && typeof right === \"number\"\n ? left - right\n : String(left ?? \"\").localeCompare(String(right ?? \"\"));\n return compare * (o.sort!.direction === \"asc\" ? 1 : -1);\n });\n }\n if (o.top != null && rows.length > o.top) {\n omit.push({\n path: \"/rows\",\n reason: \"truncated\",\n count: rows.length - o.top,\n });\n rows = rows.slice(0, o.top);\n }\n if (o.structuredProjection === \"visible\") {\n rows = rows.map((r) =>\n Object.fromEntries(cols.map((c) => [c.id, get(r, c) ?? null])),\n );\n const excludedColumns = d.payload.columns.length - cols.length;\n if (excludedColumns > 0)\n omit.push({\n path: \"/columns\",\n reason: \"excluded\",\n count: excludedColumns,\n });\n }\n return revise(\n d,\n {\n kind: \"rows\",\n rows,\n columns: cols.map((c) => ({\n ...c,\n visible: true,\n ...(o.structuredProjection === \"visible\"\n ? { path: `/${enc(c.id)}` }\n : {}),\n })),\n },\n omit,\n \"Prepared row subset\",\n );\n}\nexport function reduceJson(\n draft: Draft,\n options: {\n exclude?: readonly string[];\n maxDepth?: number | null;\n maxStringChars?: number | null;\n maxArrayItems?: number | null;\n },\n): Draft {\n if (draft.payload.kind !== \"json\" && draft.payload.kind !== \"registered\")\n return draft;\n const omissions: Omission[] = [];\n options.exclude?.forEach(validatePointer);\n const filtered =\n without(cloneTransferJson(draft.payload.value), options.exclude ?? [], omissions) ??\n null;\n const payload = limitPayload(\n { ...draft.payload, value: filtered },\n options,\n omissions,\n );\n return revise(draft, payload, omissions, \"Reduced JSON\");\n}\nexport async function collectAllMatching(\n c: BoundAllMatching,\n signal: AbortSignal,\n): Promise<{\n rows: Record<string, Json>[];\n coverage: RawSnapshot[\"coverage\"];\n omissions: Omission[];\n error?: ContentTransferError;\n}> {\n const rows: Record<string, Json>[] = [],\n omissions: Omission[] = [],\n ids = new Set<string>(),\n cursors = new Set<string>();\n let cursor: string | null = null,\n token: string | null | undefined,\n last: RawSnapshot[\"coverage\"] = {\n status: \"unknown\",\n included: 0,\n total: null,\n };\n let certifiedTotal: number | null | undefined;\n const maxRows = c.limits?.maxRows;\n if (maxRows != null) assertLimit(maxRows, \"maxRows\");\n const finishAtLimit = (total: number | null, knownOmitted?: number) => ({\n rows: deepFreeze(rows),\n omissions: deepFreeze([\n ...omissions,\n {\n path: \"/rows\",\n reason: \"truncated\" as const,\n ...(knownOmitted === undefined || knownOmitted <= 0\n ? {}\n : { count: knownOmitted }),\n },\n ]),\n coverage: deepFreeze({\n status: \"partial\" as const,\n included: rows.length,\n total,\n reason: \"Source export limit applied\",\n }),\n });\n try {\n // A zero source cap is still a real cap: do not request a page merely to\n // discover data that the caller is not allowed to receive.\n if (maxRows === 0) return finishAtLimit(null);\n do {\n stop(signal);\n const page = await c.source.page({\n query: c.source.query,\n cursor,\n consistencyToken: token ?? null,\n signal,\n });\n stop(signal);\n if (token === undefined) token = page.consistencyToken;\n else if (page.consistencyToken !== token)\n throw new ContentTransferError(\n \"consistency-token-changed\",\n \"All-matching export changed during collection\",\n );\n if (page.coverage.included !== page.rows.length)\n throw new ContentTransferError(\n \"invalid-page-coverage\",\n \"Page count does not match returned rows\",\n );\n if (\n page.coverage.total !== null &&\n (!Number.isSafeInteger(page.coverage.total) || page.coverage.total < 0)\n )\n throw new ContentTransferError(\n \"invalid-page-coverage\",\n \"Page total must be a non-negative integer or null\",\n );\n if (certifiedTotal === undefined) certifiedTotal = page.coverage.total;\n else if (page.coverage.total !== certifiedTotal)\n throw new ContentTransferError(\n \"inconsistent-total\",\n \"All-matching source changed its reported total during collection\",\n );\n const projected = (\n normalize(\n { kind: \"rows\", rows: page.rows, columns: [...c.columns] },\n c.projection,\n omissions,\n ) as Extract<Payload, { kind: \"rows\" }>\n ).rows;\n let uniqueBeyondLimit = 0;\n for (const row of projected) {\n // The callback is part of the transfer boundary, so it receives only\n // the projected row. Sources requiring identity must declare an\n // exportable opaque ID; raw credential/non-exportable fields never\n // cross this seam merely for de-duplication.\n const id = c.source.getRowId(row);\n if (!id)\n throw new ContentTransferError(\n \"invalid-row-id\",\n \"All-matching source returned empty projected ID\",\n );\n if (!ids.has(id)) {\n ids.add(id);\n if (maxRows == null || rows.length < maxRows)\n rows.push(\n (\n limitPayload(\n { kind: \"rows\", rows: [row], columns: [...c.columns] },\n { ...c.limits, maxRows: null },\n omissions,\n ) as Extract<Payload, { kind: \"rows\" }>\n ).rows[0]!,\n );\n else uniqueBeyondLimit += 1;\n } else omissions.push({ path: `/rows/${enc(id)}`, reason: \"duplicate\" });\n }\n // Check after de-duplication and before the source-cap return: ids also\n // includes unique rows that the cap intentionally did not retain.\n if (page.coverage.total !== null && page.coverage.total < ids.size)\n throw new ContentTransferError(\n \"invalid-page-coverage\",\n \"Page total is smaller than the accumulated unique row count\",\n );\n last = page.coverage;\n if (\n maxRows != null &&\n rows.length >= maxRows &&\n (uniqueBeyondLimit > 0 || page.nextCursor !== null)\n )\n return finishAtLimit(\n page.coverage.total,\n page.nextCursor === null\n ? uniqueBeyondLimit\n : page.coverage.total != null && page.coverage.total > rows.length\n ? page.coverage.total - rows.length\n : undefined,\n );\n cursor = page.nextCursor;\n if (cursor !== null && (!cursor || cursors.has(cursor)))\n throw new ContentTransferError(\n \"non-advancing-cursor\",\n \"All-matching source repeated a cursor\",\n );\n if (cursor) cursors.add(cursor);\n } while (cursor !== null);\n const complete =\n c.source.stableSnapshot === true &&\n last.status === \"complete\" &&\n last.total === rows.length &&\n token !== undefined;\n return {\n rows: deepFreeze(rows),\n omissions: deepFreeze(omissions),\n coverage: deepFreeze(\n complete\n ? { ...last, included: rows.length }\n : {\n ...last,\n status: \"partial\",\n included: rows.length,\n reason:\n last.reason ??\n \"Source did not certify a stable complete snapshot\",\n },\n ),\n };\n } catch (e) {\n const error =\n e instanceof ContentTransferError\n ? e\n : new ContentTransferError(\n signal.aborted ? \"cancelled\" : \"page-failed\",\n e instanceof Error ? e.message : \"Page collection failed\",\n );\n return {\n rows: deepFreeze(rows),\n omissions: deepFreeze(omissions),\n coverage: deepFreeze({\n status: \"partial\",\n included: rows.length,\n total: last.total,\n reason: error.message,\n }),\n error,\n };\n }\n}\n// An EMPTY cell is empty text. `null` is how a row carries \"nothing here\"\n// (hosts normalise a missing cell to JSON null so the row keeps its shape), and\n// until 2026-09-21 it serialized as the word \"null\" in every plain / CSV / TSV\n// / Markdown copy — six \"null\"s across a copied row of a data table.\nconst textValue = (value: Json | undefined): string =>\n value === undefined || value === null\n ? \"\"\n : typeof value === \"string\"\n ? value\n : stringifyJson(value, { style: \"compact\" });\n\n/** Preserve artifact-byte identity even if a consumer mutates a returned view. */\nexport function sealTransferArtifact(artifact: Artifact): Artifact {\n if (!artifact.file)\n return deepFreeze({\n ...artifact,\n omissions: artifact.omissions.map((o) => ({ ...o })),\n });\n const bytes = artifact.file.bytes.slice();\n const file = {\n filename: artifact.file.filename,\n mime: artifact.file.mime,\n get bytes() {\n return bytes.slice();\n },\n };\n return deepFreeze({\n ...artifact,\n file,\n omissions: artifact.omissions.map((o) => ({ ...o })),\n });\n}\n\n/** The columns a text serialization writes: visible, exportable, never secret or credential. */\nexport function exportedColumns(columns: readonly Column[]): Column[] {\n return columns.filter(\n (c) =>\n c.visible &&\n c.exportable !== false &&\n c.classification !== \"secret\" &&\n c.classification !== \"credential\",\n );\n}\n\nexport function serialize(\n draft: Draft,\n format:\n \"plain\" | \"markdown\" | \"json\" | \"compact-json\" | \"csv\" | \"tsv\" | \"html\",\n options: { spreadsheetSafe?: boolean; filename?: string } = {},\n): Artifact {\n const payload = draft.payload;\n const omissions = draft.omissions.map((o) => ({ ...o }));\n let plainText = \"\",\n html: string | undefined;\n let mime = \"text/plain;charset=utf-8\",\n extension = \"txt\";\n const columnCell = (row: Record<string, Json>, column: Column) =>\n textValue(readTransferCell(row, column));\n if ((format === \"csv\" || format === \"tsv\") && payload.kind !== \"rows\") {\n throw new ContentTransferError(\n \"unsupported-format\",\n `${format.toUpperCase()} requires tabular data`,\n );\n }\n if (format === \"json\" || format === \"compact-json\") {\n const value =\n payload.kind === \"rows\"\n ? payload.rows\n : payload.kind === \"text\" || payload.kind === \"markdown\"\n ? payload.text\n : payload.value;\n plainText = stringifyJson(value, {\n style: format === \"json\" ? \"pretty\" : \"compact\",\n });\n mime = \"application/json;charset=utf-8\";\n extension = \"json\";\n } else if (payload.kind === \"rows\") {\n const columns = exportedColumns(payload.columns);\n if (format === \"markdown\") {\n const escapeCell = (value: string) =>\n escapeHtml(value)\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/\\|/g, \"\\\\|\")\n .replace(/\\r\\n|\\r|\\n/g, \"<br>\");\n plainText = [\n `| ${columns.map((c) => escapeCell(c.label)).join(\" | \")} |`,\n `| ${columns.map(() => \"---\").join(\" | \")} |`,\n ...payload.rows.map(\n (row) =>\n `| ${columns.map((c) => escapeCell(columnCell(row, c))).join(\" | \")} |`,\n ),\n ].join(\"\\n\");\n mime = \"text/markdown;charset=utf-8\";\n extension = \"md\";\n } else {\n const delimiter = format === \"csv\" ? \",\" : \"\\t\";\n const safe = options.spreadsheetSafe ?? format === \"tsv\";\n const encode = (value: string, path: string, isString = true) => {\n let text = value;\n if (safe && isString && /^[\\t\\r\\n ]*[=+\\-@]/.test(text)) {\n text = `'${text}`;\n omissions.push({ path, reason: \"escaped\", count: 1 });\n }\n return text.includes(delimiter) || /[\"\\n\\r]/.test(text)\n ? `\"${text.replace(/\"/g, '\"\"')}\"`\n : text;\n };\n plainText = [\n columns\n .map((c) => encode(c.label, `/columns/${enc(c.id)}`))\n .join(delimiter),\n ...payload.rows.map((row, index) =>\n columns\n .map((c) =>\n encode(\n columnCell(row, c),\n `/rows/${index}${c.path}`,\n typeof readTransferCell(row, c) === \"string\",\n ),\n )\n .join(delimiter),\n ),\n ].join(\"\\n\");\n if (format === \"csv\") {\n mime = \"text/csv;charset=utf-8\";\n extension = \"csv\";\n }\n if (format === \"tsv\") {\n mime = \"text/tab-separated-values;charset=utf-8\";\n extension = \"tsv\";\n }\n }\n if (format === \"html\")\n html = `<table><thead><tr>${columns.map((c) => `<th>${escapeHtml(c.label)}</th>`).join(\"\")}</tr></thead><tbody>${payload.rows.map((row) => `<tr>${columns.map((c) => `<td>${escapeHtml(columnCell(row, c)).replace(/\\r\\n|\\r|\\n/g, \"<br>\")}</td>`).join(\"\")}</tr>`).join(\"\")}</tbody></table>`;\n } else {\n plainText =\n payload.kind === \"text\" || payload.kind === \"markdown\"\n ? payload.text\n : stringifyJson(payload.value, { style: \"pretty\" });\n if (format === \"markdown\") {\n mime = \"text/markdown;charset=utf-8\";\n extension = \"md\";\n }\n if (format === \"html\") html = `<pre>${escapeHtml(plainText)}</pre>`;\n }\n if (format === \"html\") {\n mime = \"text/html;charset=utf-8\";\n extension = \"html\";\n }\n return sealTransferArtifact({\n snapshotId: draft.snapshotId,\n draftRevision: draft.revision,\n format,\n plainText,\n ...(html === undefined ? {} : { html }),\n file: {\n filename: options.filename ?? `export.${extension}`,\n mime,\n bytes: new TextEncoder().encode(\n format === \"html\" ? (html ?? plainText) : plainText,\n ),\n },\n omissions,\n });\n}\nexport function createBrowserTransport(): Transport {\n return {\n async copy(a, signal) {\n const cancelled = () =>\n signal.aborted ? { status: \"cancelled\" as const } : undefined;\n try {\n if (cancelled()) return cancelled()!;\n const c = globalThis.navigator?.clipboard;\n if (!c?.write && !c?.writeText)\n return {\n status: \"error\",\n code: \"clipboard-unavailable\",\n message: \"Clipboard writing is unavailable\",\n retryable: false,\n };\n if (\n a.html &&\n c.write &&\n typeof globalThis.ClipboardItem === \"function\"\n ) {\n try {\n await c.write([\n new ClipboardItem({\n \"text/plain\": new Blob([a.plainText], { type: \"text/plain\" }),\n \"text/html\": new Blob([a.html], { type: \"text/html\" }),\n }),\n ]);\n if (cancelled()) return cancelled()!;\n return {\n status: \"success\",\n delivered: \"clipboard\",\n mimeTypes: [\"text/plain\", \"text/html\"],\n };\n } catch {\n if (cancelled()) return cancelled()!;\n }\n }\n if (c.writeText) {\n await c.writeText(a.plainText);\n if (cancelled()) return cancelled()!;\n return a.html\n ? {\n status: \"degraded\",\n delivered: \"plain-text\",\n reason: \"Rich clipboard write was unavailable or rejected\",\n }\n : {\n status: \"success\",\n delivered: \"clipboard\",\n mimeTypes: [\"text/plain\"],\n };\n }\n return {\n status: \"error\",\n code: \"clipboard-rejected\",\n message: \"Clipboard rejected the write\",\n retryable: true,\n };\n } catch (e) {\n return (\n cancelled() ?? {\n status: \"error\",\n code: \"clipboard-rejected\",\n message:\n e instanceof Error ? e.message : \"Clipboard rejected the write\",\n retryable: true,\n }\n );\n }\n },\n async download(a, signal) {\n if (signal.aborted) return { status: \"cancelled\" };\n if (!a.file || typeof document === \"undefined\" || !URL?.createObjectURL)\n return {\n status: \"error\",\n code: \"download-unavailable\",\n message: \"Download is unavailable\",\n retryable: false,\n };\n let url: string | undefined;\n try {\n url = URL.createObjectURL(\n new Blob([a.file.bytes.slice().buffer as ArrayBuffer], {\n type: a.file.mime,\n }),\n );\n const link = document.createElement(\"a\");\n link.href = url;\n link.download = a.file.filename;\n link.click();\n return signal.aborted\n ? { status: \"cancelled\" }\n : {\n status: \"success\",\n delivered: \"download-started\",\n mimeTypes: [a.file.mime],\n };\n } catch (e) {\n return signal.aborted\n ? { status: \"cancelled\" }\n : {\n status: \"error\",\n code: \"download-failed\",\n message: e instanceof Error ? e.message : \"Download failed\",\n retryable: true,\n };\n } finally {\n if (url) setTimeout(() => URL.revokeObjectURL(url!), 0);\n }\n },\n };\n}\n\nfunction assertLimit(value: number | null | undefined, name: string): void {\n if (value != null && (!Number.isSafeInteger(value) || value < 0)) {\n throw new ContentTransferError(\n \"invalid-limit\",\n `${name} must be a non-negative safe integer or null`,\n );\n }\n}\nexport type TransferPreferenceOverrides = Partial<\n Omit<TransferPreferences, \"limits\">\n> & { limits?: Partial<TransferLimits> };\nconst limitKeys = [\n \"maxRows\",\n \"maxStringChars\",\n \"maxDepth\",\n \"maxArrayItems\",\n \"targetTokens\",\n] as const;\n/** Restrictions intersect: neither a menu nor a provider can relax a source limit. */\nexport function resolveTransferPreferences(\n provider: TransferPreferenceOverrides = {},\n menu: TransferPreferenceOverrides = {},\n sourceLimits: Partial<TransferLimits> = {},\n): TransferPreferences {\n const limits: TransferLimits = {\n maxRows: null,\n maxStringChars: null,\n maxDepth: null,\n maxArrayItems: null,\n targetTokens: null,\n };\n for (const key of limitKeys) {\n const candidates = [\n provider.limits?.[key],\n menu.limits?.[key],\n sourceLimits[key],\n ];\n candidates.forEach((value) => assertLimit(value, key));\n const finite = candidates.filter((value): value is number => value != null);\n limits[key] = finite.length ? Math.min(...finite) : null;\n }\n return deepFreeze({\n initialFormat: menu.initialFormat ?? provider.initialFormat ?? \"plain\",\n initialPreset: menu.initialPreset ?? provider.initialPreset ?? \"full\",\n initialEnvelope: menu.initialEnvelope ?? provider.initialEnvelope ?? \"ai\",\n limits,\n });\n}\n/**\n * True for the objects React uses to represent a component or an element —\n * `forwardRef`, `memo`, `lazy`, portals, elements. They are all tagged with the\n * `$$typeof` symbol, which is exactly the test React uses itself.\n */\nfunction isReactElementType(value: object): boolean {\n const tag = (value as { $$typeof?: unknown }).$$typeof;\n return typeof tag === \"symbol\";\n}\n\nfunction cloneTransferRegistration(\n value: unknown,\n path = \"registration\",\n seen = new WeakSet<object>(),\n): unknown {\n if (value === null || (typeof value !== \"object\" && typeof value !== \"function\"))\n return value;\n // Registered behavior remains callable by identity. Freezing a caller-owned\n // function would mutate it and its attached state.\n if (typeof value === \"function\") return value;\n // A React component or element is registered BEHAVIOUR too — it is rendered,\n // never inspected — and `React.forwardRef` / `React.memo` produce OBJECTS\n // rather than functions. React's development build defines their\n // `displayName` as an ACCESSOR, so walking into one threw \"cannot use\n // accessors\" at `registration/icon/displayName` and took EVERY dashboard page\n // that renders a transfer menu down behind its error boundary (2026-09-14;\n // every AI variant the admin dashboard registers carries a lucide `icon`).\n // Recognised the way React itself recognises them.\n if (isReactElementType(value)) return value;\n if (seen.has(value))\n throw new ContentTransferError(\n \"unsupported-registration-metadata\",\n \"Circular transfer registration metadata is unsupported\",\n path,\n );\n seen.add(value);\n if (Array.isArray(value)) {\n const copy = value.map((item, index) =>\n cloneTransferRegistration(item, `${path}/${index}`, seen),\n );\n seen.delete(value);\n return Object.freeze(copy);\n }\n const prototype = Object.getPrototypeOf(value);\n if (prototype !== Object.prototype && prototype !== null)\n throw new ContentTransferError(\n \"unsupported-registration-metadata\",\n \"Transfer registration metadata must use plain objects and arrays\",\n path,\n );\n const copy = Object.create(prototype) as Record<PropertyKey, unknown>;\n for (const key of Reflect.ownKeys(value)) {\n const descriptor = Object.getOwnPropertyDescriptor(value, key)!;\n if (!(\"value\" in descriptor))\n throw new ContentTransferError(\n \"unsupported-registration-metadata\",\n \"Transfer registration metadata cannot use accessors\",\n `${path}/${String(key)}`,\n );\n Object.defineProperty(copy, key, {\n value: cloneTransferRegistration(\n descriptor.value,\n `${path}/${String(key)}`,\n seen,\n ),\n enumerable: descriptor.enumerable === true,\n writable: false,\n configurable: false,\n });\n }\n seen.delete(value);\n return Object.freeze(copy);\n}\n\n/** One stable id per registered format or action; ambiguous overrides fail loudly. */\nexport function resolveTransferRegistry<T extends { id: string }>(\n ...groups: readonly (readonly T[])[]\n): readonly T[] {\n const ids = new Set<string>();\n const result: T[] = [];\n for (const group of groups)\n for (const item of group) {\n const copy = cloneTransferRegistration(item) as T;\n if (!copy.id.trim() || ids.has(copy.id))\n throw new ContentTransferError(\n \"duplicate-registration\",\n `Transfer id ${JSON.stringify(copy.id)} is empty or registered more than once`,\n );\n ids.add(copy.id);\n result.push(copy);\n }\n return Object.freeze(result);\n}\n\nexport type TransferSurfaceDeclaration = {\n surfaceName: string;\n label?: string;\n values: readonly {\n name: string;\n label: string;\n exportable?: boolean;\n classification?: \"ordinary\" | \"secret\" | \"credential\";\n includedByDefault?: boolean;\n }[];\n};\n/** A structural adapter to the actual mounted surface, never a global name lookup. */\nexport function createSurfaceTransferHandle(options: {\n instanceId: string;\n manifest: TransferSurfaceDeclaration;\n isMounted(): boolean;\n getScope(): Record<string, unknown> | Promise<Record<string, unknown>>;\n getRevision?(): string;\n unsavedChanges?(): boolean;\n projection?: ExportProjection;\n limits?: TransferLimits;\n}): SurfaceHandle {\n const declarations = resolveTransferRegistry(\n options.manifest.values.map((value) => ({ ...value, id: value.name })),\n );\n return {\n instanceId: options.instanceId,\n surfaceName: options.manifest.surfaceName,\n isMounted: options.isMounted,\n async capture(signal) {\n stop(signal);\n if (!options.isMounted())\n throw new ContentTransferError(\n \"surface-unmounted\",\n \"The captured surface is no longer mounted\",\n );\n const revisionBefore = options.getRevision?.();\n const scope = await options.getScope();\n stop(signal);\n if (!options.isMounted())\n throw new ContentTransferError(\n \"surface-unmounted\",\n \"The surface closed during capture\",\n );\n if (\n revisionBefore !== undefined &&\n options.getRevision?.() !== revisionBefore\n )\n throw new ContentTransferError(\n \"surface-changed\",\n \"The surface changed during capture; capture again\",\n );\n const value: Record<string, Json> = {};\n for (const declaration of declarations) {\n if (\n Object.hasOwn(scope, declaration.name) &&\n scope[declaration.name] !== undefined\n ) {\n Object.defineProperty(value, declaration.name, {\n value: cloneSurfaceValue(\n scope[declaration.name],\n `/${enc(declaration.name)}`,\n ),\n enumerable: true,\n writable: true,\n configurable: true,\n });\n }\n }\n return directSource(\n { kind: \"json\", value },\n {\n sourceId: options.instanceId,\n revision: revisionBefore ?? uid(),\n label: options.manifest.label ?? options.manifest.surfaceName,\n unsavedChanges: options.unsavedChanges?.() ?? false,\n sections: declarations\n .filter((d) => Object.hasOwn(value, d.name))\n .map((d) => ({\n id: d.name,\n label: d.label,\n path: `/${enc(d.name)}`,\n includedByDefault: d.includedByDefault ?? true,\n })),\n projection: {\n rules: [\n ...(options.projection?.rules ?? []),\n ...declarations.map((d) => ({\n target: \"payload\" as const,\n path: `/${enc(d.name)}`,\n exportable: d.exportable ?? true,\n classification: d.classification ?? (\"ordinary\" as const),\n })),\n ],\n },\n ...(options.limits ? { limits: options.limits } : {}),\n },\n );\n },\n };\n}\n\nfunction limitJson(\n value: Json,\n limits: Partial<TransferLimits>,\n omissions: Omission[],\n path = \"\",\n depth = 0,\n): Json {\n if (limits.maxDepth != null && depth > limits.maxDepth) {\n omissions.push({ path, reason: \"truncated\" });\n return null;\n }\n if (\n typeof value === \"string\" &&\n limits.maxStringChars != null &&\n Array.from(value).length > limits.maxStringChars\n ) {\n const chars = Array.from(value);\n omissions.push({\n path,\n reason: \"truncated\",\n count: chars.length - limits.maxStringChars,\n });\n return chars.slice(0, limits.maxStringChars).join(\"\");\n }\n if (Array.isArray(value)) {\n const count = Math.min(value.length, limits.maxArrayItems ?? value.length);\n if (count < value.length)\n omissions.push({\n path,\n reason: \"truncated\",\n count: value.length - count,\n });\n return value\n .slice(0, count)\n .map((item, index) =>\n limitJson(item, limits, omissions, `${path}/${index}`, depth + 1),\n );\n }\n if (value && typeof value === \"object\")\n return Object.fromEntries(\n Object.entries(value).map(([key, item]) => [\n key,\n limitJson(item, limits, omissions, `${path}/${enc(key)}`, depth + 1),\n ]),\n );\n return value;\n}\nfunction limitPayload(\n payload: Payload,\n limits: Partial<TransferLimits>,\n omissions: Omission[],\n): Payload {\n limitKeys.forEach((key) => assertLimit(limits[key], key));\n if (payload.kind === \"rows\") {\n const count = Math.min(\n payload.rows.length,\n limits.maxRows ?? payload.rows.length,\n );\n if (count < payload.rows.length)\n omissions.push({\n path: \"/rows\",\n reason: \"truncated\",\n count: payload.rows.length - count,\n });\n return {\n ...payload,\n rows: payload.rows\n .slice(0, count)\n .map(\n (row, index) =>\n limitJson(row, limits, omissions, `/rows/${index}`) as Record<\n string,\n Json\n >,\n ),\n };\n }\n if (payload.kind === \"json\" || payload.kind === \"registered\")\n return { ...payload, value: limitJson(payload.value, limits, omissions) };\n return {\n ...payload,\n text: limitJson(payload.text, limits, omissions) as string,\n };\n}\n/** Applies explicit preparation/source limits with exact omissions; token target is advisory. */\nexport function applyTransferLimits(\n draft: Draft,\n limits: Partial<TransferLimits>,\n): Draft {\n const omissions: Omission[] = [];\n const payload = limitPayload(draft.payload, limits, omissions);\n return omissions.length\n ? revise(draft, payload, omissions, \"Applied preparation limits\")\n : draft;\n}\n","/**\n * Wrap text in a Markdown code fence that cannot be closed by its contents.\n *\n * Fence writing is deliberately centralized here. Consumers should not grow\n * their own backtick-run detection when exporting code-shaped payloads.\n */\nexport function fencedCodeBlock(text: string, language: string): string {\n const runs = text.match(/`+/g) ?? [];\n const fence = \"`\".repeat(Math.max(3, ...runs.map((run) => run.length + 1)));\n return `${fence}${language}\\n${text}${text.endsWith(\"\\n\") ? \"\" : \"\\n\"}${fence}`;\n}\n","/**\n * `@ai-matrx/alchemy/operate/formats/rich-html` — the rich clipboard format (CONTRACT §1.9a).\n *\n * Engine: THE one Markdown→HTML engine (`@ai-matrx/print/markdown-html`, loaded lazily) with the\n * `inline` skin — styles on elements, no class dependence, because Google Docs and Word drop\n * `<style>` and classes on paste. Docs and Word are one action (\"Copy formatted\") until a\n * destination profile produces different bytes. Private images go through the host's asset\n * port. Every HTML string ends in print's allow-list.\n */\n\nimport type { MarkdownHtmlEngine } from \"@ai-matrx/print/markdown-html\";\nimport { ContentTransferError, sealTransferArtifact } from \"../core\";\nimport type { FormatEngine, FormatOptions } from \"../contract\";\nimport type { Artifact, Draft } from \"../types\";\n\nconst ENGINE = Symbol.for(\"ai-matrx.alchemy.operate.markdown-html-engine\");\n\n/** One engine per realm, created on first use (the print module itself loads only here). */\nexport async function loadMarkdownHtmlEngine(): Promise<MarkdownHtmlEngine> {\n const g = globalThis as unknown as Record<symbol, Promise<MarkdownHtmlEngine> | undefined>;\n g[ENGINE] ??= import(\"@ai-matrx/print/markdown-html\").then((m) => m.createMarkdownHtmlEngine());\n return g[ENGINE] as Promise<MarkdownHtmlEngine>;\n}\n\nfunction parseOptions(options: FormatOptions, signal: AbortSignal) {\n return {\n signal,\n ...(options.onNotice ? { onNotice: options.onNotice } : {}),\n ...(options.assets ? { assets: options.assets } : {}),\n };\n}\n\n/** Plain text from the one parse (the `plain` format for Markdown payloads). */\nexport async function markdownPlainText(markdown: string, options: FormatOptions, signal: AbortSignal): Promise<string> {\n const engine = await loadMarkdownHtmlEngine();\n return engine.plainText(await engine.parse(markdown, parseOptions(options, signal)));\n}\n\nexport const richHtmlEngine: FormatEngine = {\n async build(draft: Draft, options: FormatOptions, signal: AbortSignal): Promise<Artifact> {\n if (signal.aborted) throw new DOMException(\"Cancelled\", \"AbortError\");\n if (draft.payload.kind !== \"markdown\") {\n throw new ContentTransferError(\"unsupported-format\", \"Formatted copy needs Markdown content. Copy it as plain text instead.\");\n }\n const engine = await loadMarkdownHtmlEngine();\n const tree = await engine.parse(draft.payload.text, parseOptions(options, signal));\n if (signal.aborted) throw new DOMException(\"Cancelled\", \"AbortError\");\n const html = engine.fragment(tree, { skin: options.skin ?? \"inline\", profile: options.profile ?? \"generic\" });\n return sealTransferArtifact({\n snapshotId: draft.snapshotId,\n draftRevision: draft.revision,\n format: \"rich-html\",\n plainText: engine.plainText(tree),\n html,\n file: { filename: options.filename ?? \"export.html\", mime: \"text/html;charset=utf-8\", bytes: new TextEncoder().encode(html) },\n omissions: draft.omissions.map((o) => ({ ...o })),\n });\n },\n};\n","/**\n * `@ai-matrx/alchemy/operate/formats/document` — HTML page, PDF, DOCX and EPUB (CONTRACT §1.9).\n *\n * One tree: the HTML page is THE one Markdown→HTML engine's `document()` (print's\n * `documentToHtml`, body allow-listed); PDF (vector, selectable text), DOCX and EPUB are print's\n * `/document` writers over the same parse. Print loads only when one of these is chosen.\n * A text payload with a language exports as its fenced code block.\n */\n\nimport { ContentTransferError, sealTransferArtifact } from \"../core\";\nimport type { FormatEngine, FormatOptions } from \"../contract\";\nimport { fencedCodeBlock } from \"../fenced-code\";\nimport type { Artifact, Draft } from \"../types\";\nimport { loadMarkdownHtmlEngine } from \"./rich-html\";\n\nconst enc = new TextEncoder();\n\nfunction markdownOf(draft: Draft): string {\n const p = draft.payload;\n if (p.kind === \"markdown\") return p.text;\n if (p.kind === \"text\") {\n if (!p.language) return p.text;\n return fencedCodeBlock(p.text, p.language);\n }\n throw new ContentTransferError(\"unsupported-format\", \"A document needs text or Markdown content. Download it as JSON or CSV instead.\");\n}\n\nconst stem = (filename: string | undefined) => (filename ? filename.replace(/\\.[a-z0-9]+$/i, \"\") : undefined);\n\n/** The standalone HTML page (the `html` format for prose). */\nexport const documentHtmlEngine: FormatEngine = {\n async build(draft: Draft, options: FormatOptions, signal: AbortSignal): Promise<Artifact> {\n if (signal.aborted) throw new DOMException(\"Cancelled\", \"AbortError\");\n const engine = await loadMarkdownHtmlEngine();\n const tree = await engine.parse(markdownOf(draft), {\n signal,\n ...(options.onNotice ? { onNotice: options.onNotice } : {}),\n ...(options.assets ? { assets: options.assets } : {}),\n });\n const page = engine.document(tree);\n return sealTransferArtifact({\n snapshotId: draft.snapshotId,\n draftRevision: draft.revision,\n format: \"html\",\n plainText: engine.plainText(tree),\n html: page,\n file: { filename: options.filename ?? \"export.html\", mime: \"text/html;charset=utf-8\", bytes: enc.encode(page) },\n omissions: draft.omissions.map((o) => ({ ...o })),\n });\n },\n};\n\n/** PDF / DOCX / EPUB through print's `/document` (lazy). */\nexport function documentEngine(format: \"pdf\" | \"docx\" | \"epub\"): FormatEngine {\n return {\n async build(draft: Draft, options: FormatOptions, signal: AbortSignal): Promise<Artifact> {\n if (signal.aborted) throw new DOMException(\"Cancelled\", \"AbortError\");\n const source = markdownOf(draft);\n const { exportDocument } = await import(\"@ai-matrx/print/document\");\n const name = stem(options.filename);\n const out = await exportDocument(source, format, {\n ...(name ? { fileName: name } : {}),\n ...(options.onNotice ? { onNotice: (n) => options.onNotice?.({ code: n.code, sentence: n.message, remedy: n.remedy }) } : {}),\n });\n if (signal.aborted) throw new DOMException(\"Cancelled\", \"AbortError\");\n return sealTransferArtifact({\n snapshotId: draft.snapshotId,\n draftRevision: draft.revision,\n format,\n plainText: source,\n file: { filename: out.fileName, mime: out.mime, bytes: out.bytes },\n omissions: draft.omissions.map((o) => ({ ...o })),\n });\n },\n };\n}\n"],"mappings":"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","names":["ContentTransferError","normalizeTransferJson","ContentTransferError"]}
1
+ {"version":3,"sources":["../src/operate/core.ts","../src/operate/fenced-code.ts","../src/operate/formats/rich-html.ts","../src/operate/formats/document.ts"],"sourcesContent":["import { downloadFile } from \"@ai-matrx/kit/download\";\nimport { stringifyJson } from \"@ai-matrx/kit/json-format\";\nimport { escapeHtml } from \"@ai-matrx/kit/html-escape\";\nimport {\n ContentTransferError,\n cloneTransferJson,\n normalizeTransferJson,\n normalizeTransferJsonAtPath,\n} from \"@ai-matrx/kit/transfer-json\";\nexport {\n ContentTransferError,\n normalizeTransferJson,\n type NormalizeTransferJsonOptions,\n} from \"@ai-matrx/kit/transfer-json\";\nimport type {\n Artifact,\n CapturedAllMatching,\n CapturedSource,\n Column,\n Draft,\n ExportProjection,\n Json,\n Omission,\n Payload,\n RawSnapshot,\n Snapshot,\n Source,\n Transport,\n TransferScope,\n TransferLimits,\n TransferPreferences,\n SurfaceHandle,\n} from \"./types\";\nexport * from \"./types\";\nexport * from \"./agent-payload\";\n\n/** A realm-wide brand (Symbol.for), so a binding made by one loaded copy is recognised by another. */\nconst bound: unique symbol = Symbol.for(\"ai-matrx.alchemy.operate.bound-all-matching\");\nexport type BoundAllMatching = {\n readonly [bound]: true;\n readonly source: CapturedAllMatching;\n readonly projection: ExportProjection;\n readonly columns: readonly Column[];\n readonly limits?: TransferLimits;\n};\nconst stop = (s: AbortSignal) => {\n if (s.aborted) throw new DOMException(\"Cancelled\", \"AbortError\");\n};\nconst enc = (x: string) => x.replace(/~/g, \"~0\").replace(/\\//g, \"~1\");\nconst dec = (x: string) => x.replace(/~1/g, \"/\").replace(/~0/g, \"~\");\nconst maxSurfacePointerSegmentLength = 64;\nconst maxSurfacePointerLength = 192;\nconst truncateSurfacePointer = (value: string, maxLength: number) => {\n const characters = Array.from(value);\n return characters.length > maxLength\n ? `${characters.slice(0, maxLength - 1).join(\"\")}…`\n : value;\n};\nfunction displaySurfacePointer(path: string | undefined): string {\n if (!path) return \"/\";\n const safe = path\n .split(\"/\")\n .slice(1)\n .map((segment) => {\n const controlSafe = segment.replace(/[\\u0000-\\u001F\\u007F-\\u009F]/g, (character) =>\n `\\\\u${character.charCodeAt(0).toString(16).padStart(4, \"0\")}`,\n );\n return truncateSurfacePointer(controlSafe, maxSurfacePointerSegmentLength);\n })\n .join(\"/\");\n const pointer = `/${safe}`;\n return truncateSurfacePointer(pointer, maxSurfacePointerLength);\n}\nfunction cloneSurfaceValue(value: unknown, path: string): Json {\n try {\n return normalizeTransferJsonAtPath(value, path, {\n omitUndefinedObjectProperties: true,\n });\n } catch (error) {\n if (error instanceof ContentTransferError)\n throw new ContentTransferError(\n error.code,\n `Surface capture rejected value at ${displaySurfacePointer(error.path)}: ${error.message}`,\n error.path,\n );\n throw error;\n }\n}\nfunction deepFreeze<T>(v: T): T {\n if (ArrayBuffer.isView(v)) return v;\n if (v && typeof v === \"object\" && !Object.isFrozen(v)) {\n Object.freeze(v);\n for (const x of Object.values(v as object)) deepFreeze(x);\n }\n return v;\n}\nfunction at(v: Json, path: string): Json | undefined {\n validatePointer(path);\n if (path === \"\") return v;\n if (!path.startsWith(\"/\"))\n throw new ContentTransferError(\n \"invalid-path\",\n \"Expected RFC 6901 pointer\",\n path,\n );\n let cur: Json | undefined = v;\n for (const part of path.slice(1).split(\"/\").map(dec)) {\n if (Array.isArray(cur)) cur = cur[Number(part)];\n else if (cur && typeof cur === \"object\")\n cur = Object.hasOwn(cur, part) ? cur[part] : undefined;\n else return undefined;\n }\n return cur;\n}\nfunction validatePointer(path: string): void {\n if ((path !== \"\" && !path.startsWith(\"/\")) || /~(?![01])/u.test(path))\n throw new ContentTransferError(\n \"invalid-path\",\n \"Expected an RFC 6901 JSON pointer\",\n path,\n );\n}\n/** Column ids are identities, never an alternative data address. */\nexport function readTransferCell(\n row: Record<string, Json>,\n column: Column,\n): Json | undefined {\n return at(row, column.path);\n}\nfunction without(\n v: Json,\n paths: readonly string[],\n omit: Omission[],\n path = \"\",\n): Json | undefined {\n if (paths.includes(path)) {\n omit.push({ path, reason: \"excluded\" });\n return undefined;\n }\n if (Array.isArray(v))\n return v\n .map((x, i) => without(x, paths, omit, `${path}/${i}`))\n .filter((x): x is Json => x !== undefined);\n if (v && typeof v === \"object\") {\n const out: Record<string, Json> = Object.create(null);\n for (const [k, x] of Object.entries(v)) {\n const y = without(x, paths, omit, `${path}/${enc(k)}`);\n if (y !== undefined) out[k] = y;\n }\n return out;\n }\n return v;\n}\nfunction projectValue(\n v: Json,\n rules: ExportProjection[\"rules\"],\n target: \"payload\" | \"row\",\n omit: Omission[],\n): Json {\n rules.forEach((rule) => validatePointer(rule.path));\n return (\n without(\n cloneTransferJson(v),\n rules\n .filter(\n (r) =>\n r.target === target &&\n (!r.exportable || r.classification !== \"ordinary\"),\n )\n .map((r) => r.path),\n omit,\n ) ?? null\n );\n}\nfunction excludedColumn(column: Column): boolean {\n return (\n column.exportable === false ||\n column.classification === \"secret\" ||\n column.classification === \"credential\"\n );\n}\nfunction rowProjectionExcludesColumnSchema(\n column: Column,\n rules: ExportProjection[\"rules\"],\n): boolean {\n return rules.some(\n (rule) =>\n rule.target === \"row\" &&\n (!rule.exportable || rule.classification !== \"ordinary\") &&\n (rule.path === column.path || column.path.startsWith(`${rule.path}/`)),\n );\n}\n/** Project the schema with the same payload rules that govern column metadata. */\nfunction projectColumnSchema(\n column: Column,\n rules: ExportProjection[\"rules\"],\n omit: Omission[],\n): Column[\"schema\"] | undefined {\n if (column.schema === undefined) return undefined;\n const root = `/columns/${enc(column.id)}/schema`;\n const schemaRules = rules\n .filter((rule) => rule.target === \"payload\" && (rule.path === root || rule.path.startsWith(`${root}/`)))\n .map((rule) => ({ ...rule, path: rule.path.slice(root.length) }));\n const projected = projectValue(column.schema, schemaRules, \"payload\", omit);\n if (schemaRules.some((rule) => rule.path === \"\" && (!rule.exportable || rule.classification !== \"ordinary\")))\n return undefined;\n return projected as Column[\"schema\"];\n}\nfunction normalize(\n p: Payload,\n projection: ExportProjection,\n omit: Omission[],\n): Payload {\n if (p.kind === \"text\" || p.kind === \"markdown\") {\n if (typeof p.text !== \"string\")\n throw new ContentTransferError(\n \"unsupported-value\",\n \"Text payload must contain a string\",\n \"/text\",\n );\n const value = projectValue(p.text, projection.rules, \"payload\", omit);\n const text = typeof value === \"string\" ? value : \"\";\n // A code block keeps its language (downloads take its extension; Markdown fences it).\n return p.kind === \"text\" && typeof p.language === \"string\" && p.language.trim()\n ? { kind: \"text\", text, language: p.language.trim() }\n : { kind: p.kind, text };\n }\n if (p.kind === \"json\")\n return {\n kind: \"json\",\n value: projectValue(p.value, projection.rules, \"payload\", omit),\n };\n if (p.kind === \"registered\")\n return {\n kind: \"registered\",\n format: p.format,\n value: projectValue(p.value, projection.rules, \"payload\", omit),\n };\n const rowsPayload = p as Extract<Payload, { kind: \"rows\" }>;\n if (p.kind !== \"rows\")\n throw new ContentTransferError(\n \"invalid-payload\",\n \"Unsupported payload kind\",\n );\n const sourceColumns = rowsPayload.columns.map((column) => ({ ...column }));\n const columns = sourceColumns\n .filter((column) => !excludedColumn(column))\n .map((column) => {\n // Do not spread source schema back after an explicit schema or row exclusion.\n const { schema: _sourceSchema, ...withoutSchema } = column;\n const schema = rowProjectionExcludesColumnSchema(column, projection.rules)\n ? undefined\n : projectColumnSchema(column, projection.rules, omit);\n return {\n ...withoutSchema,\n ...(schema === undefined ? {} : { schema }),\n };\n });\n resolveTransferRegistry(columns);\n columns.forEach((column) => validatePointer(column.path));\n const rules = [\n ...projection.rules,\n ...sourceColumns\n .filter(excludedColumn)\n .map((c) => ({\n target: \"row\" as const,\n path: c.path,\n exportable: c.exportable ?? true,\n classification: c.classification ?? (\"ordinary\" as const),\n })),\n ];\n return {\n kind: \"rows\",\n columns,\n rows: rowsPayload.rows.map((row, index) => {\n if (!row || typeof row !== \"object\" || Array.isArray(row))\n throw new ContentTransferError(\n \"invalid-row\",\n \"A table row must be a JSON object\",\n `/rows/${index}`,\n );\n const projected = projectValue(row, rules, \"row\", omit);\n return projected === null ? {} : (projected as Record<string, Json>);\n }),\n };\n}\nexport function projectSnapshot(\n raw: RawSnapshot,\n projection: ExportProjection,\n): Snapshot {\n const omissions = raw.omissions.map((o) => ({ ...o }));\n const normalized = normalize(raw.payload, projection, omissions);\n const payload = raw.limits\n ? limitPayload(normalized, raw.limits, omissions)\n : normalized;\n const included =\n payload.kind === \"rows\" ? payload.rows.length : raw.coverage.included;\n return deepFreeze({\n ...raw,\n payload,\n coverage: {\n ...raw.coverage,\n included,\n ...(included < raw.coverage.included\n ? { status: \"partial\" as const, reason: \"Source export limit applied\" }\n : {}),\n },\n omissions,\n sections: raw.sections\n .filter(\n (section) =>\n !projection.rules.some(\n (rule) =>\n rule.target === \"payload\" &&\n (!rule.exportable || rule.classification !== \"ordinary\") &&\n (section.path === rule.path ||\n section.path.startsWith(`${rule.path}/`)),\n ),\n )\n .map((x) => ({ ...x })),\n ...(raw.limits ? { limits: { ...raw.limits } } : {}),\n } as Snapshot);\n}\nconst uid = () =>\n globalThis.crypto?.randomUUID?.() ?? `capture-${Date.now()}-${Math.random()}`;\nexport function directSource(\n payload: Payload,\n options: Partial<\n Pick<\n RawSnapshot,\n | \"id\"\n | \"sourceId\"\n | \"revision\"\n | \"label\"\n | \"coverage\"\n | \"unsavedChanges\"\n | \"sections\"\n | \"limits\"\n > & { projection: ExportProjection }\n > = {},\n): CapturedSource {\n const id = options.id ?? uid();\n return {\n projection: options.projection ?? { rules: [] },\n snapshot: {\n id,\n sourceId: options.sourceId ?? id,\n revision: options.revision ?? uid(),\n capturedAt: new Date().toISOString(),\n label: options.label ?? \"Content\",\n payload,\n coverage: options.coverage ?? {\n status: \"complete\",\n included: payload.kind === \"rows\" ? payload.rows.length : 1,\n total: payload.kind === \"rows\" ? payload.rows.length : 1,\n },\n unsavedChanges: options.unsavedChanges ?? false,\n omissions: [],\n sections: options.sections ?? [],\n ...(options.limits ? { limits: { ...options.limits } } : {}),\n },\n };\n}\nexport async function capture(\n source: Source | Payload,\n signal: AbortSignal,\n scope: TransferScope = \"target\",\n): Promise<{ snapshot: Snapshot; allMatching?: BoundAllMatching }> {\n stop(signal);\n const captured =\n \"kind\" in source\n ? directSource(source)\n : await source.capture({ scope, signal });\n stop(signal);\n const all = captured.allMatching;\n if (\n all &&\n (all.query.sourceId !== captured.snapshot.sourceId ||\n all.query.revision !== captured.snapshot.revision ||\n all.query.order.length === 0)\n )\n throw new ContentTransferError(\n \"query-mismatch\",\n \"All-matching query must be captured with stable ordering\",\n );\n const snapshot = projectSnapshot(captured.snapshot, captured.projection);\n if (!all) return { snapshot };\n return {\n snapshot,\n allMatching: deepFreeze({\n [bound]: true as const,\n source: {\n ...all,\n query: {\n ...all.query,\n query: cloneTransferJson(all.query.query),\n order: all.query.order.map((x) => ({ ...x })),\n },\n },\n projection: { rules: captured.projection.rules.map((x) => ({ ...x })) },\n // Page rows must be normalized against the original declaration so excluded\n // column values are removed before stable IDs or any caller can observe them.\n columns:\n captured.snapshot.payload.kind === \"rows\"\n ? captured.snapshot.payload.columns.map((column) => ({ ...column }))\n : [],\n ...(snapshot.limits ? { limits: { ...snapshot.limits } } : {}),\n }),\n };\n}\nexport function createDraft(s: Snapshot): Draft {\n return deepFreeze({\n snapshotId: s.id,\n sourceId: s.sourceId,\n sourceRevision: s.revision,\n revision: 0,\n payload: normalize(s.payload, { rules: [] }, []),\n omissions: [...s.omissions],\n manuallyEdited: false,\n provenance: [{ kind: \"original\", description: \"Captured source snapshot\" }],\n });\n}\nfunction revise(\n d: Draft,\n p: Payload,\n o: Omission[],\n description: string,\n manual = d.manuallyEdited,\n kind: Draft[\"provenance\"][number][\"kind\"] = \"deterministic\",\n baseOmissions: readonly Omission[] = d.omissions,\n): Draft {\n const { history: _history, ...previous } = d;\n return deepFreeze({\n ...d,\n revision: d.revision + 1,\n payload: normalize(p, { rules: [] }, []),\n omissions: [...baseOmissions, ...o],\n manuallyEdited: manual,\n provenance: [...d.provenance, { kind, description }],\n history: [...(d.history ?? []), previous],\n });\n}\nexport const editDraft = (d: Draft, p: Payload) =>\n revise(d, p, [], \"Manual edit\", true, \"manual\");\n/**\n * A deterministic revision of `d` (the structure trees, PP-06/07): same snapshot, next revision,\n * the given omissions added. `baseOmissions` replaces the draft's own when a step re-projects from\n * the snapshot (a section selection), exactly as `applySectionSelection` does.\n */\nexport const deriveDraft = (\n d: Draft,\n p: Payload,\n o: Omission[],\n description: string,\n baseOmissions: readonly Omission[] = d.omissions,\n) => revise(d, p, o, description, d.manuallyEdited, \"deterministic\", baseOmissions);\nexport const resetDraft = (s: Snapshot) => createDraft(s);\nexport const sourceChanged = (d: Draft, s: Snapshot) =>\n d.sourceId !== s.sourceId || d.sourceRevision !== s.revision;\nexport function undoDraft(d: Draft): Draft {\n const previous = d.history?.at(-1);\n if (!previous) return d;\n return deepFreeze({\n ...previous,\n revision: d.revision + 1,\n history: d.history!.slice(0, -1),\n });\n}\n/** Keep and regenerate are explicit choices; observing fresh data never overwrites edits. */\nexport function refreshDraft(\n draft: Draft,\n snapshot: Snapshot,\n choice: \"keep\" | \"regenerate\",\n): Draft {\n return choice === \"keep\" ? draft : createDraft(snapshot);\n}\nexport function applySectionSelection(\n d: Draft,\n s: Snapshot,\n ids: readonly string[],\n): Draft {\n if (\n d.snapshotId !== s.id ||\n d.sourceId !== s.sourceId ||\n d.sourceRevision !== s.revision\n )\n throw new ContentTransferError(\n \"snapshot-mismatch\",\n \"Section selection requires the draft's captured source snapshot\",\n );\n const selected = new Set(ids),\n omit: Omission[] = [],\n paths = s.sections\n .filter((x) => !selected.has(x.id))\n .map((x) => {\n omit.push({ path: x.path, reason: \"excluded\" });\n return x.path;\n }),\n // Selection is a replacement projection from the snapshot, so only the\n // snapshot's omissions remain authoritative. Retaining draft omissions\n // would preserve stale transforms after the snapshot payload is rebuilt.\n baseOmissions = s.omissions;\n if (s.payload.kind === \"json\")\n return revise(\n d,\n { kind: \"json\", value: without(s.payload.value, paths, []) ?? null },\n omit,\n \"Selected sections\",\n d.manuallyEdited,\n \"deterministic\",\n baseOmissions,\n );\n if (s.payload.kind === \"registered\")\n return revise(\n d,\n {\n kind: \"registered\",\n format: s.payload.format,\n value: without(s.payload.value, paths, []) ?? null,\n },\n omit,\n \"Selected sections\",\n d.manuallyEdited,\n \"deterministic\",\n baseOmissions,\n );\n if (s.payload.kind === \"markdown\" || s.payload.kind === \"text\") {\n // A prose section is a character range of the text: `/text/<start>-<end>`.\n const text = s.payload.text;\n const ranges = paths\n .map((path) => {\n const m = /^\\/text\\/(\\d+)-(\\d+)$/.exec(path);\n const start = m ? Number(m[1]) : NaN,\n end = m ? Number(m[2]) : NaN;\n if (!m || start > end || end > text.length)\n throw new ContentTransferError(\n \"invalid-section\",\n `Section \"${path}\" is not a range of this text. Declare a prose section as /text/<start>-<end> within the text.`,\n );\n return [start, end] as const;\n })\n .sort((a, b) => b[0] - a[0]);\n let kept = text;\n for (const [start, end] of ranges) kept = kept.slice(0, start) + kept.slice(end);\n return revise(\n d,\n { ...s.payload, text: kept },\n omit,\n \"Selected sections\",\n d.manuallyEdited,\n \"deterministic\",\n baseOmissions,\n );\n }\n return revise(\n d,\n s.payload,\n omit,\n \"Selected sections\",\n d.manuallyEdited,\n \"deterministic\",\n baseOmissions,\n );\n}\nexport function transformRows(\n d: Draft,\n o: {\n search?: string;\n sort?: { column: string; direction: \"asc\" | \"desc\" };\n filters?: Readonly<Record<string, string>>;\n /** Positions in this call's input draft rows; unlike ids, no guessed key is used. */\n selectedIndices?: readonly number[];\n selectedIds?: readonly string[];\n idColumn?: string;\n visibleColumns?: readonly string[];\n structuredProjection?: \"full\" | \"visible\";\n top?: number | null;\n },\n): Draft {\n if (d.payload.kind !== \"rows\") return d;\n // Validate before deciding whether truncation is necessary. A negative/NaN\n // top is an invalid request even for an empty result set.\n if (o.top != null) assertLimit(o.top, \"maxRows\");\n const cols = o.visibleColumns\n ? o.visibleColumns\n .map((id) =>\n d.payload.kind === \"rows\"\n ? d.payload.columns.find((c) => c.id === id)\n : undefined,\n )\n .filter((c): c is Column => Boolean(c))\n : d.payload.columns.filter((c) => c.visible);\n const get = readTransferCell;\n let rows = [...d.payload.rows],\n omit: Omission[] = [];\n const before = rows.length;\n if (o.selectedIndices !== undefined) {\n const selected = new Set<number>();\n for (const index of o.selectedIndices) {\n if (!Number.isSafeInteger(index) || index < 0 || index >= d.payload.rows.length)\n throw new ContentTransferError(\n \"invalid-selection-index\",\n \"Selected row indexes must be non-negative positions in the input draft rows\",\n );\n selected.add(index);\n }\n rows = d.payload.rows.filter((_, index) => selected.has(index));\n }\n if (o.selectedIds && o.idColumn) {\n const set = new Set(o.selectedIds);\n const idColumn = d.payload.columns.find((c) => c.id === o.idColumn);\n rows = rows.filter((r) =>\n set.has(String(idColumn ? get(r, idColumn) : r[o.idColumn!])),\n );\n }\n if (o.search) {\n const q = o.search.toLowerCase();\n rows = rows.filter((r) =>\n cols.some((c) =>\n String(get(r, c) ?? \"\")\n .toLowerCase()\n .includes(q),\n ),\n );\n }\n for (const [id, q] of Object.entries(o.filters ?? {})) {\n const c = cols.find((x) => x.id === id);\n if (c)\n rows = rows.filter((r) =>\n String(get(r, c) ?? \"\")\n .toLowerCase()\n .includes(q.toLowerCase()),\n );\n }\n if (rows.length !== before)\n omit.push({\n path: \"/rows\",\n reason: \"filtered\",\n count: before - rows.length,\n });\n if (o.sort) {\n const c = cols.find((x) => x.id === o.sort!.column);\n if (c)\n rows.sort((a, b) => {\n const left = get(a, c);\n const right = get(b, c);\n const compare =\n typeof left === \"number\" && typeof right === \"number\"\n ? left - right\n : String(left ?? \"\").localeCompare(String(right ?? \"\"));\n return compare * (o.sort!.direction === \"asc\" ? 1 : -1);\n });\n }\n if (o.top != null && rows.length > o.top) {\n omit.push({\n path: \"/rows\",\n reason: \"truncated\",\n count: rows.length - o.top,\n });\n rows = rows.slice(0, o.top);\n }\n if (o.structuredProjection === \"visible\") {\n rows = rows.map((r) =>\n Object.fromEntries(cols.map((c) => [c.id, get(r, c) ?? null])),\n );\n const excludedColumns = d.payload.columns.length - cols.length;\n if (excludedColumns > 0)\n omit.push({\n path: \"/columns\",\n reason: \"excluded\",\n count: excludedColumns,\n });\n }\n return revise(\n d,\n {\n kind: \"rows\",\n rows,\n columns: cols.map((c) => ({\n ...c,\n visible: true,\n ...(o.structuredProjection === \"visible\"\n ? { path: `/${enc(c.id)}` }\n : {}),\n })),\n },\n omit,\n \"Prepared row subset\",\n );\n}\nexport function reduceJson(\n draft: Draft,\n options: {\n exclude?: readonly string[];\n maxDepth?: number | null;\n maxStringChars?: number | null;\n maxArrayItems?: number | null;\n },\n): Draft {\n if (draft.payload.kind !== \"json\" && draft.payload.kind !== \"registered\")\n return draft;\n const omissions: Omission[] = [];\n options.exclude?.forEach(validatePointer);\n const filtered =\n without(cloneTransferJson(draft.payload.value), options.exclude ?? [], omissions) ??\n null;\n const payload = limitPayload(\n { ...draft.payload, value: filtered },\n options,\n omissions,\n );\n return revise(draft, payload, omissions, \"Reduced JSON\");\n}\nexport async function collectAllMatching(\n c: BoundAllMatching,\n signal: AbortSignal,\n): Promise<{\n rows: Record<string, Json>[];\n coverage: RawSnapshot[\"coverage\"];\n omissions: Omission[];\n error?: ContentTransferError;\n}> {\n const rows: Record<string, Json>[] = [],\n omissions: Omission[] = [],\n ids = new Set<string>(),\n cursors = new Set<string>();\n let cursor: string | null = null,\n token: string | null | undefined,\n last: RawSnapshot[\"coverage\"] = {\n status: \"unknown\",\n included: 0,\n total: null,\n };\n let certifiedTotal: number | null | undefined;\n const maxRows = c.limits?.maxRows;\n if (maxRows != null) assertLimit(maxRows, \"maxRows\");\n const finishAtLimit = (total: number | null, knownOmitted?: number) => ({\n rows: deepFreeze(rows),\n omissions: deepFreeze([\n ...omissions,\n {\n path: \"/rows\",\n reason: \"truncated\" as const,\n ...(knownOmitted === undefined || knownOmitted <= 0\n ? {}\n : { count: knownOmitted }),\n },\n ]),\n coverage: deepFreeze({\n status: \"partial\" as const,\n included: rows.length,\n total,\n reason: \"Source export limit applied\",\n }),\n });\n try {\n // A zero source cap is still a real cap: do not request a page merely to\n // discover data that the caller is not allowed to receive.\n if (maxRows === 0) return finishAtLimit(null);\n do {\n stop(signal);\n const page = await c.source.page({\n query: c.source.query,\n cursor,\n consistencyToken: token ?? null,\n signal,\n });\n stop(signal);\n if (token === undefined) token = page.consistencyToken;\n else if (page.consistencyToken !== token)\n throw new ContentTransferError(\n \"consistency-token-changed\",\n \"All-matching export changed during collection\",\n );\n if (page.coverage.included !== page.rows.length)\n throw new ContentTransferError(\n \"invalid-page-coverage\",\n \"Page count does not match returned rows\",\n );\n if (\n page.coverage.total !== null &&\n (!Number.isSafeInteger(page.coverage.total) || page.coverage.total < 0)\n )\n throw new ContentTransferError(\n \"invalid-page-coverage\",\n \"Page total must be a non-negative integer or null\",\n );\n if (certifiedTotal === undefined) certifiedTotal = page.coverage.total;\n else if (page.coverage.total !== certifiedTotal)\n throw new ContentTransferError(\n \"inconsistent-total\",\n \"All-matching source changed its reported total during collection\",\n );\n const projected = (\n normalize(\n { kind: \"rows\", rows: page.rows, columns: [...c.columns] },\n c.projection,\n omissions,\n ) as Extract<Payload, { kind: \"rows\" }>\n ).rows;\n let uniqueBeyondLimit = 0;\n for (const row of projected) {\n // The callback is part of the transfer boundary, so it receives only\n // the projected row. Sources requiring identity must declare an\n // exportable opaque ID; raw credential/non-exportable fields never\n // cross this seam merely for de-duplication.\n const id = c.source.getRowId(row);\n if (!id)\n throw new ContentTransferError(\n \"invalid-row-id\",\n \"All-matching source returned empty projected ID\",\n );\n if (!ids.has(id)) {\n ids.add(id);\n if (maxRows == null || rows.length < maxRows)\n rows.push(\n (\n limitPayload(\n { kind: \"rows\", rows: [row], columns: [...c.columns] },\n { ...c.limits, maxRows: null },\n omissions,\n ) as Extract<Payload, { kind: \"rows\" }>\n ).rows[0]!,\n );\n else uniqueBeyondLimit += 1;\n } else omissions.push({ path: `/rows/${enc(id)}`, reason: \"duplicate\" });\n }\n // Check after de-duplication and before the source-cap return: ids also\n // includes unique rows that the cap intentionally did not retain.\n if (page.coverage.total !== null && page.coverage.total < ids.size)\n throw new ContentTransferError(\n \"invalid-page-coverage\",\n \"Page total is smaller than the accumulated unique row count\",\n );\n last = page.coverage;\n if (\n maxRows != null &&\n rows.length >= maxRows &&\n (uniqueBeyondLimit > 0 || page.nextCursor !== null)\n )\n return finishAtLimit(\n page.coverage.total,\n page.nextCursor === null\n ? uniqueBeyondLimit\n : page.coverage.total != null && page.coverage.total > rows.length\n ? page.coverage.total - rows.length\n : undefined,\n );\n cursor = page.nextCursor;\n if (cursor !== null && (!cursor || cursors.has(cursor)))\n throw new ContentTransferError(\n \"non-advancing-cursor\",\n \"All-matching source repeated a cursor\",\n );\n if (cursor) cursors.add(cursor);\n } while (cursor !== null);\n const complete =\n c.source.stableSnapshot === true &&\n last.status === \"complete\" &&\n last.total === rows.length &&\n token !== undefined;\n return {\n rows: deepFreeze(rows),\n omissions: deepFreeze(omissions),\n coverage: deepFreeze(\n complete\n ? { ...last, included: rows.length }\n : {\n ...last,\n status: \"partial\",\n included: rows.length,\n reason:\n last.reason ??\n \"Source did not certify a stable complete snapshot\",\n },\n ),\n };\n } catch (e) {\n const error =\n e instanceof ContentTransferError\n ? e\n : new ContentTransferError(\n signal.aborted ? \"cancelled\" : \"page-failed\",\n e instanceof Error ? e.message : \"Page collection failed\",\n );\n return {\n rows: deepFreeze(rows),\n omissions: deepFreeze(omissions),\n coverage: deepFreeze({\n status: \"partial\",\n included: rows.length,\n total: last.total,\n reason: error.message,\n }),\n error,\n };\n }\n}\n// An EMPTY cell is empty text. `null` is how a row carries \"nothing here\"\n// (hosts normalise a missing cell to JSON null so the row keeps its shape), and\n// until 2026-09-21 it serialized as the word \"null\" in every plain / CSV / TSV\n// / Markdown copy — six \"null\"s across a copied row of a data table.\nconst textValue = (value: Json | undefined): string =>\n value === undefined || value === null\n ? \"\"\n : typeof value === \"string\"\n ? value\n : stringifyJson(value, { style: \"compact\" });\n\n/** Preserve artifact-byte identity even if a consumer mutates a returned view. */\nexport function sealTransferArtifact(artifact: Artifact): Artifact {\n if (!artifact.file)\n return deepFreeze({\n ...artifact,\n omissions: artifact.omissions.map((o) => ({ ...o })),\n });\n const bytes = artifact.file.bytes.slice();\n const file = {\n filename: artifact.file.filename,\n mime: artifact.file.mime,\n get bytes() {\n return bytes.slice();\n },\n };\n return deepFreeze({\n ...artifact,\n file,\n omissions: artifact.omissions.map((o) => ({ ...o })),\n });\n}\n\n/** The columns a text serialization writes: visible, exportable, never secret or credential. */\nexport function exportedColumns(columns: readonly Column[]): Column[] {\n return columns.filter(\n (c) =>\n c.visible &&\n c.exportable !== false &&\n c.classification !== \"secret\" &&\n c.classification !== \"credential\",\n );\n}\n\nexport function serialize(\n draft: Draft,\n format:\n \"plain\" | \"markdown\" | \"json\" | \"compact-json\" | \"csv\" | \"tsv\" | \"html\",\n options: { spreadsheetSafe?: boolean; filename?: string } = {},\n): Artifact {\n const payload = draft.payload;\n const omissions = draft.omissions.map((o) => ({ ...o }));\n let plainText = \"\",\n html: string | undefined;\n let mime = \"text/plain;charset=utf-8\",\n extension = \"txt\";\n const columnCell = (row: Record<string, Json>, column: Column) =>\n textValue(readTransferCell(row, column));\n if ((format === \"csv\" || format === \"tsv\") && payload.kind !== \"rows\") {\n throw new ContentTransferError(\n \"unsupported-format\",\n `${format.toUpperCase()} requires tabular data`,\n );\n }\n if (format === \"json\" || format === \"compact-json\") {\n const value =\n payload.kind === \"rows\"\n ? payload.rows\n : payload.kind === \"text\" || payload.kind === \"markdown\"\n ? payload.text\n : payload.value;\n plainText = stringifyJson(value, {\n style: format === \"json\" ? \"pretty\" : \"compact\",\n });\n mime = \"application/json;charset=utf-8\";\n extension = \"json\";\n } else if (payload.kind === \"rows\") {\n const columns = exportedColumns(payload.columns);\n if (format === \"markdown\") {\n const escapeCell = (value: string) =>\n escapeHtml(value)\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/\\|/g, \"\\\\|\")\n .replace(/\\r\\n|\\r|\\n/g, \"<br>\");\n plainText = [\n `| ${columns.map((c) => escapeCell(c.label)).join(\" | \")} |`,\n `| ${columns.map(() => \"---\").join(\" | \")} |`,\n ...payload.rows.map(\n (row) =>\n `| ${columns.map((c) => escapeCell(columnCell(row, c))).join(\" | \")} |`,\n ),\n ].join(\"\\n\");\n mime = \"text/markdown;charset=utf-8\";\n extension = \"md\";\n } else {\n const delimiter = format === \"csv\" ? \",\" : \"\\t\";\n const safe = options.spreadsheetSafe ?? format === \"tsv\";\n const encode = (value: string, path: string, isString = true) => {\n let text = value;\n if (safe && isString && /^[\\t\\r\\n ]*[=+\\-@]/.test(text)) {\n text = `'${text}`;\n omissions.push({ path, reason: \"escaped\", count: 1 });\n }\n return text.includes(delimiter) || /[\"\\n\\r]/.test(text)\n ? `\"${text.replace(/\"/g, '\"\"')}\"`\n : text;\n };\n plainText = [\n columns\n .map((c) => encode(c.label, `/columns/${enc(c.id)}`))\n .join(delimiter),\n ...payload.rows.map((row, index) =>\n columns\n .map((c) =>\n encode(\n columnCell(row, c),\n `/rows/${index}${c.path}`,\n typeof readTransferCell(row, c) === \"string\",\n ),\n )\n .join(delimiter),\n ),\n ].join(\"\\n\");\n if (format === \"csv\") {\n mime = \"text/csv;charset=utf-8\";\n extension = \"csv\";\n }\n if (format === \"tsv\") {\n mime = \"text/tab-separated-values;charset=utf-8\";\n extension = \"tsv\";\n }\n }\n if (format === \"html\")\n html = `<table><thead><tr>${columns.map((c) => `<th>${escapeHtml(c.label)}</th>`).join(\"\")}</tr></thead><tbody>${payload.rows.map((row) => `<tr>${columns.map((c) => `<td>${escapeHtml(columnCell(row, c)).replace(/\\r\\n|\\r|\\n/g, \"<br>\")}</td>`).join(\"\")}</tr>`).join(\"\")}</tbody></table>`;\n } else {\n plainText =\n payload.kind === \"text\" || payload.kind === \"markdown\"\n ? payload.text\n : stringifyJson(payload.value, { style: \"pretty\" });\n if (format === \"markdown\") {\n mime = \"text/markdown;charset=utf-8\";\n extension = \"md\";\n }\n if (format === \"html\") html = `<pre>${escapeHtml(plainText)}</pre>`;\n }\n if (format === \"html\") {\n mime = \"text/html;charset=utf-8\";\n extension = \"html\";\n }\n return sealTransferArtifact({\n snapshotId: draft.snapshotId,\n draftRevision: draft.revision,\n format,\n plainText,\n ...(html === undefined ? {} : { html }),\n file: {\n filename: options.filename ?? `export.${extension}`,\n mime,\n bytes: new TextEncoder().encode(\n format === \"html\" ? (html ?? plainText) : plainText,\n ),\n },\n omissions,\n });\n}\nexport function createBrowserTransport(): Transport {\n return {\n async copy(a, signal) {\n const cancelled = () =>\n signal.aborted ? { status: \"cancelled\" as const } : undefined;\n try {\n if (cancelled()) return cancelled()!;\n const c = globalThis.navigator?.clipboard;\n if (!c?.write && !c?.writeText)\n return {\n status: \"error\",\n code: \"clipboard-unavailable\",\n message: \"Clipboard writing is unavailable\",\n retryable: false,\n };\n if (\n a.html &&\n c.write &&\n typeof globalThis.ClipboardItem === \"function\"\n ) {\n try {\n await c.write([\n new ClipboardItem({\n \"text/plain\": new Blob([a.plainText], { type: \"text/plain\" }),\n \"text/html\": new Blob([a.html], { type: \"text/html\" }),\n }),\n ]);\n if (cancelled()) return cancelled()!;\n return {\n status: \"success\",\n delivered: \"clipboard\",\n mimeTypes: [\"text/plain\", \"text/html\"],\n };\n } catch {\n if (cancelled()) return cancelled()!;\n }\n }\n if (c.writeText) {\n await c.writeText(a.plainText);\n if (cancelled()) return cancelled()!;\n return a.html\n ? {\n status: \"degraded\",\n delivered: \"plain-text\",\n reason: \"Rich clipboard write was unavailable or rejected\",\n }\n : {\n status: \"success\",\n delivered: \"clipboard\",\n mimeTypes: [\"text/plain\"],\n };\n }\n return {\n status: \"error\",\n code: \"clipboard-rejected\",\n message: \"Clipboard rejected the write\",\n retryable: true,\n };\n } catch (e) {\n return (\n cancelled() ?? {\n status: \"error\",\n code: \"clipboard-rejected\",\n message:\n e instanceof Error ? e.message : \"Clipboard rejected the write\",\n retryable: true,\n }\n );\n }\n },\n async download(a, signal) {\n if (signal.aborted) return { status: \"cancelled\" };\n if (!a.file || typeof document === \"undefined\" || !URL?.createObjectURL)\n return {\n status: \"error\",\n code: \"download-unavailable\",\n message: \"Download is unavailable\",\n retryable: false,\n };\n try {\n // THE download door (kit): the object URL outlives the click by a beat, so Safari keeps the save.\n downloadFile(a.file.filename, a.file.bytes.slice().buffer as ArrayBuffer, a.file.mime);\n return signal.aborted\n ? { status: \"cancelled\" }\n : {\n status: \"success\",\n delivered: \"download-started\",\n mimeTypes: [a.file.mime],\n };\n } catch (e) {\n return signal.aborted\n ? { status: \"cancelled\" }\n : {\n status: \"error\",\n code: \"download-failed\",\n message: e instanceof Error ? e.message : \"Download failed\",\n retryable: true,\n };\n }\n },\n };\n}\n\nfunction assertLimit(value: number | null | undefined, name: string): void {\n if (value != null && (!Number.isSafeInteger(value) || value < 0)) {\n throw new ContentTransferError(\n \"invalid-limit\",\n `${name} must be a non-negative safe integer or null`,\n );\n }\n}\nexport type TransferPreferenceOverrides = Partial<\n Omit<TransferPreferences, \"limits\">\n> & { limits?: Partial<TransferLimits> };\nconst limitKeys = [\n \"maxRows\",\n \"maxStringChars\",\n \"maxDepth\",\n \"maxArrayItems\",\n \"targetTokens\",\n] as const;\n/** Restrictions intersect: neither a menu nor a provider can relax a source limit. */\nexport function resolveTransferPreferences(\n provider: TransferPreferenceOverrides = {},\n menu: TransferPreferenceOverrides = {},\n sourceLimits: Partial<TransferLimits> = {},\n): TransferPreferences {\n const limits: TransferLimits = {\n maxRows: null,\n maxStringChars: null,\n maxDepth: null,\n maxArrayItems: null,\n targetTokens: null,\n };\n for (const key of limitKeys) {\n const candidates = [\n provider.limits?.[key],\n menu.limits?.[key],\n sourceLimits[key],\n ];\n candidates.forEach((value) => assertLimit(value, key));\n const finite = candidates.filter((value): value is number => value != null);\n limits[key] = finite.length ? Math.min(...finite) : null;\n }\n return deepFreeze({\n initialFormat: menu.initialFormat ?? provider.initialFormat ?? \"plain\",\n initialPreset: menu.initialPreset ?? provider.initialPreset ?? \"full\",\n initialEnvelope: menu.initialEnvelope ?? provider.initialEnvelope ?? \"ai\",\n limits,\n });\n}\n/**\n * True for the objects React uses to represent a component or an element —\n * `forwardRef`, `memo`, `lazy`, portals, elements. They are all tagged with the\n * `$$typeof` symbol, which is exactly the test React uses itself.\n */\nfunction isReactElementType(value: object): boolean {\n const tag = (value as { $$typeof?: unknown }).$$typeof;\n return typeof tag === \"symbol\";\n}\n\nfunction cloneTransferRegistration(\n value: unknown,\n path = \"registration\",\n seen = new WeakSet<object>(),\n): unknown {\n if (value === null || (typeof value !== \"object\" && typeof value !== \"function\"))\n return value;\n // Registered behavior remains callable by identity. Freezing a caller-owned\n // function would mutate it and its attached state.\n if (typeof value === \"function\") return value;\n // A React component or element is registered BEHAVIOUR too — it is rendered,\n // never inspected — and `React.forwardRef` / `React.memo` produce OBJECTS\n // rather than functions. React's development build defines their\n // `displayName` as an ACCESSOR, so walking into one threw \"cannot use\n // accessors\" at `registration/icon/displayName` and took EVERY dashboard page\n // that renders a transfer menu down behind its error boundary (2026-09-14;\n // every AI variant the admin dashboard registers carries a lucide `icon`).\n // Recognised the way React itself recognises them.\n if (isReactElementType(value)) return value;\n if (seen.has(value))\n throw new ContentTransferError(\n \"unsupported-registration-metadata\",\n \"Circular transfer registration metadata is unsupported\",\n path,\n );\n seen.add(value);\n if (Array.isArray(value)) {\n const copy = value.map((item, index) =>\n cloneTransferRegistration(item, `${path}/${index}`, seen),\n );\n seen.delete(value);\n return Object.freeze(copy);\n }\n const prototype = Object.getPrototypeOf(value);\n if (prototype !== Object.prototype && prototype !== null)\n throw new ContentTransferError(\n \"unsupported-registration-metadata\",\n \"Transfer registration metadata must use plain objects and arrays\",\n path,\n );\n const copy = Object.create(prototype) as Record<PropertyKey, unknown>;\n for (const key of Reflect.ownKeys(value)) {\n const descriptor = Object.getOwnPropertyDescriptor(value, key)!;\n if (!(\"value\" in descriptor))\n throw new ContentTransferError(\n \"unsupported-registration-metadata\",\n \"Transfer registration metadata cannot use accessors\",\n `${path}/${String(key)}`,\n );\n Object.defineProperty(copy, key, {\n value: cloneTransferRegistration(\n descriptor.value,\n `${path}/${String(key)}`,\n seen,\n ),\n enumerable: descriptor.enumerable === true,\n writable: false,\n configurable: false,\n });\n }\n seen.delete(value);\n return Object.freeze(copy);\n}\n\n/** One stable id per registered format or action; ambiguous overrides fail loudly. */\nexport function resolveTransferRegistry<T extends { id: string }>(\n ...groups: readonly (readonly T[])[]\n): readonly T[] {\n const ids = new Set<string>();\n const result: T[] = [];\n for (const group of groups)\n for (const item of group) {\n const copy = cloneTransferRegistration(item) as T;\n if (!copy.id.trim() || ids.has(copy.id))\n throw new ContentTransferError(\n \"duplicate-registration\",\n `Transfer id ${JSON.stringify(copy.id)} is empty or registered more than once`,\n );\n ids.add(copy.id);\n result.push(copy);\n }\n return Object.freeze(result);\n}\n\nexport type TransferSurfaceDeclaration = {\n surfaceName: string;\n label?: string;\n values: readonly {\n name: string;\n label: string;\n exportable?: boolean;\n classification?: \"ordinary\" | \"secret\" | \"credential\";\n includedByDefault?: boolean;\n }[];\n};\n/** A structural adapter to the actual mounted surface, never a global name lookup. */\nexport function createSurfaceTransferHandle(options: {\n instanceId: string;\n manifest: TransferSurfaceDeclaration;\n isMounted(): boolean;\n getScope(): Record<string, unknown> | Promise<Record<string, unknown>>;\n getRevision?(): string;\n unsavedChanges?(): boolean;\n projection?: ExportProjection;\n limits?: TransferLimits;\n}): SurfaceHandle {\n const declarations = resolveTransferRegistry(\n options.manifest.values.map((value) => ({ ...value, id: value.name })),\n );\n return {\n instanceId: options.instanceId,\n surfaceName: options.manifest.surfaceName,\n isMounted: options.isMounted,\n async capture(signal) {\n stop(signal);\n if (!options.isMounted())\n throw new ContentTransferError(\n \"surface-unmounted\",\n \"The captured surface is no longer mounted\",\n );\n const revisionBefore = options.getRevision?.();\n const scope = await options.getScope();\n stop(signal);\n if (!options.isMounted())\n throw new ContentTransferError(\n \"surface-unmounted\",\n \"The surface closed during capture\",\n );\n if (\n revisionBefore !== undefined &&\n options.getRevision?.() !== revisionBefore\n )\n throw new ContentTransferError(\n \"surface-changed\",\n \"The surface changed during capture; capture again\",\n );\n const value: Record<string, Json> = {};\n for (const declaration of declarations) {\n if (\n Object.hasOwn(scope, declaration.name) &&\n scope[declaration.name] !== undefined\n ) {\n Object.defineProperty(value, declaration.name, {\n value: cloneSurfaceValue(\n scope[declaration.name],\n `/${enc(declaration.name)}`,\n ),\n enumerable: true,\n writable: true,\n configurable: true,\n });\n }\n }\n return directSource(\n { kind: \"json\", value },\n {\n sourceId: options.instanceId,\n revision: revisionBefore ?? uid(),\n label: options.manifest.label ?? options.manifest.surfaceName,\n unsavedChanges: options.unsavedChanges?.() ?? false,\n sections: declarations\n .filter((d) => Object.hasOwn(value, d.name))\n .map((d) => ({\n id: d.name,\n label: d.label,\n path: `/${enc(d.name)}`,\n includedByDefault: d.includedByDefault ?? true,\n })),\n projection: {\n rules: [\n ...(options.projection?.rules ?? []),\n ...declarations.map((d) => ({\n target: \"payload\" as const,\n path: `/${enc(d.name)}`,\n exportable: d.exportable ?? true,\n classification: d.classification ?? (\"ordinary\" as const),\n })),\n ],\n },\n ...(options.limits ? { limits: options.limits } : {}),\n },\n );\n },\n };\n}\n\nfunction limitJson(\n value: Json,\n limits: Partial<TransferLimits>,\n omissions: Omission[],\n path = \"\",\n depth = 0,\n): Json {\n if (limits.maxDepth != null && depth > limits.maxDepth) {\n omissions.push({ path, reason: \"truncated\" });\n return null;\n }\n if (\n typeof value === \"string\" &&\n limits.maxStringChars != null &&\n Array.from(value).length > limits.maxStringChars\n ) {\n const chars = Array.from(value);\n omissions.push({\n path,\n reason: \"truncated\",\n count: chars.length - limits.maxStringChars,\n });\n return chars.slice(0, limits.maxStringChars).join(\"\");\n }\n if (Array.isArray(value)) {\n const count = Math.min(value.length, limits.maxArrayItems ?? value.length);\n if (count < value.length)\n omissions.push({\n path,\n reason: \"truncated\",\n count: value.length - count,\n });\n return value\n .slice(0, count)\n .map((item, index) =>\n limitJson(item, limits, omissions, `${path}/${index}`, depth + 1),\n );\n }\n if (value && typeof value === \"object\")\n return Object.fromEntries(\n Object.entries(value).map(([key, item]) => [\n key,\n limitJson(item, limits, omissions, `${path}/${enc(key)}`, depth + 1),\n ]),\n );\n return value;\n}\nfunction limitPayload(\n payload: Payload,\n limits: Partial<TransferLimits>,\n omissions: Omission[],\n): Payload {\n limitKeys.forEach((key) => assertLimit(limits[key], key));\n if (payload.kind === \"rows\") {\n const count = Math.min(\n payload.rows.length,\n limits.maxRows ?? payload.rows.length,\n );\n if (count < payload.rows.length)\n omissions.push({\n path: \"/rows\",\n reason: \"truncated\",\n count: payload.rows.length - count,\n });\n return {\n ...payload,\n rows: payload.rows\n .slice(0, count)\n .map(\n (row, index) =>\n limitJson(row, limits, omissions, `/rows/${index}`) as Record<\n string,\n Json\n >,\n ),\n };\n }\n if (payload.kind === \"json\" || payload.kind === \"registered\")\n return { ...payload, value: limitJson(payload.value, limits, omissions) };\n return {\n ...payload,\n text: limitJson(payload.text, limits, omissions) as string,\n };\n}\n/** Applies explicit preparation/source limits with exact omissions; token target is advisory. */\nexport function applyTransferLimits(\n draft: Draft,\n limits: Partial<TransferLimits>,\n): Draft {\n const omissions: Omission[] = [];\n const payload = limitPayload(draft.payload, limits, omissions);\n return omissions.length\n ? revise(draft, payload, omissions, \"Applied preparation limits\")\n : draft;\n}\n","/**\n * Wrap text in a Markdown code fence that cannot be closed by its contents.\n *\n * Fence writing is deliberately centralized here. Consumers should not grow\n * their own backtick-run detection when exporting code-shaped payloads.\n */\nexport function fencedCodeBlock(text: string, language: string): string {\n const runs = text.match(/`+/g) ?? [];\n const fence = \"`\".repeat(Math.max(3, ...runs.map((run) => run.length + 1)));\n return `${fence}${language}\\n${text}${text.endsWith(\"\\n\") ? \"\" : \"\\n\"}${fence}`;\n}\n","/**\n * `@ai-matrx/alchemy/operate/formats/rich-html` — the rich clipboard format (CONTRACT §1.9a).\n *\n * Engine: THE one Markdown→HTML engine (`@ai-matrx/print/markdown-html`, loaded lazily) with the\n * `inline` skin — styles on elements, no class dependence, because Google Docs and Word drop\n * `<style>` and classes on paste. Docs and Word are one action (\"Copy formatted\") until a\n * destination profile produces different bytes. Private images go through the host's asset\n * port. Every HTML string ends in print's allow-list.\n */\n\nimport type { MarkdownHtmlEngine } from \"@ai-matrx/print/markdown-html\";\nimport { ContentTransferError, sealTransferArtifact } from \"../core\";\nimport type { FormatEngine, FormatOptions } from \"../contract\";\nimport type { Artifact, Draft } from \"../types\";\n\nconst ENGINE = Symbol.for(\"ai-matrx.alchemy.operate.markdown-html-engine\");\n\n/** One engine per realm, created on first use (the print module itself loads only here). */\nexport async function loadMarkdownHtmlEngine(): Promise<MarkdownHtmlEngine> {\n const g = globalThis as unknown as Record<symbol, Promise<MarkdownHtmlEngine> | undefined>;\n g[ENGINE] ??= import(\"@ai-matrx/print/markdown-html\").then((m) => m.createMarkdownHtmlEngine());\n return g[ENGINE] as Promise<MarkdownHtmlEngine>;\n}\n\nfunction parseOptions(options: FormatOptions, signal: AbortSignal) {\n return {\n signal,\n ...(options.onNotice ? { onNotice: options.onNotice } : {}),\n ...(options.assets ? { assets: options.assets } : {}),\n };\n}\n\n/** Plain text from the one parse (the `plain` format for Markdown payloads). */\nexport async function markdownPlainText(markdown: string, options: FormatOptions, signal: AbortSignal): Promise<string> {\n const engine = await loadMarkdownHtmlEngine();\n return engine.plainText(await engine.parse(markdown, parseOptions(options, signal)));\n}\n\nexport const richHtmlEngine: FormatEngine = {\n async build(draft: Draft, options: FormatOptions, signal: AbortSignal): Promise<Artifact> {\n if (signal.aborted) throw new DOMException(\"Cancelled\", \"AbortError\");\n if (draft.payload.kind !== \"markdown\") {\n throw new ContentTransferError(\"unsupported-format\", \"Formatted copy needs Markdown content. Copy it as plain text instead.\");\n }\n const engine = await loadMarkdownHtmlEngine();\n const tree = await engine.parse(draft.payload.text, parseOptions(options, signal));\n if (signal.aborted) throw new DOMException(\"Cancelled\", \"AbortError\");\n const html = engine.fragment(tree, { skin: options.skin ?? \"inline\", profile: options.profile ?? \"generic\" });\n return sealTransferArtifact({\n snapshotId: draft.snapshotId,\n draftRevision: draft.revision,\n format: \"rich-html\",\n plainText: engine.plainText(tree),\n html,\n file: { filename: options.filename ?? \"export.html\", mime: \"text/html;charset=utf-8\", bytes: new TextEncoder().encode(html) },\n omissions: draft.omissions.map((o) => ({ ...o })),\n });\n },\n};\n","/**\n * `@ai-matrx/alchemy/operate/formats/document` — HTML page, PDF, DOCX and EPUB (CONTRACT §1.9).\n *\n * One tree: the HTML page is THE one Markdown→HTML engine's `document()` (print's\n * `documentToHtml`, body allow-listed); PDF (vector, selectable text), DOCX and EPUB are print's\n * `/document` writers over the same parse. Print loads only when one of these is chosen.\n * A text payload with a language exports as its fenced code block.\n */\n\nimport { ContentTransferError, sealTransferArtifact } from \"../core\";\nimport type { FormatEngine, FormatOptions } from \"../contract\";\nimport { fencedCodeBlock } from \"../fenced-code\";\nimport type { Artifact, Draft } from \"../types\";\nimport { loadMarkdownHtmlEngine } from \"./rich-html\";\n\nconst enc = new TextEncoder();\n\nfunction markdownOf(draft: Draft): string {\n const p = draft.payload;\n if (p.kind === \"markdown\") return p.text;\n if (p.kind === \"text\") {\n if (!p.language) return p.text;\n return fencedCodeBlock(p.text, p.language);\n }\n throw new ContentTransferError(\"unsupported-format\", \"A document needs text or Markdown content. Download it as JSON or CSV instead.\");\n}\n\nconst stem = (filename: string | undefined) => (filename ? filename.replace(/\\.[a-z0-9]+$/i, \"\") : undefined);\n\n/** The standalone HTML page (the `html` format for prose). */\nexport const documentHtmlEngine: FormatEngine = {\n async build(draft: Draft, options: FormatOptions, signal: AbortSignal): Promise<Artifact> {\n if (signal.aborted) throw new DOMException(\"Cancelled\", \"AbortError\");\n const engine = await loadMarkdownHtmlEngine();\n const tree = await engine.parse(markdownOf(draft), {\n signal,\n ...(options.onNotice ? { onNotice: options.onNotice } : {}),\n ...(options.assets ? { assets: options.assets } : {}),\n });\n const page = engine.document(tree);\n return sealTransferArtifact({\n snapshotId: draft.snapshotId,\n draftRevision: draft.revision,\n format: \"html\",\n plainText: engine.plainText(tree),\n html: page,\n file: { filename: options.filename ?? \"export.html\", mime: \"text/html;charset=utf-8\", bytes: enc.encode(page) },\n omissions: draft.omissions.map((o) => ({ ...o })),\n });\n },\n};\n\n/** PDF / DOCX / EPUB through print's `/document` (lazy). */\nexport function documentEngine(format: \"pdf\" | \"docx\" | \"epub\"): FormatEngine {\n return {\n async build(draft: Draft, options: FormatOptions, signal: AbortSignal): Promise<Artifact> {\n if (signal.aborted) throw new DOMException(\"Cancelled\", \"AbortError\");\n const source = markdownOf(draft);\n const { exportDocument } = await import(\"@ai-matrx/print/document\");\n const name = stem(options.filename);\n const out = await exportDocument(source, format, {\n ...(name ? { fileName: name } : {}),\n ...(options.onNotice ? { onNotice: (n) => options.onNotice?.({ code: n.code, sentence: n.message, remedy: n.remedy }) } : {}),\n });\n if (signal.aborted) throw new DOMException(\"Cancelled\", \"AbortError\");\n return sealTransferArtifact({\n snapshotId: draft.snapshotId,\n draftRevision: draft.revision,\n format,\n plainText: source,\n file: { filename: out.fileName, mime: out.mime, bytes: out.bytes },\n omissions: draft.omissions.map((o) => ({ ...o })),\n });\n },\n };\n}\n"],"mappings":"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","names":["ContentTransferError","normalizeTransferJson","ContentTransferError"]}
@@ -0,0 +1,6 @@
1
+ "use strict";var __create=Object.create;var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __getProtoOf=Object.getPrototypeOf,__hasOwnProp=Object.prototype.hasOwnProperty;var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:!0})},__copyProps=(to,from,except,desc)=>{if(from&&typeof from=="object"||typeof from=="function")for(let key of __getOwnPropNames(from))!__hasOwnProp.call(to,key)&&key!==except&&__defProp(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc(from,key))||desc.enumerable});return to};var __toESM=(mod,isNodeMode,target)=>(target=mod!=null?__create(__getProtoOf(mod)):{},__copyProps(isNodeMode||!mod||!mod.__esModule?__defProp(target,"default",{value:mod,enumerable:!0}):target,mod)),__toCommonJS=mod=>__copyProps(__defProp({},"__esModule",{value:!0}),mod);var operate_read_exports={};__export(operate_read_exports,{UnreadableFormatError:()=>UnreadableFormatError,fileFormatOf:()=>fileFormatOf,parseDelimited:()=>parseDelimited,readFile:()=>readFile,readWorkbook:()=>readWorkbook,toDelimitedText:()=>toDelimitedText});module.exports=__toCommonJS(operate_read_exports);var import_operate=require("@ai-matrx/alchemy/operate"),RECORD_SEP="",UNIT_SEP="",GUESS_DELIMITERS=[","," ","|",";",RECORD_SEP,UNIT_SEP],FLOAT=/^\s*-?(\d+\.?|\.\d+|\d+\.\d+)([eE][-+]?\d+)?\s*$/,ISO_DATE=/^((\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z)))$/;function guessLinebreak(text){let crlf=text.indexOf(`\r
2
+ `),lf=text.indexOf(`
3
+ `),cr=text.indexOf("\r");return crlf!==-1&&crlf===cr?`\r
4
+ `:cr!==-1&&(lf===-1||cr<lf)?"\r":`
5
+ `}function tokenize(text,delimiter,linebreak,preview=1/0){let rows=[],errors=[];if(text==="")return{rows,errors};let row=[],i=0,n=text.length,dl=delimiter.length,ll=linebreak.length;for(;i<=n&&rows.length<preview;){if(text.startsWith('"',i)){let value="",j=i+1,closed=!1;for(;j<n;){let q=text.indexOf('"',j);if(q===-1)break;if(value+=text.slice(j,q),text[q+1]==='"'){value+='"',j=q+2;continue}let after=q+1;if(after>=n||text.startsWith(delimiter,after)||text.startsWith(linebreak,after)){closed=!0,j=after;break}errors.push({type:"Quotes",code:"InvalidQuotes",message:"Trailing quote on quoted field is malformed",row:rows.length}),value+='"',j=after}if(!closed)return errors.push({type:"Quotes",code:"MissingQuotes",message:"Quoted field unterminated",row:rows.length}),value+=text.slice(j),row.push(value),rows.push(row),{rows,errors};if(row.push(value),i=j,i>=n){rows.push(row);break}if(text.startsWith(delimiter,i)){if(i+=dl,i>=n){row.push(""),rows.push(row);break}continue}if(rows.push(row),row=[],i+=ll,i>=n){rows.push([""]);break}continue}let nextDelim=text.indexOf(delimiter,i),nextBreak=text.indexOf(linebreak,i);if(nextDelim!==-1&&(nextBreak===-1||nextDelim<nextBreak)){if(row.push(text.slice(i,nextDelim)),i=nextDelim+dl,i>=n){row.push(""),rows.push(row);break}continue}if(nextBreak!==-1){if(row.push(text.slice(i,nextBreak)),rows.push(row),row=[],i=nextBreak+ll,i>=n){rows.push([""]);break}continue}row.push(text.slice(i)),rows.push(row);break}return{rows,errors}}var isEmptyRow=(row,mode)=>mode==="greedy"?row.join("").trim()==="":row.length===1&&row[0].length===0;function guessDelimiter(text,linebreak,skip){let best,bestDelta,bestAvg;for(let delim of GUESS_DELIMITERS){let{rows}=tokenize(text,delim,linebreak,10),delta=0,avg=0,empty=0,prev;for(let row of rows){if(skip&&isEmptyRow(row,skip)){empty++;continue}let count=row.length;if(avg+=count,prev===void 0){prev=count;continue}count>0&&(delta+=Math.abs(count-prev),prev=count)}rows.length>0&&(avg/=rows.length-empty),(bestDelta===void 0||delta<=bestDelta)&&(bestAvg===void 0||avg>bestAvg)&&avg>1.99&&(bestDelta=delta,bestAvg=avg,best=delim)}return{delimiter:best??",",successful:best!==void 0}}function typed(value){if(value==="true"||value==="TRUE")return!0;if(value==="false"||value==="FALSE")return!1;if(FLOAT.test(value)){let num=parseFloat(value);return num>-(2**53)&&num<2**53?num:value}return ISO_DATE.test(value)?new Date(value):value===""?null:value}function parseDelimited(text,options={}){let input=text.charCodeAt(0)===65279?text.slice(1):text,linebreak=guessLinebreak(input),errors=[],delimiter=options.delimiter;if(!delimiter){let guess=guessDelimiter(input,linebreak,options.skipEmptyLines);delimiter=guess.delimiter,guess.successful||errors.push({type:"Delimiter",code:"UndetectableDelimiter",message:"Unable to auto-detect delimiting character; defaulted to ','"})}let tokens=tokenize(input,delimiter,linebreak);errors.unshift(...tokens.errors);let rows=tokens.rows;options.skipEmptyLines&&(rows=rows.filter(row=>!isEmptyRow(row,options.skipEmptyLines)));let meta={delimiter,linebreak};if(!options.header)return{data:options.dynamicTyping?rows.map(row=>row.map(typed)):rows,errors,meta};let[head=[],...body]=rows,seen=new Map,fields=head.map((raw,index)=>{let name=options.transformHeader?options.transformHeader(raw,index):raw,count=seen.get(name)??0;return seen.set(name,count+1),count===0?name:`${name}_${count}`});return meta.fields=fields,{data:body.map((row,index)=>{let record={};return row.forEach((value,j)=>{let cell=options.dynamicTyping?typed(value):value;j>=fields.length?(record.__parsed_extra??=[]).push(cell):record[fields[j]]=cell}),row.length>fields.length?errors.push({type:"FieldMismatch",code:"TooManyFields",message:`Too many fields: expected ${fields.length} fields but parsed ${row.length}`,row:index}):row.length<fields.length&&errors.push({type:"FieldMismatch",code:"TooFewFields",message:`Too few fields: expected ${fields.length} fields but parsed ${row.length}`,row:index}),record}),errors,meta}}function toDelimitedText(header,rows,options={}){let columns=header.map((label,i)=>({id:`c${i}`,label,path:`/c${i}`,visible:!0})),record=row=>{let out={};return header.forEach((label,i)=>{let v=Array.isArray(row)?row[i]:row[label];v!==void 0&&(out[`c${i}`]=v)}),out},captured=(0,import_operate.directSource)({kind:"rows",columns,rows:rows.map(record)}),draft=(0,import_operate.createDraft)((0,import_operate.projectSnapshot)(captured.snapshot,captured.projection));return(0,import_operate.serialize)(draft,options.format??"csv",{spreadsheetSafe:options.spreadsheetSafe??!1}).plainText}async function bytesOf(input){return typeof input=="string"?new TextEncoder().encode(input):input instanceof Uint8Array?input:input instanceof ArrayBuffer?new Uint8Array(input):new Uint8Array(await input.arrayBuffer())}async function readWorkbook(input,options={}){let bytes=await bytesOf(input),{readWorkbookBytes}=await import("@ai-matrx/alchemy/operate/formats/xlsx");return readWorkbookBytes(bytes,options)}var WORKBOOK_FORMATS=new Set(["xlsx","xls","ods","xlsm","xlsb"]),DELIMITED_FORMATS=new Set(["csv","tsv","txt","tab"]),UnreadableFormatError=class extends Error{constructor(format){super(`A "${format}" file is not a table we can read. Choose a CSV, TSV, or Excel file.`);this.format=format;this.name="UnreadableFormatError"}format};function fileFormatOf(name){let m=/\.([a-z0-9]+)$/i.exec(name??"");return m?m[1].toLowerCase():""}async function readFile(input,options={}){let name=options.filename??(typeof Blob<"u"&&input instanceof Blob&&"name"in input?String(input.name):void 0),format=(options.format??fileFormatOf(name))||(typeof input=="string"?"csv":"");if(WORKBOOK_FORMATS.has(format)){let book=await readWorkbook(input,options.workbook);return{format:format==="xlsm"||format==="xlsb"?"xlsx":format,sheets:book.sheets.map(s=>({name:s.name,grid:s.grid(options.grid),merges:s.merges})),errors:[]}}if(!DELIMITED_FORMATS.has(format))throw new UnreadableFormatError(format||"unknown");let text=typeof input=="string"?input:new TextDecoder("utf-8").decode(await bytesOf(input)),parsed=parseDelimited(text,{...options,header:!1,...options.delimiter?{}:format==="tsv"||format==="tab"?{delimiter:" "}:{}});return{format:format==="tab"?"tsv":format,sheets:[{name:name?.replace(/\.[^.]+$/,"")||"Sheet1",grid:parsed.data,merges:[]}],errors:parsed.errors,delimiter:parsed.meta.delimiter}}
6
+ //# sourceMappingURL=operate-read.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/operate-read.ts","../src/operate/read.ts"],"sourcesContent":["/** `@ai-matrx/alchemy/operate/read` — the one parse door (CSV/TSV parser; workbooks through the lazy SheetJS leaf). */\nexport {\n UnreadableFormatError,\n fileFormatOf,\n parseDelimited,\n readFile,\n readWorkbook,\n toDelimitedText,\n type DelimitedCell,\n type DelimitedMeta,\n type DelimitedParseError,\n type DelimitedParseOptions,\n type DelimitedResult,\n type ReadFileOptions,\n type ReadInput,\n type TabularFile,\n type TabularFormat,\n type TabularSheet,\n type Workbook,\n type WorkbookCell,\n type WorkbookCellAddress,\n type WorkbookCellJson,\n type WorkbookCellRange,\n type WorkbookGridOptions,\n type WorkbookJson,\n type WorkbookReadOptions,\n type WorkbookRecordOptions,\n type WorkbookSheet,\n} from \"./operate/read\";\n","/**\n * `@ai-matrx/alchemy/operate/read` — the one parse door (ALC-19 T4–T12). Write has `buildFile`; read has `readFile`.\n *\n * One engine per format across read and write:\n * - CSV / TSV / delimited text: THIS module's parser (no library), the reverse of the `csv`/`tsv`\n * serializers in `./core`. `parseDelimited` is Papa Parse–compatible in the options the platform\n * uses (auto delimiter + line ending, quotes, `skipEmptyLines` true | \"greedy\", `header`,\n * `transformHeader`, `dynamicTyping`) and answers the same `{ data, errors, meta }` shape and\n * error codes, so a repointed importer keeps its behaviour.\n * - XLSX / XLS / ODS: SheetJS, the library the `xlsx` writer uses, loaded on first use from the\n * `/operate/formats/xlsx` leaf (`readWorkbook`). A caller that needs spreadsheet typing of a CSV\n * (dates, numbers, formulas as SheetJS reads them) reads it through `readWorkbook` too.\n */\n\n// Through the PUBLIC `/operate` subpath, so this entry shares the transfer core at runtime instead of bundling a copy.\nimport { createDraft, directSource, projectSnapshot, serialize, type Column, type Json } from \"@ai-matrx/alchemy/operate\";\n\n// ─── Delimited text ───────────────────────────────────────────────────────────────────────────────\n\nexport type DelimitedParseError = {\n type: \"Quotes\" | \"Delimiter\" | \"FieldMismatch\";\n code: \"MissingQuotes\" | \"InvalidQuotes\" | \"UndetectableDelimiter\" | \"TooFewFields\" | \"TooManyFields\";\n message: string;\n /** 0-based index of the data row the error belongs to (absent for whole-file errors). */\n row?: number;\n};\n\nexport type DelimitedParseOptions = {\n /** Field delimiter. Absent → guessed from `,` `\\t` `|` `;` and the ASCII record/unit separators; `,` when none fits. */\n delimiter?: string;\n /** true: an exactly empty line is dropped; \"greedy\": a line of only whitespace is dropped too. */\n skipEmptyLines?: boolean | \"greedy\";\n /** Numbers, `true`/`false` and ISO date-times become typed values; an empty field becomes null. */\n dynamicTyping?: boolean;\n /** Header mode only: rename a header (e.g. trim + lowercase). */\n transformHeader?(header: string, index: number): string;\n};\n\nexport type DelimitedCell = string | number | boolean | Date | null;\nexport type DelimitedMeta = { delimiter: string; linebreak: string; fields?: string[] };\nexport type DelimitedResult<T> = { data: T[]; errors: DelimitedParseError[]; meta: DelimitedMeta };\n\nconst RECORD_SEP = String.fromCharCode(30);\nconst UNIT_SEP = String.fromCharCode(31);\nconst GUESS_DELIMITERS = [\",\", \"\\t\", \"|\", \";\", RECORD_SEP, UNIT_SEP];\nconst FLOAT = /^\\s*-?(\\d+\\.?|\\.\\d+|\\d+\\.\\d+)([eE][-+]?\\d+)?\\s*$/;\nconst ISO_DATE =\n /^((\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d:[0-5]\\d|Z))|(\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d([+-][0-2]\\d:[0-5]\\d|Z))|(\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d([+-][0-2]\\d:[0-5]\\d|Z)))$/;\n\nfunction guessLinebreak(text: string): string {\n const crlf = text.indexOf(\"\\r\\n\");\n const lf = text.indexOf(\"\\n\");\n const cr = text.indexOf(\"\\r\");\n if (crlf !== -1 && crlf === cr) return \"\\r\\n\";\n if (cr !== -1 && (lf === -1 || cr < lf)) return \"\\r\";\n return \"\\n\";\n}\n\n/** Tokenize into rows of raw string fields; quote errors are reported against their data row. */\nfunction tokenize(text: string, delimiter: string, linebreak: string, preview = Infinity): { rows: string[][]; errors: DelimitedParseError[] } {\n const rows: string[][] = [];\n const errors: DelimitedParseError[] = [];\n if (text === \"\") return { rows, errors };\n let row: string[] = [];\n let i = 0;\n const n = text.length;\n const dl = delimiter.length;\n const ll = linebreak.length;\n while (i <= n && rows.length < preview) {\n if (text.startsWith('\"', i)) {\n // Quoted field: \"\" is a literal quote; the closing quote must precede a delimiter, a line break or the end.\n let value = \"\";\n let j = i + 1;\n let closed = false;\n while (j < n) {\n const q = text.indexOf('\"', j);\n if (q === -1) break;\n value += text.slice(j, q);\n if (text[q + 1] === '\"') {\n value += '\"';\n j = q + 2;\n continue;\n }\n const after = q + 1;\n if (after >= n || text.startsWith(delimiter, after) || text.startsWith(linebreak, after)) {\n closed = true;\n j = after;\n break;\n }\n errors.push({ type: \"Quotes\", code: \"InvalidQuotes\", message: \"Trailing quote on quoted field is malformed\", row: rows.length });\n value += '\"';\n j = after;\n }\n if (!closed) {\n errors.push({ type: \"Quotes\", code: \"MissingQuotes\", message: \"Quoted field unterminated\", row: rows.length });\n value += text.slice(j);\n row.push(value);\n rows.push(row);\n return { rows, errors };\n }\n row.push(value);\n i = j;\n if (i >= n) {\n rows.push(row);\n break;\n }\n if (text.startsWith(delimiter, i)) {\n i += dl;\n if (i >= n) {\n row.push(\"\");\n rows.push(row);\n break;\n }\n continue;\n }\n rows.push(row);\n row = [];\n i += ll;\n if (i >= n) {\n rows.push([\"\"]);\n break;\n }\n continue;\n }\n const nextDelim = text.indexOf(delimiter, i);\n const nextBreak = text.indexOf(linebreak, i);\n if (nextDelim !== -1 && (nextBreak === -1 || nextDelim < nextBreak)) {\n row.push(text.slice(i, nextDelim));\n i = nextDelim + dl;\n if (i >= n) {\n row.push(\"\");\n rows.push(row);\n break;\n }\n continue;\n }\n if (nextBreak !== -1) {\n row.push(text.slice(i, nextBreak));\n rows.push(row);\n row = [];\n i = nextBreak + ll;\n if (i >= n) {\n rows.push([\"\"]);\n break;\n }\n continue;\n }\n row.push(text.slice(i));\n rows.push(row);\n break;\n }\n return { rows, errors };\n}\n\nconst isEmptyRow = (row: readonly string[], mode: boolean | \"greedy\") =>\n mode === \"greedy\" ? row.join(\"\").trim() === \"\" : row.length === 1 && row[0]!.length === 0;\n\nfunction guessDelimiter(text: string, linebreak: string, skip: boolean | \"greedy\" | undefined): { delimiter: string; successful: boolean } {\n let best: string | undefined;\n let bestDelta: number | undefined;\n let bestAvg: number | undefined;\n for (const delim of GUESS_DELIMITERS) {\n const { rows } = tokenize(text, delim, linebreak, 10);\n let delta = 0;\n let avg = 0;\n let empty = 0;\n let prev: number | undefined;\n for (const row of rows) {\n if (skip && isEmptyRow(row, skip)) {\n empty++;\n continue;\n }\n const count = row.length;\n avg += count;\n if (prev === undefined) {\n prev = count;\n continue;\n }\n if (count > 0) {\n delta += Math.abs(count - prev);\n prev = count;\n }\n }\n if (rows.length > 0) avg /= rows.length - empty;\n if ((bestDelta === undefined || delta <= bestDelta) && (bestAvg === undefined || avg > bestAvg) && avg > 1.99) {\n bestDelta = delta;\n bestAvg = avg;\n best = delim;\n }\n }\n return { delimiter: best ?? \",\", successful: best !== undefined };\n}\n\nfunction typed(value: string): DelimitedCell {\n if (value === \"true\" || value === \"TRUE\") return true;\n if (value === \"false\" || value === \"FALSE\") return false;\n if (FLOAT.test(value)) {\n const num = parseFloat(value);\n if (num > -(2 ** 53) && num < 2 ** 53) return num;\n return value;\n }\n if (ISO_DATE.test(value)) return new Date(value);\n return value === \"\" ? null : value;\n}\n\n/** Parse delimited text into rows of fields (header row first). */\nexport function parseDelimited(text: string, options?: DelimitedParseOptions & { header?: false; dynamicTyping?: false }): DelimitedResult<string[]>;\nexport function parseDelimited(text: string, options: DelimitedParseOptions & { header?: false; dynamicTyping: true }): DelimitedResult<DelimitedCell[]>;\n/** Parse delimited text into records keyed by the (transformed) header row. Extra fields land in `__parsed_extra`. */\nexport function parseDelimited(text: string, options: DelimitedParseOptions & { header: true; dynamicTyping?: false }): DelimitedResult<Record<string, string>>;\nexport function parseDelimited(text: string, options: DelimitedParseOptions & { header: true; dynamicTyping: true }): DelimitedResult<Record<string, DelimitedCell>>;\nexport function parseDelimited(text: string, options: DelimitedParseOptions & { header?: boolean } = {}): DelimitedResult<unknown> {\n const input = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;\n const linebreak = guessLinebreak(input);\n const errors: DelimitedParseError[] = [];\n let delimiter = options.delimiter;\n if (!delimiter) {\n const guess = guessDelimiter(input, linebreak, options.skipEmptyLines);\n delimiter = guess.delimiter;\n if (!guess.successful)\n errors.push({ type: \"Delimiter\", code: \"UndetectableDelimiter\", message: \"Unable to auto-detect delimiting character; defaulted to ','\" });\n }\n const tokens = tokenize(input, delimiter, linebreak);\n errors.unshift(...tokens.errors);\n let rows = tokens.rows;\n if (options.skipEmptyLines) rows = rows.filter((row) => !isEmptyRow(row, options.skipEmptyLines!));\n const meta: DelimitedMeta = { delimiter, linebreak };\n if (!options.header) {\n return { data: options.dynamicTyping ? rows.map((row) => row.map(typed)) : rows, errors, meta };\n }\n const [head = [], ...body] = rows;\n const seen = new Map<string, number>();\n const fields = head.map((raw, index) => {\n const name = options.transformHeader ? options.transformHeader(raw, index) : raw;\n const count = seen.get(name) ?? 0;\n seen.set(name, count + 1);\n return count === 0 ? name : `${name}_${count}`;\n });\n meta.fields = fields;\n const data = body.map((row, index) => {\n const record: Record<string, unknown> = {};\n row.forEach((value, j) => {\n const cell = options.dynamicTyping ? typed(value) : value;\n if (j >= fields.length) {\n ((record.__parsed_extra as unknown[] | undefined) ??= []).push(cell);\n } else record[fields[j]!] = cell;\n });\n if (row.length > fields.length)\n errors.push({ type: \"FieldMismatch\", code: \"TooManyFields\", message: `Too many fields: expected ${fields.length} fields but parsed ${row.length}`, row: index });\n else if (row.length < fields.length)\n errors.push({ type: \"FieldMismatch\", code: \"TooFewFields\", message: `Too few fields: expected ${fields.length} fields but parsed ${row.length}`, row: index });\n return record;\n });\n return { data, errors, meta };\n}\n\n/**\n * Delimited text from a header row + body rows, through the SAME `csv`/`tsv` serializer the\n * Alchemy menu's copy and download use (quoting, nested values as compact JSON, `null` empty).\n * `spreadsheetSafe` (default false here) prefixes formula-looking text with `'`.\n */\nexport function toDelimitedText(\n header: readonly string[],\n rows: readonly (readonly (Json | undefined)[] | Readonly<Record<string, Json | undefined>>)[],\n options: { format?: \"csv\" | \"tsv\"; spreadsheetSafe?: boolean } = {},\n): string {\n const columns: Column[] = header.map((label, i) => ({ id: `c${i}`, label, path: `/c${i}`, visible: true }));\n const record = (row: (typeof rows)[number]): Record<string, Json> => {\n const out: Record<string, Json> = {};\n header.forEach((label, i) => {\n const v = Array.isArray(row) ? (row as readonly (Json | undefined)[])[i] : (row as Readonly<Record<string, Json | undefined>>)[label];\n if (v !== undefined) out[`c${i}`] = v;\n });\n return out;\n };\n const captured = directSource({ kind: \"rows\", columns, rows: rows.map(record) });\n const draft = createDraft(projectSnapshot(captured.snapshot, captured.projection));\n return serialize(draft, options.format ?? \"csv\", { spreadsheetSafe: options.spreadsheetSafe ?? false }).plainText;\n}\n\n// ─── Workbooks (the SheetJS leaf, loaded on first use) ───────────────────────────────────────────\n\nexport type WorkbookCellAddress = { r: number; c: number };\nexport type WorkbookCellRange = { s: WorkbookCellAddress; e: WorkbookCellAddress };\n/** A cell as the workbook engine reads it: type, raw value, formatted text, formula source. */\nexport type WorkbookCell = {\n t: \"s\" | \"n\" | \"b\" | \"d\" | \"e\" | \"z\";\n v?: string | number | boolean | Date;\n w?: string;\n f?: string;\n styled: boolean;\n};\n/** Grid shape options (header row included): `raw: false` → formatted text; `defval` fills gaps. */\nexport type WorkbookGridOptions = { raw?: boolean; defval?: unknown; blankrows?: boolean; dateNF?: string; rawNumbers?: boolean };\nexport type WorkbookRecordOptions = { raw?: boolean; defval?: unknown; blankrows?: boolean; dateNF?: string; rawNumbers?: boolean };\nexport interface WorkbookSheet {\n name: string;\n /** The used range, null for a sheet without one. */\n range: WorkbookCellRange | null;\n merges: WorkbookCellRange[];\n cell(r: number, c: number): WorkbookCell | null;\n cells(): Iterable<readonly [WorkbookCellAddress, WorkbookCell]>;\n grid(options?: WorkbookGridOptions): unknown[][];\n records(options?: WorkbookRecordOptions): Record<string, unknown>[];\n}\nexport interface Workbook {\n sheetNames: string[];\n sheets: WorkbookSheet[];\n sheet(name: string): WorkbookSheet | undefined;\n}\n/** `dates`: date cells as Date (`t: \"d\"`); `formulas`: keep formula source; `styles`: read cell styles. */\nexport type WorkbookReadOptions = { dates?: boolean; formulas?: boolean; styles?: boolean };\n\n/** A typed cell for the `xlsx-workbook` format; a bare string/number/boolean is a value cell. */\nexport type WorkbookCellJson = { t?: \"s\" | \"n\" | \"b\"; v?: string | number | boolean | null; f?: string };\n/** The `xlsx-workbook` payload value (`{ kind: \"registered\", format: \"workbook\", value }`). */\nexport type WorkbookJson = {\n sheets: { name: string; rows: (WorkbookCellJson | string | number | boolean | null)[][]; merges?: WorkbookCellRange[] }[];\n};\n\nexport type ReadInput = Blob | ArrayBuffer | Uint8Array | string;\n\nasync function bytesOf(input: ReadInput): Promise<Uint8Array> {\n if (typeof input === \"string\") return new TextEncoder().encode(input);\n if (input instanceof Uint8Array) return input;\n if (input instanceof ArrayBuffer) return new Uint8Array(input);\n return new Uint8Array(await input.arrayBuffer());\n}\n\n/** Read a spreadsheet (XLSX, XLS, ODS; CSV/TSV with spreadsheet typing) through the one workbook engine. */\nexport async function readWorkbook(input: ReadInput, options: WorkbookReadOptions = {}): Promise<Workbook> {\n const bytes = await bytesOf(input);\n const { readWorkbookBytes } = await import(\"@ai-matrx/alchemy/operate/formats/xlsx\");\n return readWorkbookBytes(bytes, options);\n}\n\n// ─── readFile: the door ──────────────────────────────────────────────────────────────────────────\n\nexport type TabularFormat = \"csv\" | \"tsv\" | \"txt\" | \"xlsx\" | \"xls\" | \"ods\";\nconst WORKBOOK_FORMATS: ReadonlySet<string> = new Set([\"xlsx\", \"xls\", \"ods\", \"xlsm\", \"xlsb\"]);\nconst DELIMITED_FORMATS: ReadonlySet<string> = new Set([\"csv\", \"tsv\", \"txt\", \"tab\"]);\n\nexport type ReadFileOptions = DelimitedParseOptions & {\n /** The file's format; absent → from `filename` (or a File's name) extension. */\n format?: TabularFormat | (string & {});\n filename?: string;\n /** Workbooks: the grid shape (`sheet_to_json` header-row semantics). */\n grid?: WorkbookGridOptions;\n /** Workbooks: read options. */\n workbook?: WorkbookReadOptions;\n};\nexport type TabularSheet = { name: string; grid: unknown[][]; merges: WorkbookCellRange[] };\nexport type TabularFile = { format: TabularFormat; sheets: TabularSheet[]; errors: DelimitedParseError[]; delimiter?: string };\n\nexport class UnreadableFormatError extends Error {\n constructor(readonly format: string) {\n super(`A \"${format}\" file is not a table we can read. Choose a CSV, TSV, or Excel file.`);\n this.name = \"UnreadableFormatError\";\n }\n}\n\n/** The format a file name declares (lower-case extension), or \"\" when it has none. */\nexport function fileFormatOf(name: string | undefined): string {\n const m = /\\.([a-z0-9]+)$/i.exec(name ?? \"\");\n return m ? m[1]!.toLowerCase() : \"\";\n}\n\n/**\n * Read a table file — CSV/TSV/plain text through the delimited parser, XLSX/XLS/ODS through the\n * workbook engine — into sheets of grids (header row first). Delimited text is one sheet.\n */\nexport async function readFile(input: ReadInput, options: ReadFileOptions = {}): Promise<TabularFile> {\n const name = options.filename ?? (typeof Blob !== \"undefined\" && input instanceof Blob && \"name\" in input ? String((input as File).name) : undefined);\n const format = (options.format ?? fileFormatOf(name)) || (typeof input === \"string\" ? \"csv\" : \"\");\n if (WORKBOOK_FORMATS.has(format)) {\n const book = await readWorkbook(input, options.workbook);\n return {\n format: (format === \"xlsm\" || format === \"xlsb\" ? \"xlsx\" : format) as TabularFormat,\n sheets: book.sheets.map((s) => ({ name: s.name, grid: s.grid(options.grid), merges: s.merges })),\n errors: [],\n };\n }\n if (!DELIMITED_FORMATS.has(format)) throw new UnreadableFormatError(format || \"unknown\");\n const text = typeof input === \"string\" ? input : new TextDecoder(\"utf-8\").decode(await bytesOf(input));\n const parsed = parseDelimited(text, {\n ...options,\n header: false,\n ...(options.delimiter ? {} : format === \"tsv\" || format === \"tab\" ? { delimiter: \"\\t\" } : {}),\n } as DelimitedParseOptions & { header: false; dynamicTyping?: false });\n return {\n format: (format === \"tab\" ? \"tsv\" : format) as TabularFormat,\n sheets: [{ name: name?.replace(/\\.[^.]+$/, \"\") || \"Sheet1\", grid: parsed.data, merges: [] }],\n errors: parsed.errors,\n delimiter: parsed.meta.delimiter,\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,174 @@
1
+ import { Json } from '@ai-matrx/alchemy/operate';
2
+
3
+ /**
4
+ * `@ai-matrx/alchemy/operate/read` — the one parse door (ALC-19 T4–T12). Write has `buildFile`; read has `readFile`.
5
+ *
6
+ * One engine per format across read and write:
7
+ * - CSV / TSV / delimited text: THIS module's parser (no library), the reverse of the `csv`/`tsv`
8
+ * serializers in `./core`. `parseDelimited` is Papa Parse–compatible in the options the platform
9
+ * uses (auto delimiter + line ending, quotes, `skipEmptyLines` true | "greedy", `header`,
10
+ * `transformHeader`, `dynamicTyping`) and answers the same `{ data, errors, meta }` shape and
11
+ * error codes, so a repointed importer keeps its behaviour.
12
+ * - XLSX / XLS / ODS: SheetJS, the library the `xlsx` writer uses, loaded on first use from the
13
+ * `/operate/formats/xlsx` leaf (`readWorkbook`). A caller that needs spreadsheet typing of a CSV
14
+ * (dates, numbers, formulas as SheetJS reads them) reads it through `readWorkbook` too.
15
+ */
16
+
17
+ type DelimitedParseError = {
18
+ type: "Quotes" | "Delimiter" | "FieldMismatch";
19
+ code: "MissingQuotes" | "InvalidQuotes" | "UndetectableDelimiter" | "TooFewFields" | "TooManyFields";
20
+ message: string;
21
+ /** 0-based index of the data row the error belongs to (absent for whole-file errors). */
22
+ row?: number;
23
+ };
24
+ type DelimitedParseOptions = {
25
+ /** Field delimiter. Absent → guessed from `,` `\t` `|` `;` and the ASCII record/unit separators; `,` when none fits. */
26
+ delimiter?: string;
27
+ /** true: an exactly empty line is dropped; "greedy": a line of only whitespace is dropped too. */
28
+ skipEmptyLines?: boolean | "greedy";
29
+ /** Numbers, `true`/`false` and ISO date-times become typed values; an empty field becomes null. */
30
+ dynamicTyping?: boolean;
31
+ /** Header mode only: rename a header (e.g. trim + lowercase). */
32
+ transformHeader?(header: string, index: number): string;
33
+ };
34
+ type DelimitedCell = string | number | boolean | Date | null;
35
+ type DelimitedMeta = {
36
+ delimiter: string;
37
+ linebreak: string;
38
+ fields?: string[];
39
+ };
40
+ type DelimitedResult<T> = {
41
+ data: T[];
42
+ errors: DelimitedParseError[];
43
+ meta: DelimitedMeta;
44
+ };
45
+ /** Parse delimited text into rows of fields (header row first). */
46
+ declare function parseDelimited(text: string, options?: DelimitedParseOptions & {
47
+ header?: false;
48
+ dynamicTyping?: false;
49
+ }): DelimitedResult<string[]>;
50
+ declare function parseDelimited(text: string, options: DelimitedParseOptions & {
51
+ header?: false;
52
+ dynamicTyping: true;
53
+ }): DelimitedResult<DelimitedCell[]>;
54
+ /** Parse delimited text into records keyed by the (transformed) header row. Extra fields land in `__parsed_extra`. */
55
+ declare function parseDelimited(text: string, options: DelimitedParseOptions & {
56
+ header: true;
57
+ dynamicTyping?: false;
58
+ }): DelimitedResult<Record<string, string>>;
59
+ declare function parseDelimited(text: string, options: DelimitedParseOptions & {
60
+ header: true;
61
+ dynamicTyping: true;
62
+ }): DelimitedResult<Record<string, DelimitedCell>>;
63
+ /**
64
+ * Delimited text from a header row + body rows, through the SAME `csv`/`tsv` serializer the
65
+ * Alchemy menu's copy and download use (quoting, nested values as compact JSON, `null` empty).
66
+ * `spreadsheetSafe` (default false here) prefixes formula-looking text with `'`.
67
+ */
68
+ declare function toDelimitedText(header: readonly string[], rows: readonly (readonly (Json | undefined)[] | Readonly<Record<string, Json | undefined>>)[], options?: {
69
+ format?: "csv" | "tsv";
70
+ spreadsheetSafe?: boolean;
71
+ }): string;
72
+ type WorkbookCellAddress = {
73
+ r: number;
74
+ c: number;
75
+ };
76
+ type WorkbookCellRange = {
77
+ s: WorkbookCellAddress;
78
+ e: WorkbookCellAddress;
79
+ };
80
+ /** A cell as the workbook engine reads it: type, raw value, formatted text, formula source. */
81
+ type WorkbookCell = {
82
+ t: "s" | "n" | "b" | "d" | "e" | "z";
83
+ v?: string | number | boolean | Date;
84
+ w?: string;
85
+ f?: string;
86
+ styled: boolean;
87
+ };
88
+ /** Grid shape options (header row included): `raw: false` → formatted text; `defval` fills gaps. */
89
+ type WorkbookGridOptions = {
90
+ raw?: boolean;
91
+ defval?: unknown;
92
+ blankrows?: boolean;
93
+ dateNF?: string;
94
+ rawNumbers?: boolean;
95
+ };
96
+ type WorkbookRecordOptions = {
97
+ raw?: boolean;
98
+ defval?: unknown;
99
+ blankrows?: boolean;
100
+ dateNF?: string;
101
+ rawNumbers?: boolean;
102
+ };
103
+ interface WorkbookSheet {
104
+ name: string;
105
+ /** The used range, null for a sheet without one. */
106
+ range: WorkbookCellRange | null;
107
+ merges: WorkbookCellRange[];
108
+ cell(r: number, c: number): WorkbookCell | null;
109
+ cells(): Iterable<readonly [WorkbookCellAddress, WorkbookCell]>;
110
+ grid(options?: WorkbookGridOptions): unknown[][];
111
+ records(options?: WorkbookRecordOptions): Record<string, unknown>[];
112
+ }
113
+ interface Workbook {
114
+ sheetNames: string[];
115
+ sheets: WorkbookSheet[];
116
+ sheet(name: string): WorkbookSheet | undefined;
117
+ }
118
+ /** `dates`: date cells as Date (`t: "d"`); `formulas`: keep formula source; `styles`: read cell styles. */
119
+ type WorkbookReadOptions = {
120
+ dates?: boolean;
121
+ formulas?: boolean;
122
+ styles?: boolean;
123
+ };
124
+ /** A typed cell for the `xlsx-workbook` format; a bare string/number/boolean is a value cell. */
125
+ type WorkbookCellJson = {
126
+ t?: "s" | "n" | "b";
127
+ v?: string | number | boolean | null;
128
+ f?: string;
129
+ };
130
+ /** The `xlsx-workbook` payload value (`{ kind: "registered", format: "workbook", value }`). */
131
+ type WorkbookJson = {
132
+ sheets: {
133
+ name: string;
134
+ rows: (WorkbookCellJson | string | number | boolean | null)[][];
135
+ merges?: WorkbookCellRange[];
136
+ }[];
137
+ };
138
+ type ReadInput = Blob | ArrayBuffer | Uint8Array | string;
139
+ /** Read a spreadsheet (XLSX, XLS, ODS; CSV/TSV with spreadsheet typing) through the one workbook engine. */
140
+ declare function readWorkbook(input: ReadInput, options?: WorkbookReadOptions): Promise<Workbook>;
141
+ type TabularFormat = "csv" | "tsv" | "txt" | "xlsx" | "xls" | "ods";
142
+ type ReadFileOptions = DelimitedParseOptions & {
143
+ /** The file's format; absent → from `filename` (or a File's name) extension. */
144
+ format?: TabularFormat | (string & {});
145
+ filename?: string;
146
+ /** Workbooks: the grid shape (`sheet_to_json` header-row semantics). */
147
+ grid?: WorkbookGridOptions;
148
+ /** Workbooks: read options. */
149
+ workbook?: WorkbookReadOptions;
150
+ };
151
+ type TabularSheet = {
152
+ name: string;
153
+ grid: unknown[][];
154
+ merges: WorkbookCellRange[];
155
+ };
156
+ type TabularFile = {
157
+ format: TabularFormat;
158
+ sheets: TabularSheet[];
159
+ errors: DelimitedParseError[];
160
+ delimiter?: string;
161
+ };
162
+ declare class UnreadableFormatError extends Error {
163
+ readonly format: string;
164
+ constructor(format: string);
165
+ }
166
+ /** The format a file name declares (lower-case extension), or "" when it has none. */
167
+ declare function fileFormatOf(name: string | undefined): string;
168
+ /**
169
+ * Read a table file — CSV/TSV/plain text through the delimited parser, XLSX/XLS/ODS through the
170
+ * workbook engine — into sheets of grids (header row first). Delimited text is one sheet.
171
+ */
172
+ declare function readFile(input: ReadInput, options?: ReadFileOptions): Promise<TabularFile>;
173
+
174
+ export { type DelimitedCell, type DelimitedMeta, type DelimitedParseError, type DelimitedParseOptions, type DelimitedResult, type ReadFileOptions, type ReadInput, type TabularFile, type TabularFormat, type TabularSheet, UnreadableFormatError, type Workbook, type WorkbookCell, type WorkbookCellAddress, type WorkbookCellJson, type WorkbookCellRange, type WorkbookGridOptions, type WorkbookJson, type WorkbookReadOptions, type WorkbookRecordOptions, type WorkbookSheet, fileFormatOf, parseDelimited, readFile, readWorkbook, toDelimitedText };