@ai-matrx/alchemy 0.15.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 (50) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/core.cjs +3 -3
  3. package/dist/core.cjs.map +1 -1
  4. package/dist/core.d.cts +27 -2
  5. package/dist/core.d.ts +27 -2
  6. package/dist/core.js +4 -4
  7. package/dist/core.js.map +1 -1
  8. package/dist/index.cjs +3 -3
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +27 -2
  11. package/dist/index.d.ts +27 -2
  12. package/dist/index.js +4 -4
  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 +3 -3
  45. package/dist/operate.cjs.map +1 -1
  46. package/dist/operate.d.cts +27 -2
  47. package/dist/operate.d.ts +27 -2
  48. package/dist/operate.js +4 -4
  49. package/dist/operate.js.map +1 -1
  50. package/package.json +27 -3
@@ -0,0 +1,2 @@
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_capture_exports={};__export(operate_capture_exports,{canvasToBlob:()=>canvasToBlob,captureElement:()=>captureElement,elementToImage:()=>elementToImage,elementsToPdf:()=>elementsToPdf,pagesToPdf:()=>pagesToPdf});module.exports=__toCommonJS(operate_capture_exports);var UNSAFE_COLOR=/\b(oklch|oklab|lab|lch|color)\s*\(/i;function safeColor(value,prop,dark){if(!UNSAFE_COLOR.test(value))return value;let p=prop.toLowerCase();return p.includes("background")?dark?"#1f2937":"#ffffff":p==="color"?dark?"#f3f4f6":"#111827":p.includes("border")||p.includes("outline")?dark?"#4b5563":"#d1d5db":p==="fill"||p==="stroke"?dark?"#9ca3af":"#374151":"#6b7280"}function patchComputedStyle(dark){let original=window.getComputedStyle,bound=original.bind(window);return window.getComputedStyle=function(elt,pseudo){let computed=bound(elt,pseudo);return new Proxy(computed,{get(target,prop){let value=target[prop];return typeof prop=="string"&&typeof value=="string"&&value&&UNSAFE_COLOR.test(value)?safeColor(value,prop,dark):prop==="getPropertyValue"?p=>{let raw=target.getPropertyValue(p);return raw&&UNSAFE_COLOR.test(raw)?safeColor(raw,p,dark):raw}:typeof value=="function"?value.bind(target):value}})},()=>{window.getComputedStyle=original}}var prefersDark=()=>typeof document<"u"&&(document.documentElement.classList.contains("dark")||typeof window.matchMedia=="function"&&window.matchMedia("(prefers-color-scheme: dark)").matches);async function captureElement(element,options={}){let{safeColors,...rest}=options,{default:html2canvas}=await import("html2canvas"),restore=safeColors?patchComputedStyle(typeof safeColors=="object"?safeColors.dark:prefersDark()):()=>{};try{return await html2canvas(element,{scale:2,logging:!1,useCORS:!0,...rest})}finally{restore()}}async function canvasToBlob(canvas,type="image/png",quality){let blob=await new Promise(resolve=>canvas.toBlob(resolve,type,quality));if(!blob)throw new Error("The image could not be encoded.");return blob}async function elementToImage(element,options={}){let{type,quality,...capture}=options;return canvasToBlob(await captureElement(element,capture),type,quality)}async function pagesToPdf(pages,options={}){let{jsPDF}=await import("jspdf"),doc=new jsPDF({orientation:options.orientation??"portrait",unit:options.unit??"pt",format:options.format??"a4"});return pages.forEach((page,index)=>{index>0&&doc.addPage();let pageW=doc.internal.pageSize.getWidth(),pageH=doc.internal.pageSize.getHeight();page.header&&(doc.setFontSize(page.header.fontSize??12),doc.text(page.header.text,page.header.x,page.header.y));let type=page.imageType??"PNG",data=type==="JPEG"?page.image.toDataURL("image/jpeg",page.quality??.92):page.image.toDataURL("image/png"),placement=page.placement??"fill",x=0,y=0,w=pageW,h=pageH;if(placement!=="fill"){let availW=pageW-placement.left-placement.right,availH=pageH-placement.top-placement.bottom,scale=Math.min(availW/page.image.width,availH/page.image.height);x=placement.left,y=placement.top,w=page.image.width*scale,h=page.image.height*scale}page.compression?doc.addImage(data,type,x,y,w,h,void 0,page.compression):doc.addImage(data,type,x,y,w,h)}),doc.output("blob")}async function elementsToPdf(elements,options={}){let pages=[];for(let element of elements)pages.push({...options.page,image:await captureElement(element,options.capture)});return pagesToPdf(pages,options)}
2
+ //# sourceMappingURL=operate-capture.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/operate-capture.ts","../src/operate/capture.ts"],"sourcesContent":["/** `@ai-matrx/alchemy/operate/capture` — the one DOM capture engine (element → image / PDF). html2canvas and jsPDF load on first use. */\nexport {\n canvasToBlob,\n captureElement,\n elementToImage,\n elementsToPdf,\n pagesToPdf,\n type CaptureOptions,\n type PdfDocumentOptions,\n type PdfMargins,\n type PdfPage,\n} from \"./operate/capture\";\n","/**\n * `@ai-matrx/alchemy/operate/capture` — THE DOM capture engine (ALC-19 T11): an element on screen →\n * an image or a PDF, with nothing else in the app importing html2canvas or jsPDF. Both libraries\n * load on first use (`import()`), so a page that never exports never downloads them.\n *\n * Tailwind 4 colours (oklch/lab/color()) are not parseable by html2canvas, which logs an error per\n * value; `safeColors` substitutes theme-appropriate hex values for the duration of the capture.\n */\n\nexport type CaptureOptions = {\n /** Device-pixel scale of the capture (default 2 — legible when scaled in a deck or print). */\n scale?: number;\n /** Canvas background; null keeps transparency. */\n backgroundColor?: string | null;\n useCORS?: boolean;\n allowTaint?: boolean;\n logging?: boolean;\n width?: number;\n height?: number;\n x?: number;\n y?: number;\n scrollX?: number;\n scrollY?: number;\n windowWidth?: number;\n windowHeight?: number;\n ignoreElements?(element: Element): boolean;\n onclone?(document: Document, element: HTMLElement): void | Promise<void>;\n /** Replace unparseable modern colour functions while capturing (`{ dark }` picks the palette). */\n safeColors?: boolean | { dark: boolean };\n};\n\nconst UNSAFE_COLOR = /\\b(oklch|oklab|lab|lch|color)\\s*\\(/i;\n\nfunction safeColor(value: string, prop: string, dark: boolean): string {\n if (!UNSAFE_COLOR.test(value)) return value;\n const p = prop.toLowerCase();\n if (p.includes(\"background\")) return dark ? \"#1f2937\" : \"#ffffff\";\n if (p === \"color\") return dark ? \"#f3f4f6\" : \"#111827\";\n if (p.includes(\"border\") || p.includes(\"outline\")) return dark ? \"#4b5563\" : \"#d1d5db\";\n if (p === \"fill\" || p === \"stroke\") return dark ? \"#9ca3af\" : \"#374151\";\n return \"#6b7280\";\n}\n\n/** Patch `getComputedStyle` so the capture never reads an unsafe colour; returns the restore. */\nfunction patchComputedStyle(dark: boolean): () => void {\n const original = window.getComputedStyle;\n const bound = original.bind(window);\n window.getComputedStyle = function (elt: Element, pseudo?: string | null) {\n const computed = bound(elt, pseudo);\n return new Proxy(computed, {\n get(target, prop: string | symbol) {\n const value = (target as unknown as Record<string | symbol, unknown>)[prop];\n if (typeof prop === \"string\" && typeof value === \"string\" && value && UNSAFE_COLOR.test(value)) return safeColor(value, prop, dark);\n if (prop === \"getPropertyValue\")\n return (p: string) => {\n const raw = target.getPropertyValue(p);\n return raw && UNSAFE_COLOR.test(raw) ? safeColor(raw, p, dark) : raw;\n };\n if (typeof value === \"function\") return (value as CallableFunction).bind(target);\n return value;\n },\n });\n } as typeof window.getComputedStyle;\n return () => {\n window.getComputedStyle = original;\n };\n}\n\nconst prefersDark = () =>\n typeof document !== \"undefined\" &&\n (document.documentElement.classList.contains(\"dark\") || (typeof window.matchMedia === \"function\" && window.matchMedia(\"(prefers-color-scheme: dark)\").matches));\n\n/** Rasterise a live element to a canvas (what the person sees, at `scale`). */\nexport async function captureElement(element: HTMLElement, options: CaptureOptions = {}): Promise<HTMLCanvasElement> {\n const { safeColors, ...rest } = options;\n const { default: html2canvas } = await import(\"html2canvas\");\n const restore = safeColors ? patchComputedStyle(typeof safeColors === \"object\" ? safeColors.dark : prefersDark()) : () => {};\n try {\n return await html2canvas(element, { scale: 2, logging: false, useCORS: true, ...rest } as Parameters<typeof html2canvas>[1]);\n } finally {\n restore();\n }\n}\n\n/** Encode a canvas (PNG by default; JPEG/WebP take a 0–1 quality). Throws when the browser refuses. */\nexport async function canvasToBlob(canvas: HTMLCanvasElement, type: \"image/png\" | \"image/jpeg\" | \"image/webp\" = \"image/png\", quality?: number): Promise<Blob> {\n const blob = await new Promise<Blob | null>((resolve) => canvas.toBlob(resolve, type, quality));\n if (!blob) throw new Error(\"The image could not be encoded.\");\n return blob;\n}\n\n/** Capture an element straight to an image Blob. */\nexport async function elementToImage(\n element: HTMLElement,\n options: CaptureOptions & { type?: \"image/png\" | \"image/jpeg\" | \"image/webp\"; quality?: number } = {},\n): Promise<Blob> {\n const { type, quality, ...capture } = options;\n return canvasToBlob(await captureElement(element, capture), type, quality);\n}\n\nexport type PdfMargins = { top: number; right: number; bottom: number; left: number };\nexport type PdfPage = {\n image: HTMLCanvasElement;\n /** \"JPEG\" (default 0.92 quality) keeps a flat chart an order of magnitude smaller than a PNG embed. */\n imageType?: \"PNG\" | \"JPEG\";\n quality?: number;\n compression?: \"NONE\" | \"FAST\" | \"MEDIUM\" | \"SLOW\";\n /** \"fill\": the image covers the page. Margins: the image is scaled to fit inside them, never cropped. */\n placement?: \"fill\" | PdfMargins;\n /** A line of text drawn on the page (e.g. the as-of date a chart is true of). */\n header?: { text: string; x: number; y: number; fontSize?: number };\n};\nexport type PdfDocumentOptions = {\n orientation?: \"portrait\" | \"landscape\";\n unit?: \"pt\" | \"mm\" | \"cm\" | \"in\" | \"px\";\n format?: string | [number, number];\n};\n\n/** Lay canvases out as PDF pages (one page each) and answer the PDF Blob. */\nexport async function pagesToPdf(pages: readonly PdfPage[], options: PdfDocumentOptions = {}): Promise<Blob> {\n const { jsPDF } = await import(\"jspdf\");\n const doc = new jsPDF({ orientation: options.orientation ?? \"portrait\", unit: options.unit ?? \"pt\", format: options.format ?? \"a4\" });\n pages.forEach((page, index) => {\n if (index > 0) doc.addPage();\n const pageW = doc.internal.pageSize.getWidth();\n const pageH = doc.internal.pageSize.getHeight();\n if (page.header) {\n doc.setFontSize(page.header.fontSize ?? 12);\n doc.text(page.header.text, page.header.x, page.header.y);\n }\n const type = page.imageType ?? \"PNG\";\n const data = type === \"JPEG\" ? page.image.toDataURL(\"image/jpeg\", page.quality ?? 0.92) : page.image.toDataURL(\"image/png\");\n const placement = page.placement ?? \"fill\";\n let x = 0, y = 0, w = pageW, h = pageH;\n if (placement !== \"fill\") {\n const availW = pageW - placement.left - placement.right;\n const availH = pageH - placement.top - placement.bottom;\n const scale = Math.min(availW / page.image.width, availH / page.image.height);\n x = placement.left;\n y = placement.top;\n w = page.image.width * scale;\n h = page.image.height * scale;\n }\n if (page.compression) doc.addImage(data, type, x, y, w, h, undefined, page.compression);\n else doc.addImage(data, type, x, y, w, h);\n });\n return doc.output(\"blob\");\n}\n\n/** Capture each element and lay them out one per PDF page. */\nexport async function elementsToPdf(\n elements: readonly HTMLElement[],\n options: PdfDocumentOptions & { capture?: CaptureOptions; page?: Omit<PdfPage, \"image\"> } = {},\n): Promise<Blob> {\n const pages: PdfPage[] = [];\n for (const element of elements) pages.push({ ...options.page, image: await captureElement(element, options.capture) });\n return pagesToPdf(pages, options);\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * `@ai-matrx/alchemy/operate/capture` — THE DOM capture engine (ALC-19 T11): an element on screen →
3
+ * an image or a PDF, with nothing else in the app importing html2canvas or jsPDF. Both libraries
4
+ * load on first use (`import()`), so a page that never exports never downloads them.
5
+ *
6
+ * Tailwind 4 colours (oklch/lab/color()) are not parseable by html2canvas, which logs an error per
7
+ * value; `safeColors` substitutes theme-appropriate hex values for the duration of the capture.
8
+ */
9
+ type CaptureOptions = {
10
+ /** Device-pixel scale of the capture (default 2 — legible when scaled in a deck or print). */
11
+ scale?: number;
12
+ /** Canvas background; null keeps transparency. */
13
+ backgroundColor?: string | null;
14
+ useCORS?: boolean;
15
+ allowTaint?: boolean;
16
+ logging?: boolean;
17
+ width?: number;
18
+ height?: number;
19
+ x?: number;
20
+ y?: number;
21
+ scrollX?: number;
22
+ scrollY?: number;
23
+ windowWidth?: number;
24
+ windowHeight?: number;
25
+ ignoreElements?(element: Element): boolean;
26
+ onclone?(document: Document, element: HTMLElement): void | Promise<void>;
27
+ /** Replace unparseable modern colour functions while capturing (`{ dark }` picks the palette). */
28
+ safeColors?: boolean | {
29
+ dark: boolean;
30
+ };
31
+ };
32
+ /** Rasterise a live element to a canvas (what the person sees, at `scale`). */
33
+ declare function captureElement(element: HTMLElement, options?: CaptureOptions): Promise<HTMLCanvasElement>;
34
+ /** Encode a canvas (PNG by default; JPEG/WebP take a 0–1 quality). Throws when the browser refuses. */
35
+ declare function canvasToBlob(canvas: HTMLCanvasElement, type?: "image/png" | "image/jpeg" | "image/webp", quality?: number): Promise<Blob>;
36
+ /** Capture an element straight to an image Blob. */
37
+ declare function elementToImage(element: HTMLElement, options?: CaptureOptions & {
38
+ type?: "image/png" | "image/jpeg" | "image/webp";
39
+ quality?: number;
40
+ }): Promise<Blob>;
41
+ type PdfMargins = {
42
+ top: number;
43
+ right: number;
44
+ bottom: number;
45
+ left: number;
46
+ };
47
+ type PdfPage = {
48
+ image: HTMLCanvasElement;
49
+ /** "JPEG" (default 0.92 quality) keeps a flat chart an order of magnitude smaller than a PNG embed. */
50
+ imageType?: "PNG" | "JPEG";
51
+ quality?: number;
52
+ compression?: "NONE" | "FAST" | "MEDIUM" | "SLOW";
53
+ /** "fill": the image covers the page. Margins: the image is scaled to fit inside them, never cropped. */
54
+ placement?: "fill" | PdfMargins;
55
+ /** A line of text drawn on the page (e.g. the as-of date a chart is true of). */
56
+ header?: {
57
+ text: string;
58
+ x: number;
59
+ y: number;
60
+ fontSize?: number;
61
+ };
62
+ };
63
+ type PdfDocumentOptions = {
64
+ orientation?: "portrait" | "landscape";
65
+ unit?: "pt" | "mm" | "cm" | "in" | "px";
66
+ format?: string | [number, number];
67
+ };
68
+ /** Lay canvases out as PDF pages (one page each) and answer the PDF Blob. */
69
+ declare function pagesToPdf(pages: readonly PdfPage[], options?: PdfDocumentOptions): Promise<Blob>;
70
+ /** Capture each element and lay them out one per PDF page. */
71
+ declare function elementsToPdf(elements: readonly HTMLElement[], options?: PdfDocumentOptions & {
72
+ capture?: CaptureOptions;
73
+ page?: Omit<PdfPage, "image">;
74
+ }): Promise<Blob>;
75
+
76
+ export { type CaptureOptions, type PdfDocumentOptions, type PdfMargins, type PdfPage, canvasToBlob, captureElement, elementToImage, elementsToPdf, pagesToPdf };
@@ -0,0 +1,76 @@
1
+ /**
2
+ * `@ai-matrx/alchemy/operate/capture` — THE DOM capture engine (ALC-19 T11): an element on screen →
3
+ * an image or a PDF, with nothing else in the app importing html2canvas or jsPDF. Both libraries
4
+ * load on first use (`import()`), so a page that never exports never downloads them.
5
+ *
6
+ * Tailwind 4 colours (oklch/lab/color()) are not parseable by html2canvas, which logs an error per
7
+ * value; `safeColors` substitutes theme-appropriate hex values for the duration of the capture.
8
+ */
9
+ type CaptureOptions = {
10
+ /** Device-pixel scale of the capture (default 2 — legible when scaled in a deck or print). */
11
+ scale?: number;
12
+ /** Canvas background; null keeps transparency. */
13
+ backgroundColor?: string | null;
14
+ useCORS?: boolean;
15
+ allowTaint?: boolean;
16
+ logging?: boolean;
17
+ width?: number;
18
+ height?: number;
19
+ x?: number;
20
+ y?: number;
21
+ scrollX?: number;
22
+ scrollY?: number;
23
+ windowWidth?: number;
24
+ windowHeight?: number;
25
+ ignoreElements?(element: Element): boolean;
26
+ onclone?(document: Document, element: HTMLElement): void | Promise<void>;
27
+ /** Replace unparseable modern colour functions while capturing (`{ dark }` picks the palette). */
28
+ safeColors?: boolean | {
29
+ dark: boolean;
30
+ };
31
+ };
32
+ /** Rasterise a live element to a canvas (what the person sees, at `scale`). */
33
+ declare function captureElement(element: HTMLElement, options?: CaptureOptions): Promise<HTMLCanvasElement>;
34
+ /** Encode a canvas (PNG by default; JPEG/WebP take a 0–1 quality). Throws when the browser refuses. */
35
+ declare function canvasToBlob(canvas: HTMLCanvasElement, type?: "image/png" | "image/jpeg" | "image/webp", quality?: number): Promise<Blob>;
36
+ /** Capture an element straight to an image Blob. */
37
+ declare function elementToImage(element: HTMLElement, options?: CaptureOptions & {
38
+ type?: "image/png" | "image/jpeg" | "image/webp";
39
+ quality?: number;
40
+ }): Promise<Blob>;
41
+ type PdfMargins = {
42
+ top: number;
43
+ right: number;
44
+ bottom: number;
45
+ left: number;
46
+ };
47
+ type PdfPage = {
48
+ image: HTMLCanvasElement;
49
+ /** "JPEG" (default 0.92 quality) keeps a flat chart an order of magnitude smaller than a PNG embed. */
50
+ imageType?: "PNG" | "JPEG";
51
+ quality?: number;
52
+ compression?: "NONE" | "FAST" | "MEDIUM" | "SLOW";
53
+ /** "fill": the image covers the page. Margins: the image is scaled to fit inside them, never cropped. */
54
+ placement?: "fill" | PdfMargins;
55
+ /** A line of text drawn on the page (e.g. the as-of date a chart is true of). */
56
+ header?: {
57
+ text: string;
58
+ x: number;
59
+ y: number;
60
+ fontSize?: number;
61
+ };
62
+ };
63
+ type PdfDocumentOptions = {
64
+ orientation?: "portrait" | "landscape";
65
+ unit?: "pt" | "mm" | "cm" | "in" | "px";
66
+ format?: string | [number, number];
67
+ };
68
+ /** Lay canvases out as PDF pages (one page each) and answer the PDF Blob. */
69
+ declare function pagesToPdf(pages: readonly PdfPage[], options?: PdfDocumentOptions): Promise<Blob>;
70
+ /** Capture each element and lay them out one per PDF page. */
71
+ declare function elementsToPdf(elements: readonly HTMLElement[], options?: PdfDocumentOptions & {
72
+ capture?: CaptureOptions;
73
+ page?: Omit<PdfPage, "image">;
74
+ }): Promise<Blob>;
75
+
76
+ export { type CaptureOptions, type PdfDocumentOptions, type PdfMargins, type PdfPage, canvasToBlob, captureElement, elementToImage, elementsToPdf, pagesToPdf };
@@ -0,0 +1,2 @@
1
+ var UNSAFE_COLOR=/\b(oklch|oklab|lab|lch|color)\s*\(/i;function safeColor(value,prop,dark){if(!UNSAFE_COLOR.test(value))return value;let p=prop.toLowerCase();return p.includes("background")?dark?"#1f2937":"#ffffff":p==="color"?dark?"#f3f4f6":"#111827":p.includes("border")||p.includes("outline")?dark?"#4b5563":"#d1d5db":p==="fill"||p==="stroke"?dark?"#9ca3af":"#374151":"#6b7280"}function patchComputedStyle(dark){let original=window.getComputedStyle,bound=original.bind(window);return window.getComputedStyle=function(elt,pseudo){let computed=bound(elt,pseudo);return new Proxy(computed,{get(target,prop){let value=target[prop];return typeof prop=="string"&&typeof value=="string"&&value&&UNSAFE_COLOR.test(value)?safeColor(value,prop,dark):prop==="getPropertyValue"?p=>{let raw=target.getPropertyValue(p);return raw&&UNSAFE_COLOR.test(raw)?safeColor(raw,p,dark):raw}:typeof value=="function"?value.bind(target):value}})},()=>{window.getComputedStyle=original}}var prefersDark=()=>typeof document<"u"&&(document.documentElement.classList.contains("dark")||typeof window.matchMedia=="function"&&window.matchMedia("(prefers-color-scheme: dark)").matches);async function captureElement(element,options={}){let{safeColors,...rest}=options,{default:html2canvas}=await import("html2canvas"),restore=safeColors?patchComputedStyle(typeof safeColors=="object"?safeColors.dark:prefersDark()):()=>{};try{return await html2canvas(element,{scale:2,logging:!1,useCORS:!0,...rest})}finally{restore()}}async function canvasToBlob(canvas,type="image/png",quality){let blob=await new Promise(resolve=>canvas.toBlob(resolve,type,quality));if(!blob)throw new Error("The image could not be encoded.");return blob}async function elementToImage(element,options={}){let{type,quality,...capture}=options;return canvasToBlob(await captureElement(element,capture),type,quality)}async function pagesToPdf(pages,options={}){let{jsPDF}=await import("jspdf"),doc=new jsPDF({orientation:options.orientation??"portrait",unit:options.unit??"pt",format:options.format??"a4"});return pages.forEach((page,index)=>{index>0&&doc.addPage();let pageW=doc.internal.pageSize.getWidth(),pageH=doc.internal.pageSize.getHeight();page.header&&(doc.setFontSize(page.header.fontSize??12),doc.text(page.header.text,page.header.x,page.header.y));let type=page.imageType??"PNG",data=type==="JPEG"?page.image.toDataURL("image/jpeg",page.quality??.92):page.image.toDataURL("image/png"),placement=page.placement??"fill",x=0,y=0,w=pageW,h=pageH;if(placement!=="fill"){let availW=pageW-placement.left-placement.right,availH=pageH-placement.top-placement.bottom,scale=Math.min(availW/page.image.width,availH/page.image.height);x=placement.left,y=placement.top,w=page.image.width*scale,h=page.image.height*scale}page.compression?doc.addImage(data,type,x,y,w,h,void 0,page.compression):doc.addImage(data,type,x,y,w,h)}),doc.output("blob")}async function elementsToPdf(elements,options={}){let pages=[];for(let element of elements)pages.push({...options.page,image:await captureElement(element,options.capture)});return pagesToPdf(pages,options)}export{canvasToBlob,captureElement,elementToImage,elementsToPdf,pagesToPdf};
2
+ //# sourceMappingURL=operate-capture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/operate/capture.ts"],"sourcesContent":["/**\n * `@ai-matrx/alchemy/operate/capture` — THE DOM capture engine (ALC-19 T11): an element on screen →\n * an image or a PDF, with nothing else in the app importing html2canvas or jsPDF. Both libraries\n * load on first use (`import()`), so a page that never exports never downloads them.\n *\n * Tailwind 4 colours (oklch/lab/color()) are not parseable by html2canvas, which logs an error per\n * value; `safeColors` substitutes theme-appropriate hex values for the duration of the capture.\n */\n\nexport type CaptureOptions = {\n /** Device-pixel scale of the capture (default 2 — legible when scaled in a deck or print). */\n scale?: number;\n /** Canvas background; null keeps transparency. */\n backgroundColor?: string | null;\n useCORS?: boolean;\n allowTaint?: boolean;\n logging?: boolean;\n width?: number;\n height?: number;\n x?: number;\n y?: number;\n scrollX?: number;\n scrollY?: number;\n windowWidth?: number;\n windowHeight?: number;\n ignoreElements?(element: Element): boolean;\n onclone?(document: Document, element: HTMLElement): void | Promise<void>;\n /** Replace unparseable modern colour functions while capturing (`{ dark }` picks the palette). */\n safeColors?: boolean | { dark: boolean };\n};\n\nconst UNSAFE_COLOR = /\\b(oklch|oklab|lab|lch|color)\\s*\\(/i;\n\nfunction safeColor(value: string, prop: string, dark: boolean): string {\n if (!UNSAFE_COLOR.test(value)) return value;\n const p = prop.toLowerCase();\n if (p.includes(\"background\")) return dark ? \"#1f2937\" : \"#ffffff\";\n if (p === \"color\") return dark ? \"#f3f4f6\" : \"#111827\";\n if (p.includes(\"border\") || p.includes(\"outline\")) return dark ? \"#4b5563\" : \"#d1d5db\";\n if (p === \"fill\" || p === \"stroke\") return dark ? \"#9ca3af\" : \"#374151\";\n return \"#6b7280\";\n}\n\n/** Patch `getComputedStyle` so the capture never reads an unsafe colour; returns the restore. */\nfunction patchComputedStyle(dark: boolean): () => void {\n const original = window.getComputedStyle;\n const bound = original.bind(window);\n window.getComputedStyle = function (elt: Element, pseudo?: string | null) {\n const computed = bound(elt, pseudo);\n return new Proxy(computed, {\n get(target, prop: string | symbol) {\n const value = (target as unknown as Record<string | symbol, unknown>)[prop];\n if (typeof prop === \"string\" && typeof value === \"string\" && value && UNSAFE_COLOR.test(value)) return safeColor(value, prop, dark);\n if (prop === \"getPropertyValue\")\n return (p: string) => {\n const raw = target.getPropertyValue(p);\n return raw && UNSAFE_COLOR.test(raw) ? safeColor(raw, p, dark) : raw;\n };\n if (typeof value === \"function\") return (value as CallableFunction).bind(target);\n return value;\n },\n });\n } as typeof window.getComputedStyle;\n return () => {\n window.getComputedStyle = original;\n };\n}\n\nconst prefersDark = () =>\n typeof document !== \"undefined\" &&\n (document.documentElement.classList.contains(\"dark\") || (typeof window.matchMedia === \"function\" && window.matchMedia(\"(prefers-color-scheme: dark)\").matches));\n\n/** Rasterise a live element to a canvas (what the person sees, at `scale`). */\nexport async function captureElement(element: HTMLElement, options: CaptureOptions = {}): Promise<HTMLCanvasElement> {\n const { safeColors, ...rest } = options;\n const { default: html2canvas } = await import(\"html2canvas\");\n const restore = safeColors ? patchComputedStyle(typeof safeColors === \"object\" ? safeColors.dark : prefersDark()) : () => {};\n try {\n return await html2canvas(element, { scale: 2, logging: false, useCORS: true, ...rest } as Parameters<typeof html2canvas>[1]);\n } finally {\n restore();\n }\n}\n\n/** Encode a canvas (PNG by default; JPEG/WebP take a 0–1 quality). Throws when the browser refuses. */\nexport async function canvasToBlob(canvas: HTMLCanvasElement, type: \"image/png\" | \"image/jpeg\" | \"image/webp\" = \"image/png\", quality?: number): Promise<Blob> {\n const blob = await new Promise<Blob | null>((resolve) => canvas.toBlob(resolve, type, quality));\n if (!blob) throw new Error(\"The image could not be encoded.\");\n return blob;\n}\n\n/** Capture an element straight to an image Blob. */\nexport async function elementToImage(\n element: HTMLElement,\n options: CaptureOptions & { type?: \"image/png\" | \"image/jpeg\" | \"image/webp\"; quality?: number } = {},\n): Promise<Blob> {\n const { type, quality, ...capture } = options;\n return canvasToBlob(await captureElement(element, capture), type, quality);\n}\n\nexport type PdfMargins = { top: number; right: number; bottom: number; left: number };\nexport type PdfPage = {\n image: HTMLCanvasElement;\n /** \"JPEG\" (default 0.92 quality) keeps a flat chart an order of magnitude smaller than a PNG embed. */\n imageType?: \"PNG\" | \"JPEG\";\n quality?: number;\n compression?: \"NONE\" | \"FAST\" | \"MEDIUM\" | \"SLOW\";\n /** \"fill\": the image covers the page. Margins: the image is scaled to fit inside them, never cropped. */\n placement?: \"fill\" | PdfMargins;\n /** A line of text drawn on the page (e.g. the as-of date a chart is true of). */\n header?: { text: string; x: number; y: number; fontSize?: number };\n};\nexport type PdfDocumentOptions = {\n orientation?: \"portrait\" | \"landscape\";\n unit?: \"pt\" | \"mm\" | \"cm\" | \"in\" | \"px\";\n format?: string | [number, number];\n};\n\n/** Lay canvases out as PDF pages (one page each) and answer the PDF Blob. */\nexport async function pagesToPdf(pages: readonly PdfPage[], options: PdfDocumentOptions = {}): Promise<Blob> {\n const { jsPDF } = await import(\"jspdf\");\n const doc = new jsPDF({ orientation: options.orientation ?? \"portrait\", unit: options.unit ?? \"pt\", format: options.format ?? \"a4\" });\n pages.forEach((page, index) => {\n if (index > 0) doc.addPage();\n const pageW = doc.internal.pageSize.getWidth();\n const pageH = doc.internal.pageSize.getHeight();\n if (page.header) {\n doc.setFontSize(page.header.fontSize ?? 12);\n doc.text(page.header.text, page.header.x, page.header.y);\n }\n const type = page.imageType ?? \"PNG\";\n const data = type === \"JPEG\" ? page.image.toDataURL(\"image/jpeg\", page.quality ?? 0.92) : page.image.toDataURL(\"image/png\");\n const placement = page.placement ?? \"fill\";\n let x = 0, y = 0, w = pageW, h = pageH;\n if (placement !== \"fill\") {\n const availW = pageW - placement.left - placement.right;\n const availH = pageH - placement.top - placement.bottom;\n const scale = Math.min(availW / page.image.width, availH / page.image.height);\n x = placement.left;\n y = placement.top;\n w = page.image.width * scale;\n h = page.image.height * scale;\n }\n if (page.compression) doc.addImage(data, type, x, y, w, h, undefined, page.compression);\n else doc.addImage(data, type, x, y, w, h);\n });\n return doc.output(\"blob\");\n}\n\n/** Capture each element and lay them out one per PDF page. */\nexport async function elementsToPdf(\n elements: readonly HTMLElement[],\n options: PdfDocumentOptions & { capture?: CaptureOptions; page?: Omit<PdfPage, \"image\"> } = {},\n): Promise<Blob> {\n const pages: PdfPage[] = [];\n for (const element of elements) pages.push({ ...options.page, image: await captureElement(element, options.capture) });\n return pagesToPdf(pages, options);\n}\n"],"mappings":"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","names":[]}
@@ -1,4 +1,4 @@
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_document_exports={};__export(operate_document_exports,{documentEngine:()=>documentEngine,documentHtmlEngine:()=>documentHtmlEngine});module.exports=__toCommonJS(operate_document_exports);var import_json_format=require("@ai-matrx/kit/json-format"),import_html_escape=require("@ai-matrx/kit/html-escape"),import_transfer_json=require("@ai-matrx/kit/transfer-json"),import_transfer_json2=require("@ai-matrx/kit/transfer-json");function fencedCodeBlock(text,language){let runs=text.match(/`+/g)??[],fence="`".repeat(Math.max(3,...runs.map(run=>run.length+1)));return`${fence}${language}
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_document_exports={};__export(operate_document_exports,{documentEngine:()=>documentEngine,documentHtmlEngine:()=>documentHtmlEngine});module.exports=__toCommonJS(operate_document_exports);var import_download=require("@ai-matrx/kit/download"),import_json_format=require("@ai-matrx/kit/json-format"),import_html_escape=require("@ai-matrx/kit/html-escape"),import_transfer_json=require("@ai-matrx/kit/transfer-json"),import_transfer_json2=require("@ai-matrx/kit/transfer-json");function fencedCodeBlock(text,language){let runs=text.match(/`+/g)??[],fence="`".repeat(Math.max(3,...runs.map(run=>run.length+1)));return`${fence}${language}
2
2
  ${text}${text.endsWith(`
3
3
  `)?"":`
4
4
  `}${fence}`}function deepFreeze(v){if(ArrayBuffer.isView(v))return v;if(v&&typeof v=="object"&&!Object.isFrozen(v)){Object.freeze(v);for(let x of Object.values(v))deepFreeze(x)}return v}function sealTransferArtifact(artifact){if(!artifact.file)return deepFreeze({...artifact,omissions:artifact.omissions.map(o=>({...o}))});let bytes=artifact.file.bytes.slice(),file={filename:artifact.file.filename,mime:artifact.file.mime,get bytes(){return bytes.slice()}};return deepFreeze({...artifact,file,omissions:artifact.omissions.map(o=>({...o}))})}var ENGINE=Symbol.for("ai-matrx.alchemy.operate.markdown-html-engine");async function loadMarkdownHtmlEngine(){let g=globalThis;return g[ENGINE]??=import("@ai-matrx/print/markdown-html").then(m=>m.createMarkdownHtmlEngine()),g[ENGINE]}var enc=new TextEncoder;function markdownOf(draft){let p=draft.payload;if(p.kind==="markdown")return p.text;if(p.kind==="text")return p.language?fencedCodeBlock(p.text,p.language):p.text;throw new import_transfer_json2.ContentTransferError("unsupported-format","A document needs text or Markdown content. Download it as JSON or CSV instead.")}var stem=filename=>filename?filename.replace(/\.[a-z0-9]+$/i,""):void 0,documentHtmlEngine={async build(draft,options,signal){if(signal.aborted)throw new DOMException("Cancelled","AbortError");let engine=await loadMarkdownHtmlEngine(),tree=await engine.parse(markdownOf(draft),{signal,...options.onNotice?{onNotice:options.onNotice}:{},...options.assets?{assets:options.assets}:{}}),page=engine.document(tree);return sealTransferArtifact({snapshotId:draft.snapshotId,draftRevision:draft.revision,format:"html",plainText:engine.plainText(tree),html:page,file:{filename:options.filename??"export.html",mime:"text/html;charset=utf-8",bytes:enc.encode(page)},omissions:draft.omissions.map(o=>({...o}))})}};function documentEngine(format){return{async build(draft,options,signal){if(signal.aborted)throw new DOMException("Cancelled","AbortError");let source=markdownOf(draft),{exportDocument}=await import("@ai-matrx/print/document"),name=stem(options.filename),out=await exportDocument(source,format,{...name?{fileName:name}:{},...options.onNotice?{onNotice:n=>options.onNotice?.({code:n.code,sentence:n.message,remedy:n.remedy})}:{}});if(signal.aborted)throw new DOMException("Cancelled","AbortError");return sealTransferArtifact({snapshotId:draft.snapshotId,draftRevision:draft.revision,format,plainText:source,file:{filename:out.fileName,mime:out.mime,bytes:out.bytes},omissions:draft.omissions.map(o=>({...o}))})}}}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/operate-document.ts","../src/operate/core.ts","../src/operate/fenced-code.ts","../src/operate/formats/rich-html.ts","../src/operate/formats/document.ts"],"sourcesContent":["export { documentEngine, documentHtmlEngine } from \"./operate/formats/document\";\n","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":"y5BAAA,uMCAA,uBAA8B,qCAC9B,mBAA2B,qCAC3B,qBAKO,uCACPA,sBAIO,uCCNA,SAAS,gBAAgB,KAAc,SAA0B,CACtE,IAAM,KAAO,KAAK,MAAM,KAAK,GAAK,CAAC,EAC7B,MAAQ,IAAI,OAAO,KAAK,IAAI,EAAG,GAAG,KAAK,IAAK,KAAQ,IAAI,OAAS,CAAC,CAAC,CAAC,EAC1E,MAAO,GAAG,KAAK,GAAG,QAAQ;AAAA,EAAK,IAAI,GAAG,KAAK,SAAS;AAAA,CAAI,EAAI,GAAK;AAAA,CAAI,GAAG,KAAK,EAC/E,CD6EA,SAAS,WAAc,EAAS,CAC9B,GAAI,YAAY,OAAO,CAAC,EAAG,OAAO,EAClC,GAAI,GAAK,OAAO,GAAM,UAAY,CAAC,OAAO,SAAS,CAAC,EAAG,CACrD,OAAO,OAAO,CAAC,EACf,QAAW,KAAK,OAAO,OAAO,CAAW,EAAG,WAAW,CAAC,CAC1D,CACA,OAAO,CACT,CA+yBO,SAAS,qBAAqB,SAA8B,CACjE,GAAI,CAAC,SAAS,KACZ,OAAO,WAAW,CAChB,GAAG,SACH,UAAW,SAAS,UAAU,IAAK,IAAO,CAAE,GAAG,CAAE,EAAE,CACrD,CAAC,EACH,IAAM,MAAQ,SAAS,KAAK,MAAM,MAAM,EAClC,KAAO,CACX,SAAU,SAAS,KAAK,SACxB,KAAM,SAAS,KAAK,KACpB,IAAI,OAAQ,CACV,OAAO,MAAM,MAAM,CACrB,CACF,EACA,OAAO,WAAW,CAChB,GAAG,SACH,KACA,UAAW,SAAS,UAAU,IAAK,IAAO,CAAE,GAAG,CAAE,EAAE,CACrD,CAAC,CACH,CEj5BA,IAAM,OAAS,OAAO,IAAI,+CAA+C,EAGzE,eAAsB,wBAAsD,CAC1E,IAAM,EAAI,WACV,SAAE,MAAM,IAAM,OAAO,+BAA+B,EAAE,KAAM,GAAM,EAAE,yBAAyB,CAAC,EACvF,EAAE,MAAM,CACjB,CCPA,IAAM,IAAM,IAAI,YAEhB,SAAS,WAAW,MAAsB,CACxC,IAAM,EAAI,MAAM,QAChB,GAAI,EAAE,OAAS,WAAY,OAAO,EAAE,KACpC,GAAI,EAAE,OAAS,OACb,OAAK,EAAE,SACA,gBAAgB,EAAE,KAAM,EAAE,QAAQ,EADjB,EAAE,KAG5B,MAAM,IAAI,2CAAqB,qBAAsB,gFAAgF,CACvI,CAEA,IAAM,KAAQ,UAAkC,SAAW,SAAS,QAAQ,gBAAiB,EAAE,EAAI,OAGtF,mBAAmC,CAC9C,MAAM,MAAM,MAAc,QAAwB,OAAwC,CACxF,GAAI,OAAO,QAAS,MAAM,IAAI,aAAa,YAAa,YAAY,EACpE,IAAM,OAAS,MAAM,uBAAuB,EACtC,KAAO,MAAM,OAAO,MAAM,WAAW,KAAK,EAAG,CACjD,OACA,GAAI,QAAQ,SAAW,CAAE,SAAU,QAAQ,QAAS,EAAI,CAAC,EACzD,GAAI,QAAQ,OAAS,CAAE,OAAQ,QAAQ,MAAO,EAAI,CAAC,CACrD,CAAC,EACK,KAAO,OAAO,SAAS,IAAI,EACjC,OAAO,qBAAqB,CAC1B,WAAY,MAAM,WAClB,cAAe,MAAM,SACrB,OAAQ,OACR,UAAW,OAAO,UAAU,IAAI,EAChC,KAAM,KACN,KAAM,CAAE,SAAU,QAAQ,UAAY,cAAe,KAAM,0BAA2B,MAAO,IAAI,OAAO,IAAI,CAAE,EAC9G,UAAW,MAAM,UAAU,IAAK,IAAO,CAAE,GAAG,CAAE,EAAE,CAClD,CAAC,CACH,CACF,EAGO,SAAS,eAAe,OAA+C,CAC5E,MAAO,CACL,MAAM,MAAM,MAAc,QAAwB,OAAwC,CACxF,GAAI,OAAO,QAAS,MAAM,IAAI,aAAa,YAAa,YAAY,EACpE,IAAM,OAAS,WAAW,KAAK,EACzB,CAAE,cAAe,EAAI,KAAM,QAAO,0BAA0B,EAC5D,KAAO,KAAK,QAAQ,QAAQ,EAC5B,IAAM,MAAM,eAAe,OAAQ,OAAQ,CAC/C,GAAI,KAAO,CAAE,SAAU,IAAK,EAAI,CAAC,EACjC,GAAI,QAAQ,SAAW,CAAE,SAAW,GAAM,QAAQ,WAAW,CAAE,KAAM,EAAE,KAAM,SAAU,EAAE,QAAS,OAAQ,EAAE,MAAO,CAAC,CAAE,EAAI,CAAC,CAC7H,CAAC,EACD,GAAI,OAAO,QAAS,MAAM,IAAI,aAAa,YAAa,YAAY,EACpE,OAAO,qBAAqB,CAC1B,WAAY,MAAM,WAClB,cAAe,MAAM,SACrB,OACA,UAAW,OACX,KAAM,CAAE,SAAU,IAAI,SAAU,KAAM,IAAI,KAAM,MAAO,IAAI,KAAM,EACjE,UAAW,MAAM,UAAU,IAAK,IAAO,CAAE,GAAG,CAAE,EAAE,CAClD,CAAC,CACH,CACF,CACF","names":["import_transfer_json"]}
1
+ {"version":3,"sources":["../src/operate-document.ts","../src/operate/core.ts","../src/operate/fenced-code.ts","../src/operate/formats/rich-html.ts","../src/operate/formats/document.ts"],"sourcesContent":["export { documentEngine, documentHtmlEngine } from \"./operate/formats/document\";\n","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":["import_transfer_json"]}
@@ -1,4 +1,4 @@
1
- import{stringifyJson}from"@ai-matrx/kit/json-format";import{escapeHtml}from"@ai-matrx/kit/html-escape";import{ContentTransferError,cloneTransferJson,normalizeTransferJsonAtPath}from"@ai-matrx/kit/transfer-json";import{ContentTransferError as ContentTransferError2,normalizeTransferJson as normalizeTransferJson2}from"@ai-matrx/kit/transfer-json";function fencedCodeBlock(text,language){let runs=text.match(/`+/g)??[],fence="`".repeat(Math.max(3,...runs.map(run=>run.length+1)));return`${fence}${language}
1
+ import{downloadFile}from"@ai-matrx/kit/download";import{stringifyJson}from"@ai-matrx/kit/json-format";import{escapeHtml}from"@ai-matrx/kit/html-escape";import{ContentTransferError,cloneTransferJson,normalizeTransferJsonAtPath}from"@ai-matrx/kit/transfer-json";import{ContentTransferError as ContentTransferError2,normalizeTransferJson as normalizeTransferJson2}from"@ai-matrx/kit/transfer-json";function fencedCodeBlock(text,language){let runs=text.match(/`+/g)??[],fence="`".repeat(Math.max(3,...runs.map(run=>run.length+1)));return`${fence}${language}
2
2
  ${text}${text.endsWith(`
3
3
  `)?"":`
4
4
  `}${fence}`}function deepFreeze(v){if(ArrayBuffer.isView(v))return v;if(v&&typeof v=="object"&&!Object.isFrozen(v)){Object.freeze(v);for(let x of Object.values(v))deepFreeze(x)}return v}function sealTransferArtifact(artifact){if(!artifact.file)return deepFreeze({...artifact,omissions:artifact.omissions.map(o=>({...o}))});let bytes=artifact.file.bytes.slice(),file={filename:artifact.file.filename,mime:artifact.file.mime,get bytes(){return bytes.slice()}};return deepFreeze({...artifact,file,omissions:artifact.omissions.map(o=>({...o}))})}var ENGINE=Symbol.for("ai-matrx.alchemy.operate.markdown-html-engine");async function loadMarkdownHtmlEngine(){let g=globalThis;return g[ENGINE]??=import("@ai-matrx/print/markdown-html").then(m=>m.createMarkdownHtmlEngine()),g[ENGINE]}var enc=new TextEncoder;function markdownOf(draft){let p=draft.payload;if(p.kind==="markdown")return p.text;if(p.kind==="text")return p.language?fencedCodeBlock(p.text,p.language):p.text;throw new ContentTransferError2("unsupported-format","A document needs text or Markdown content. Download it as JSON or CSV instead.")}var stem=filename=>filename?filename.replace(/\.[a-z0-9]+$/i,""):void 0,documentHtmlEngine={async build(draft,options,signal){if(signal.aborted)throw new DOMException("Cancelled","AbortError");let engine=await loadMarkdownHtmlEngine(),tree=await engine.parse(markdownOf(draft),{signal,...options.onNotice?{onNotice:options.onNotice}:{},...options.assets?{assets:options.assets}:{}}),page=engine.document(tree);return sealTransferArtifact({snapshotId:draft.snapshotId,draftRevision:draft.revision,format:"html",plainText:engine.plainText(tree),html:page,file:{filename:options.filename??"export.html",mime:"text/html;charset=utf-8",bytes:enc.encode(page)},omissions:draft.omissions.map(o=>({...o}))})}};function documentEngine(format){return{async build(draft,options,signal){if(signal.aborted)throw new DOMException("Cancelled","AbortError");let source=markdownOf(draft),{exportDocument}=await import("@ai-matrx/print/document"),name=stem(options.filename),out=await exportDocument(source,format,{...name?{fileName:name}:{},...options.onNotice?{onNotice:n=>options.onNotice?.({code:n.code,sentence:n.message,remedy:n.remedy})}:{}});if(signal.aborted)throw new DOMException("Cancelled","AbortError");return sealTransferArtifact({snapshotId:draft.snapshotId,draftRevision:draft.revision,format,plainText:source,file:{filename:out.fileName,mime:out.mime,bytes:out.bytes},omissions:draft.omissions.map(o=>({...o}))})}}}export{documentEngine,documentHtmlEngine};