@wenathlan/extension 1.1.37 → 1.1.38

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.
@@ -520,21 +520,122 @@ var sessionmemory = class {
520
520
  async getcodevalue() {
521
521
  return this.adapter.get("codevalue");
522
522
  }
523
+ /** Stores one dataset with its column specs and rows, replacing the previous record of that id. */
524
+ async setdataset(value) {
525
+ await this.adapter.set(`dataset${value.id}`, value);
526
+ const ids = (await this.adapter.get("datasets") ?? []).filter((id) => id !== value.id);
527
+ await this.adapter.set("datasets", [value.id, ...ids]);
528
+ }
529
+ /** Returns one stored dataset by its id with its column specs and row count. */
530
+ async getdataset(id) {
531
+ return this.adapter.get(`dataset${id}`);
532
+ }
533
+ /** Returns every stored dataset id, newest first. */
534
+ async getdatasets() {
535
+ const ids = await this.adapter.get("datasets") ?? [];
536
+ const records = [];
537
+ for (const id of ids) {
538
+ const record2 = await this.adapter.get(`dataset${id}`);
539
+ if (record2) records.push(record2);
540
+ }
541
+ return records;
542
+ }
543
+ /** Stores one imported csv dataset for fill loops beside the dataset store. */
544
+ async addimport(value) {
545
+ await this.setdataset(value);
546
+ const ids = (await this.adapter.get("imports") ?? []).filter((id) => id !== value.id);
547
+ await this.adapter.set("imports", [value.id, ...ids]);
548
+ }
549
+ /** Returns every imported csv dataset for fill loops, newest first. */
550
+ async getimports() {
551
+ const ids = await this.adapter.get("imports") ?? [];
552
+ const records = [];
553
+ for (const id of ids) {
554
+ const record2 = await this.adapter.get(`dataset${id}`);
555
+ if (record2) records.push(record2);
556
+ }
557
+ return records;
558
+ }
559
+ /** Stores one extraction session with its cursor and page history, replacing the previous session of that id. */
560
+ async setextractsession(value) {
561
+ await this.adapter.set(`extract${value.id}`, value);
562
+ const ids = (await this.adapter.get("extracts") ?? []).filter((id) => id !== value.id);
563
+ await this.adapter.set("extracts", [value.id, ...ids]);
564
+ }
565
+ /** Returns every extraction session with its cursor and page history, newest first. */
566
+ async getextractsessions() {
567
+ const ids = await this.adapter.get("extracts") ?? [];
568
+ const records = [];
569
+ for (const id of ids) {
570
+ const record2 = await this.adapter.get(`extract${id}`);
571
+ if (record2) records.push(record2);
572
+ }
573
+ return records;
574
+ }
575
+ /** Records one provenance record of an exported artifact. */
576
+ async addprovenance(record2) {
577
+ const records = await this.getprovenances();
578
+ await this.adapter.set("provenances", [record2, ...records]);
579
+ }
580
+ /** Returns every provenance record per exported artifact, newest first. */
581
+ async getprovenances() {
582
+ return await this.adapter.get("provenances") ?? [];
583
+ }
584
+ /** Stores the transform rules and dedupe keys of one task, replacing the previous record of that task. */
585
+ async settaskrules(value) {
586
+ const records = (await this.adapter.get("taskrules") ?? []).filter((item) => item.taskid !== value.taskid);
587
+ await this.adapter.set("taskrules", [value, ...records]);
588
+ }
589
+ /** Returns the transform rules and dedupe keys per task, newest first. */
590
+ async gettaskrules() {
591
+ return await this.adapter.get("taskrules") ?? [];
592
+ }
593
+ /** Stores one stream chunk state for resume, replacing the previous state of that dataset. */
594
+ async setstream(state) {
595
+ const records = (await this.adapter.get("streams") ?? []).filter((item) => item.datasetid !== state.datasetid);
596
+ await this.adapter.set("streams", [state, ...records]);
597
+ }
598
+ /** Returns every stream chunk state persisted for resume, newest first. */
599
+ async getstreams() {
600
+ return await this.adapter.get("streams") ?? [];
601
+ }
602
+ /** Stores one reviewed sheet endpoint config behind its origin grant, replacing the previous config of that origin. */
603
+ async setsheetendpoint(config) {
604
+ const records = (await this.adapter.get("sheetendpoints") ?? []).filter((item) => item.origin !== config.origin);
605
+ await this.adapter.set("sheetendpoints", [config, ...records]);
606
+ }
607
+ /** Returns every reviewed sheet endpoint config, newest first. */
608
+ async getsheetendpoints() {
609
+ return await this.adapter.get("sheetendpoints") ?? [];
610
+ }
611
+ /** Records one exported data artifact; artifact retention is a user setting and an absent setting keeps every artifact. */
612
+ async addexport(artifact) {
613
+ const records = await this.getexports();
614
+ const combined = [artifact, ...records.filter((item) => item.id !== artifact.id)];
615
+ const retention = (await this.getsettings())?.artifactretention;
616
+ await this.adapter.set("exports", retention === void 0 ? combined : combined.slice(0, retention));
617
+ }
618
+ /** Returns every exported data artifact with its content and checksum, newest first. */
619
+ async getexports() {
620
+ return await this.adapter.get("exports") ?? [];
621
+ }
523
622
  };
524
623
  function randomid() {
525
624
  return crypto.randomUUID();
526
625
  }
527
626
 
528
627
  // policy.ts
529
- var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate", "select", "presskey", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "reload", "back", "forward", "writestorage", "setattribute", "removeattribute", "evaluate", "tabcreate", "tabactivate", "tabclose", "tabreload", "windowcreate", "windowclose", "windowresize", "downloadfile", "clickpoint", "shiftclick", "dismissdialog", "enterframe", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "openlink", "openprivate", "reloadcache", "stopnav", "followlink", "spanav", "rewritequery", "setfragment", "navlist", "navprofile", "handleauth", "printpdf", "prefetch", "preconnect", "deeplink", "reopentab", "pausenav", "navrate", "openclipboard", "batchopen", "duplicatetab", "closepattern", "pintab", "mutetab", "movetab", "movetabwindow", "grouptabs", "colorgroup", "collapsegroup", "discardtab", "reloadtabs", "zoomin", "zoomout", "switchtab", "maximizewindow", "minimizewindow", "restorewindow", "focuswindow", "scratchwindow", "incognitowindow", "restoretab", "restorelayout", "reopenrun", "badgetab", "fillform", "filllabel", "fillplaceholder", "submitform", "retryform", "runwizard", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcard", "fillcode", "consentpassword"]);
628
+ var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate", "select", "presskey", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "reload", "back", "forward", "writestorage", "setattribute", "removeattribute", "evaluate", "tabcreate", "tabactivate", "tabclose", "tabreload", "windowcreate", "windowclose", "windowresize", "downloadfile", "clickpoint", "shiftclick", "dismissdialog", "enterframe", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "openlink", "openprivate", "reloadcache", "stopnav", "followlink", "spanav", "rewritequery", "setfragment", "navlist", "navprofile", "handleauth", "printpdf", "prefetch", "preconnect", "deeplink", "reopentab", "pausenav", "navrate", "openclipboard", "batchopen", "duplicatetab", "closepattern", "pintab", "mutetab", "movetab", "movetabwindow", "grouptabs", "colorgroup", "collapsegroup", "discardtab", "reloadtabs", "zoomin", "zoomout", "switchtab", "maximizewindow", "minimizewindow", "restorewindow", "focuswindow", "scratchwindow", "incognitowindow", "restoretab", "restorelayout", "reopenrun", "badgetab", "fillform", "filllabel", "fillplaceholder", "submitform", "retryform", "runwizard", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcard", "fillcode", "consentpassword", "exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "streamdisk", "paginateextract", "resumeextract"]);
530
629
  var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover", "clickdeep", "rightclick", "doubleclick", "scrollpage", "scrollby", "scrollend", "scrolltop", "fullscreen", "zoomset", "movepointer", "clicktext", "clickaria", "clickname", "expanddetails", "pierceshadow", "retryaction"]);
531
- var readactions = /* @__PURE__ */ new Set(["observe", "inspect", "extract", "wait", "waitfor", "waittext", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "highlight", "tablist", "windowlist", "tabsnapshot", "mapclicks", "verifyvisible", "verifyenabled", "resolvexpath", "a11ytree", "readvisible", "readertree", "detectlists", "detecttables", "readjson", "watchmutate", "waitquiet", "watchbanner", "detectinfinitescroll", "detectvirtual", "detectlazy", "readscrollpos", "readlang", "readoutline", "countpages", "listshadow", "listframes", "classifypage", "fingerprintsection", "diffsnapshots", "readselection", "watchfocus", "detectsticky", "detectscrolllock", "readopengraph", "detectlanguage", "deriveselector", "waitload", "waiturl", "spawait", "detecthttp", "readredirects", "readfinalurl", "trailaudit", "navintent", "checksafe", "querytabs", "watchtab", "findclones", "searchtabs", "listaudio", "snapshotsession", "savelayout", "attachmeta", "detectfields", "generatevalues", "saveprofiles", "asksubmit", "readerrors", "skiphoneypot", "detectlogin", "detecttemplate", "handoffcaptcha"]);
630
+ var readactions = /* @__PURE__ */ new Set(["observe", "inspect", "extract", "wait", "waitfor", "waittext", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "highlight", "tablist", "windowlist", "tabsnapshot", "mapclicks", "verifyvisible", "verifyenabled", "resolvexpath", "a11ytree", "readvisible", "readertree", "detectlists", "detecttables", "readjson", "watchmutate", "waitquiet", "watchbanner", "detectinfinitescroll", "detectvirtual", "detectlazy", "readscrollpos", "readlang", "readoutline", "countpages", "listshadow", "listframes", "classifypage", "fingerprintsection", "diffsnapshots", "readselection", "watchfocus", "detectsticky", "detectscrolllock", "readopengraph", "detectlanguage", "deriveselector", "waitload", "waiturl", "spawait", "detecthttp", "readredirects", "readfinalurl", "trailaudit", "navintent", "checksafe", "querytabs", "watchtab", "findclones", "searchtabs", "listaudio", "snapshotsession", "savelayout", "attachmeta", "detectfields", "generatevalues", "saveprofiles", "asksubmit", "readerrors", "skiphoneypot", "detectlogin", "detecttemplate", "handoffcaptcha", "scrapetable", "importcsv", "looprows", "transformvalues", "deduperows", "mergepages", "stamplerows", "previewgrid", "logprovenance"]);
532
631
  var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
533
632
  var watchactions = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus", "watchtab"]);
534
- var targetactions = /* @__PURE__ */ new Set(["inspect", "focus", "click", "type", "scroll", "select", "hover", "clickdeep", "rightclick", "doubleclick", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "highlight", "setattribute", "removeattribute", "waitfor", "shiftclick", "typetime", "appendtext", "setvalue", "typeedit", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "expanddetails", "verifyvisible", "verifyenabled", "pierceshadow", "deriveselector", "fingerprintsection", "submitform", "retryform", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcode", "consentpassword"]);
633
+ var targetactions = /* @__PURE__ */ new Set(["inspect", "focus", "click", "type", "scroll", "select", "hover", "clickdeep", "rightclick", "doubleclick", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "highlight", "setattribute", "removeattribute", "waitfor", "shiftclick", "typetime", "appendtext", "setvalue", "typeedit", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "expanddetails", "verifyvisible", "verifyenabled", "pierceshadow", "deriveselector", "fingerprintsection", "submitform", "retryform", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcode", "consentpassword", "scrapetable", "paginateextract"]);
535
634
  var valueactions = /* @__PURE__ */ new Set(["presskey", "drag", "drop", "upload", "readattribute", "removeattribute", "waittext", "evaluate", "zoomset", "tabactivate", "tabclose", "tabreload", "windowclose", "windowresize", "tabcreate", "windowcreate", "downloadfile", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "chooseradio", "setslider", "setdate", "setcolor", "followlink", "setfragment", "handleauth", "navintent", "openclipboard", "checksafe", "reopentab", "spanav", "duplicatetab", "pintab", "mutetab", "movetab", "movetabwindow", "searchtabs", "badgetab", "attachmeta", "focuswindow", "maximizewindow", "minimizewindow", "restorewindow", "incognitowindow", "asksubmit", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcode", "consentpassword"]);
536
635
  var tabscommandactions = /* @__PURE__ */ new Set(["querytabs", "duplicatetab", "closepattern", "pintab", "mutetab", "movetab", "movetabwindow", "grouptabs", "colorgroup", "collapsegroup", "discardtab", "reloadtabs", "zoomin", "zoomout", "watchtab", "switchtab", "maximizewindow", "minimizewindow", "restorewindow", "focuswindow", "scratchwindow", "incognitowindow", "restoretab", "savelayout", "restorelayout", "findclones", "searchtabs", "badgetab", "attachmeta", "listaudio", "reopenrun", "snapshotsession"]);
537
636
  var formactions = /* @__PURE__ */ new Set(["fillform", "filllabel", "fillplaceholder", "detectfields", "generatevalues", "saveprofiles", "asksubmit", "submitform", "readerrors", "retryform", "runwizard", "selectchain", "picktypeahead", "pickdate", "attachfile", "handoffcaptcha", "fillcard", "fillcode", "consentpassword", "skiphoneypot", "detectlogin", "detecttemplate"]);
637
+ var datasetactions = /* @__PURE__ */ new Set(["scrapetable", "exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "importcsv", "looprows", "transformvalues", "deduperows", "paginateextract", "mergepages", "stamplerows", "previewgrid", "streamdisk", "resumeextract", "logprovenance"]);
638
+ var exportactions = /* @__PURE__ */ new Set(["exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "streamdisk"]);
538
639
  var fieldkinds = ["text", "email", "phone", "date", "number", "select", "check", "radio", "file", "password", "card", "code"];
539
640
  var layoutmutationactions = /* @__PURE__ */ new Set(["grouptabs", "colorgroup", "collapsegroup", "savelayout", "restorelayout"]);
540
641
  var groupcolors = ["grey", "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"];
@@ -569,6 +670,7 @@ function requiredcapability(kind) {
569
670
  if (kind === "tablist") return "tabs";
570
671
  if (kind === "downloadfile") return "downloads";
571
672
  if (kind === "openclipboard") return "clipboardRead";
673
+ if (kind === "copytable") return "clipboardWrite";
572
674
  if (kind === "openlink" || kind === "openprivate" || kind === "navlist" || kind === "batchopen" || kind === "reopentab" || kind === "deeplink") return "tabs";
573
675
  if (tabscommandactions.has(kind)) return "tabs";
574
676
  return void 0;
@@ -582,6 +684,16 @@ function islayoutkind(kind) {
582
684
  function isformkind(kind) {
583
685
  return formactions.has(kind);
584
686
  }
687
+ function isdatasetkind(kind) {
688
+ return datasetactions.has(kind);
689
+ }
690
+ function isexportkind(kind) {
691
+ return exportactions.has(kind);
692
+ }
693
+ function exportgranted(session, origin) {
694
+ if (!origingranted(session, origin)) return { allowed: false, reason: `The export of extracted data from ${origin} needs the session origin grants before it leaves local memory.` };
695
+ return { allowed: true };
696
+ }
585
697
  function validatefieldmatch(value) {
586
698
  if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed field match is required in options." };
587
699
  const match = value;
@@ -679,6 +791,89 @@ function validateformgrammar(step, options) {
679
791
  if (kind === "consentpassword" && !isnonempty(options.consentref)) return { allowed: false, reason: "A reviewed consent ref is required in options before any password is filled." };
680
792
  return { allowed: true };
681
793
  }
794
+ function validatetransformrule(value) {
795
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed transform rule with an expression, sources and a target is required in options." };
796
+ const rule = value;
797
+ const expression = rule.expression;
798
+ if (typeof expression !== "string" || !/^(trim|upper|lower|number|prefix|suffix|replace)(?::.+)?$/.test(expression)) return { allowed: false, reason: "The reviewed transform expression must be trim, upper, lower, number, prefix, suffix or replace with an optional argument." };
799
+ if (expression.startsWith("replace") && !expression.slice("replace".length).includes("=>")) return { allowed: false, reason: "The reviewed replace expression needs the from=>to separator." };
800
+ if (expression.startsWith("replace") && expression.slice("replace:".length).split("=>")[0] === "") return { allowed: false, reason: "The reviewed replace expression needs a non-empty from part." };
801
+ if (!Array.isArray(rule.sources) || rule.sources.length === 0 || !rule.sources.every((source) => isnonempty(source))) return { allowed: false, reason: "Every reviewed transform rule needs a non-empty list of source columns." };
802
+ if (!isnonempty(rule.target)) return { allowed: false, reason: "Every reviewed transform rule needs a non-empty target column." };
803
+ return { allowed: true };
804
+ }
805
+ function validatedatasetids(options, key) {
806
+ const ids = options[key];
807
+ if (!Array.isArray(ids) || ids.length === 0 || !ids.every((id) => isnonempty(id))) return { allowed: false, reason: `A reviewed non-empty list of dataset ids is required in options as ${key}.` };
808
+ return { allowed: true };
809
+ }
810
+ function validatedatagrammar(step, options, origin) {
811
+ const kind = step.kind;
812
+ if (kind === "scrapetable") {
813
+ if (options.name !== void 0 && !isnonempty(options.name)) return { allowed: false, reason: "The reviewed dataset name must be a non-empty string." };
814
+ if (options.rowlimit !== void 0 && (typeof options.rowlimit !== "number" || !Number.isInteger(options.rowlimit) || options.rowlimit < 1)) return { allowed: false, reason: "The reviewed row limit must be a positive integer with no code ceiling." };
815
+ }
816
+ if (kind === "paginateextract") {
817
+ if (!isnonempty(options.next)) return { allowed: false, reason: "A reviewed next control selector is required in options." };
818
+ if (options.pages !== void 0 && (typeof options.pages !== "number" || !Number.isInteger(options.pages) || options.pages < 1)) return { allowed: false, reason: "The reviewed page count must be a positive integer with no code ceiling." };
819
+ if (!nonnegativeoption(options, "wait")) return { allowed: false, reason: "The reviewed row freshness wait must be zero or a positive number of milliseconds." };
820
+ }
821
+ if (kind === "exportcsv" || kind === "exportjson" || kind === "exportexcel" || kind === "copytable" || kind === "streamdisk") {
822
+ if (!isnonempty(options.dataset)) return { allowed: false, reason: "A reviewed dataset id is required in options." };
823
+ if (options.name !== void 0 && !isnonempty(options.name)) return { allowed: false, reason: "The reviewed artifact name must be a non-empty string." };
824
+ }
825
+ if (kind === "exportcsv" && options.delimiter !== void 0 && (typeof options.delimiter !== "string" || options.delimiter.length !== 1)) return { allowed: false, reason: "The reviewed csv delimiter must be a single character." };
826
+ if (kind === "streamdisk" && (typeof options.chunk !== "number" || !Number.isInteger(options.chunk) || options.chunk < 1)) return { allowed: false, reason: "The reviewed streaming chunk size must be a positive integer with no code ceiling." };
827
+ if (kind === "pushsheets") {
828
+ if (!isnonempty(options.dataset)) return { allowed: false, reason: "A reviewed dataset id is required in options." };
829
+ if (!isnonempty(options.sheet)) return { allowed: false, reason: "A reviewed sheet endpoint url is required in options." };
830
+ if (!ishttpsurl(options.sheet)) return { allowed: false, reason: "The reviewed sheet endpoint url must use HTTPS." };
831
+ if (options.reviewed !== true) return { allowed: false, reason: "The sheet push needs the explicit reviewed flag before any data leaves local memory." };
832
+ }
833
+ if (kind === "importcsv") {
834
+ if (typeof options.csv !== "string" || !options.csv.trim()) return { allowed: false, reason: "Reviewed csv content is required in options." };
835
+ if (options.name !== void 0 && !isnonempty(options.name)) return { allowed: false, reason: "The reviewed dataset name must be a non-empty string." };
836
+ if (options.mapping !== void 0) {
837
+ const mapping = options.mapping;
838
+ if (!mapping || typeof mapping !== "object" || Array.isArray(mapping) || !Object.values(mapping).every((item) => typeof item === "string")) return { allowed: false, reason: "The reviewed csv column mapping must be an object of string values." };
839
+ }
840
+ }
841
+ if (kind === "looprows") {
842
+ if (!isnonempty(options.dataset)) return { allowed: false, reason: "A reviewed dataset id is required in options." };
843
+ if (options.variable !== void 0 && !isnonempty(options.variable)) return { allowed: false, reason: "The reviewed row variable name must be a non-empty string." };
844
+ const inner = validateinnerstep(options, origin);
845
+ if (!inner.allowed) return inner;
846
+ }
847
+ if (kind === "transformvalues") {
848
+ if (!isnonempty(options.dataset)) return { allowed: false, reason: "A reviewed dataset id is required in options." };
849
+ const rules = options.rules;
850
+ if (!Array.isArray(rules) || rules.length === 0) return { allowed: false, reason: "A reviewed non-empty list of transform rules is required in options." };
851
+ for (const item of rules) {
852
+ const rulecheck = validatetransformrule(item);
853
+ if (!rulecheck.allowed) return rulecheck;
854
+ }
855
+ }
856
+ if (kind === "deduperows") {
857
+ if (!isnonempty(options.dataset)) return { allowed: false, reason: "A reviewed dataset id is required in options." };
858
+ const keys = options.keys;
859
+ if (!Array.isArray(keys) || keys.length === 0 || !keys.every((key) => isnonempty(key))) return { allowed: false, reason: "A reviewed non-empty list of dedupe column keys is required in options." };
860
+ }
861
+ if (kind === "mergepages") {
862
+ const listcheck = validatedatasetids(options, "datasets");
863
+ if (!listcheck.allowed) return listcheck;
864
+ }
865
+ if (kind === "stamplerows") {
866
+ if (!isnonempty(options.dataset)) return { allowed: false, reason: "A reviewed dataset id is required in options." };
867
+ if (options.url !== void 0 && !ishttpsurl(options.url)) return { allowed: false, reason: "The reviewed source url must use HTTPS." };
868
+ }
869
+ if (kind === "previewgrid") {
870
+ if (!isnonempty(options.dataset)) return { allowed: false, reason: "A reviewed dataset id is required in options." };
871
+ if (options.sample !== void 0 && (typeof options.sample !== "number" || !Number.isInteger(options.sample) || options.sample < 1)) return { allowed: false, reason: "The reviewed sample row count must be a positive integer with no code ceiling." };
872
+ }
873
+ if (kind === "resumeextract" && !isnonempty(options.session)) return { allowed: false, reason: "A reviewed extract session id is required in options." };
874
+ if (kind === "logprovenance" && !isnonempty(options.artifact)) return { allowed: false, reason: "A reviewed artifact id or name is required in options." };
875
+ return { allowed: true };
876
+ }
682
877
  function submitreviewgranted(steps, submitid) {
683
878
  const position = steps.findIndex((candidate) => candidate.id === submitid);
684
879
  const asked = steps.some((candidate, index) => candidate.kind === "asksubmit" && (position === -1 || index < position));
@@ -817,7 +1012,7 @@ function validateinnerstep(options, origin) {
817
1012
  return { allowed: true };
818
1013
  }
819
1014
  if (typeof kind !== "string" || !kind.trim()) return { allowed: false, reason: "A reviewed step id or inline step kind is required in options." };
820
- if (kind === "retryaction" || kind === "enterframe") return { allowed: false, reason: "The reviewed inner step cannot be another wrapper kind." };
1015
+ if (kind === "retryaction" || kind === "enterframe" || kind === "looprows") return { allowed: false, reason: "The reviewed inner step cannot be another wrapper kind." };
821
1016
  if (!allowedactions.has(kind)) return { allowed: false, reason: "The reviewed inner step kind is unsupported." };
822
1017
  const inneroptions = options.options;
823
1018
  if (inneroptions !== void 0 && (!inneroptions || typeof inneroptions !== "object" || Array.isArray(inneroptions))) return { allowed: false, reason: "The reviewed inner step options must be an object." };
@@ -1201,6 +1396,10 @@ function validatestep(step, origin) {
1201
1396
  const formcheck = validateformgrammar(step, options);
1202
1397
  if (!formcheck.allowed) return formcheck;
1203
1398
  }
1399
+ if (isdatasetkind(step.kind)) {
1400
+ const datacheck = validatedatagrammar(step, options, origin);
1401
+ if (!datacheck.allowed) return datacheck;
1402
+ }
1204
1403
  if (step.kind === "tabcreate") {
1205
1404
  if (options.background !== void 0 && typeof options.background !== "boolean") return { allowed: false, reason: "The reviewed background flag must be a boolean." };
1206
1405
  if (options.window !== void 0 && (typeof options.window !== "number" || !Number.isInteger(options.window) || options.window < 0)) return { allowed: false, reason: "The reviewed target window id must be a non-negative integer." };
@@ -1228,6 +1427,10 @@ function canexecute(input) {
1228
1427
  if (input.plan.expiresat <= now) return { allowed: false, reason: "The approved plan has expired." };
1229
1428
  if ((input.step.kind === "pierceshadow" || input.step.kind === "enterframe") && !origingranted(input.session, input.origin)) return { allowed: false, reason: "The shadow or frame step is outside the session origin grants." };
1230
1429
  if (input.step.kind === "readjson" && !origingranted(input.session, input.origin)) return { allowed: false, reason: "The json state read is outside the session origin grants." };
1430
+ if (isexportkind(input.step.kind)) {
1431
+ const exportgate = exportgranted(input.session, input.origin);
1432
+ if (!exportgate.allowed) return exportgate;
1433
+ }
1231
1434
  if (input.step.kind === "navlist") {
1232
1435
  let options = {};
1233
1436
  try {
@@ -1336,6 +1539,15 @@ function wizardcompletion(state) {
1336
1539
  if (state.steps <= 0) return 0;
1337
1540
  return Math.min(1, state.completed.filter(Boolean).length / state.steps);
1338
1541
  }
1542
+ function extractionshare(rowscollected, estimatedtotal) {
1543
+ if (!Number.isFinite(estimatedtotal) || estimatedtotal <= 0) return 0;
1544
+ return Math.min(1, Math.max(0, rowscollected) / estimatedtotal);
1545
+ }
1546
+ function recordextraction(progress, planid, stepid, entry, now) {
1547
+ const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
1548
+ const outcome = { stepid, ok: true, summary: `Extraction page ${entry.page} collected ${entry.rows} row${entry.rows === 1 ? "" : "s"} at cursor ${entry.cursor}.`, details: { extraction: entry }, at: now };
1549
+ return recordoutcome(base, planid, outcome, now);
1550
+ }
1339
1551
  function recordwizardstep(progress, planid, stepid, state, now) {
1340
1552
  const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
1341
1553
  const executed = Math.min(state.index, state.steps);
@@ -1345,7 +1557,7 @@ function recordwizardstep(progress, planid, stepid, state, now) {
1345
1557
  }
1346
1558
 
1347
1559
  // version.ts
1348
- var packageversion = "1.1.37";
1560
+ var packageversion = "1.1.38";
1349
1561
 
1350
1562
  // types.ts
1351
1563
  var protocolversion = packageversion;
@@ -1382,9 +1594,9 @@ function parseproposal(value, origin) {
1382
1594
  return step;
1383
1595
  });
1384
1596
  for (const step of steps) {
1385
- if (step.kind !== "retryaction" && step.kind !== "enterframe") continue;
1597
+ if (step.kind !== "retryaction" && step.kind !== "enterframe" && step.kind !== "looprows") continue;
1386
1598
  const options = parseoptions(step);
1387
- if (typeof options.stepid === "string" && !steps.some((candidate) => candidate.id === options.stepid)) throw new Error("A retry or frame wrapper references an unknown step id.");
1599
+ if (typeof options.stepid === "string" && !steps.some((candidate) => candidate.id === options.stepid)) throw new Error("A retry, frame or loop wrapper references an unknown step id.");
1388
1600
  }
1389
1601
  for (const step of steps) {
1390
1602
  if (step.kind !== "submitform" && step.kind !== "retryform") continue;
@@ -1451,6 +1663,17 @@ function errorreportresponse(input) {
1451
1663
  function wizardreport(input) {
1452
1664
  return { version: protocolversion, ...input.sessionid ? { sessionid: input.sessionid } : {}, wizards: input.wizards, picks: input.picks };
1453
1665
  }
1666
+ function datasetresponse(input) {
1667
+ const sample = Math.max(0, Math.floor(input.sample ?? 10));
1668
+ const payload = { ...input.dataset, rows: input.dataset.rows.slice(0, sample), totalrows: input.dataset.rows.length };
1669
+ return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, dataset: payload });
1670
+ }
1671
+ function extractionreport(input) {
1672
+ return { version: protocolversion, sessions: input.sessions };
1673
+ }
1674
+ function provenancereport(input) {
1675
+ return { version: protocolversion, records: input.records };
1676
+ }
1454
1677
 
1455
1678
  // extension/browsertabs.ts
1456
1679
  var browserkinds = /* @__PURE__ */ new Set(["tablist", "tabcreate", "tabactivate", "tabclose", "tabreload", "tabsnapshot", "windowlist", "windowcreate", "windowclose", "zoomset", "windowresize", "downloadfile"]);
@@ -2213,6 +2436,261 @@ function backoffwaits(attempts, wait, factor) {
2213
2436
  return windows;
2214
2437
  }
2215
2438
 
2439
+ // extension/pagedata.ts
2440
+ function normalizeheader(label) {
2441
+ const slug = label.trim().toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "");
2442
+ return slug || "column";
2443
+ }
2444
+ function columnspecof(label, used = /* @__PURE__ */ new Set()) {
2445
+ const base = normalizeheader(label);
2446
+ let key = base;
2447
+ let suffix = 2;
2448
+ while (used.has(key)) {
2449
+ key = `${base}${suffix}`;
2450
+ suffix += 1;
2451
+ }
2452
+ used.add(key);
2453
+ return { key, label: label.trim(), kind: "text", normalized: label.trim().toLowerCase() };
2454
+ }
2455
+ function rowhash(row, keys) {
2456
+ const source = (keys.length > 0 ? keys : Object.keys(row).sort()).map((key) => `${key}=${row[key] ?? ""}`).join("|");
2457
+ let hash = 5381;
2458
+ for (let index = 0; index < source.length; index += 1) hash = (hash * 33 ^ source.charCodeAt(index)) >>> 0;
2459
+ return hash.toString(16);
2460
+ }
2461
+ function dedupebykeys(rows, keys) {
2462
+ const seen = /* @__PURE__ */ new Set();
2463
+ const kept = [];
2464
+ for (const row of rows) {
2465
+ const hash = rowhash(row, keys);
2466
+ if (seen.has(hash)) continue;
2467
+ seen.add(hash);
2468
+ kept.push(row);
2469
+ }
2470
+ return { kept, removed: rows.length - kept.length };
2471
+ }
2472
+ function applyexpression(value, expression) {
2473
+ const split = expression.indexOf(":");
2474
+ const op = split === -1 ? expression : expression.slice(0, split);
2475
+ const argument = split === -1 ? void 0 : expression.slice(split + 1);
2476
+ if (op === "trim") return value.trim();
2477
+ if (op === "upper") return value.toUpperCase();
2478
+ if (op === "lower") return value.toLowerCase();
2479
+ if (op === "number") return value.replace(/[^\d.\-]/g, "");
2480
+ if (op === "prefix") return `${argument ?? ""}${value}`;
2481
+ if (op === "suffix") return `${value}${argument ?? ""}`;
2482
+ if (op === "replace") {
2483
+ const separator = argument?.indexOf("=>") ?? -1;
2484
+ if (separator === -1 || separator === 0) throw new Error(`The reviewed transform expression ${expression} needs the from=>to separator.`);
2485
+ const from = argument.slice(0, separator);
2486
+ const to = argument.slice(separator + 2);
2487
+ return value.split(from).join(to);
2488
+ }
2489
+ throw new Error(`The reviewed transform expression ${op} is not supported.`);
2490
+ }
2491
+ function transformrows(rows, rules) {
2492
+ const errors = [];
2493
+ const output = rows.map((row) => ({ ...row }));
2494
+ for (const rule of rules) {
2495
+ const updated = [];
2496
+ try {
2497
+ for (const row of output) updated.push({ ...row, [rule.target]: applyexpression(rule.sources.map((source) => row[source] ?? "").join(" "), rule.expression) });
2498
+ } catch (error) {
2499
+ errors.push(`${rule.target}: ${error instanceof Error ? error.message : String(error)}`);
2500
+ continue;
2501
+ }
2502
+ output.splice(0, output.length, ...updated);
2503
+ }
2504
+ return { rows: output, errors };
2505
+ }
2506
+ function mergedatasets(datasets) {
2507
+ const columns = [];
2508
+ const seen = /* @__PURE__ */ new Set();
2509
+ for (const dataset of datasets) {
2510
+ for (const column of dataset.columns) {
2511
+ if (seen.has(column.key)) continue;
2512
+ seen.add(column.key);
2513
+ columns.push(column);
2514
+ }
2515
+ }
2516
+ const rows = datasets.flatMap((dataset) => dataset.rows.map((row) => {
2517
+ const merged = {};
2518
+ for (const column of columns) merged[column.key] = row[column.key] ?? "";
2519
+ return merged;
2520
+ }));
2521
+ return { columns, rows };
2522
+ }
2523
+ function samplerows(rows, url, stepid, at) {
2524
+ const stamped = rows.map((row) => ({ ...row, source: url, capturedat: String(at), step: stepid }));
2525
+ const sources = stamped.map((row, index) => ({ row: index, url, at, stepid }));
2526
+ return { rows: stamped, sources };
2527
+ }
2528
+ function csvfield(value, delimiter) {
2529
+ return value.includes(delimiter) || value.includes('"') || value.includes("\n") ? `"${value.replace(/"/g, '""')}"` : value;
2530
+ }
2531
+ function tocsv(columns, rows, delimiter = ",") {
2532
+ const lines = [columns.map((column) => csvfield(column.label || column.key, delimiter)).join(delimiter)];
2533
+ for (const row of rows) lines.push(columns.map((column) => csvfield(row[column.key] ?? "", delimiter)).join(delimiter));
2534
+ return lines.join("\n");
2535
+ }
2536
+ function parsecsv(text2, delimiter = ",") {
2537
+ const records = [];
2538
+ let field = "";
2539
+ let record2 = [];
2540
+ let quoted = false;
2541
+ for (let index = 0; index < text2.length; index += 1) {
2542
+ const character = text2[index];
2543
+ if (quoted) {
2544
+ if (character === '"') {
2545
+ if (text2[index + 1] === '"') {
2546
+ field += '"';
2547
+ index += 1;
2548
+ } else quoted = false;
2549
+ } else field += character;
2550
+ continue;
2551
+ }
2552
+ if (character === '"') {
2553
+ quoted = true;
2554
+ continue;
2555
+ }
2556
+ if (character === delimiter) {
2557
+ record2.push(field);
2558
+ field = "";
2559
+ continue;
2560
+ }
2561
+ if (character === "\n" || character === "\r") {
2562
+ if (character === "\r" && text2[index + 1] === "\n") index += 1;
2563
+ record2.push(field);
2564
+ field = "";
2565
+ if (record2.some((value) => value.length > 0) || record2.length > 1) records.push(record2);
2566
+ record2 = [];
2567
+ continue;
2568
+ }
2569
+ field += character;
2570
+ }
2571
+ record2.push(field);
2572
+ if (record2.some((value) => value.length > 0) || record2.length > 1) records.push(record2);
2573
+ const [headers = [], ...rows] = records;
2574
+ return { headers, rows };
2575
+ }
2576
+ function mapcolumns(headers, mapping = {}) {
2577
+ const used = /* @__PURE__ */ new Set();
2578
+ return headers.map((header) => {
2579
+ const target = mapping[header] ?? mapping[normalizeheader(header)] ?? header;
2580
+ return columnspecof(target, used);
2581
+ });
2582
+ }
2583
+ function tojson(columns, rows) {
2584
+ return JSON.stringify({ columns, rows });
2585
+ }
2586
+ function xmltext(value) {
2587
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2588
+ }
2589
+ function toexcel(columns, rows, name) {
2590
+ const head = columns.map((column) => `<Cell ss:StyleID="head"><Data ss:Type="String">${xmltext(column.label || column.key)}</Data></Cell>`).join("");
2591
+ const body = rows.map((row) => `<Row>${columns.map((column) => {
2592
+ const value = row[column.key] ?? "";
2593
+ const numeric = column.kind === "number" && value.trim() !== "" && Number.isFinite(Number(value));
2594
+ return numeric ? `<Cell><Data ss:Type="Number">${xmltext(value)}</Data></Cell>` : `<Cell><Data ss:Type="String">${xmltext(value)}</Data></Cell>`;
2595
+ }).join("")}</Row>`).join("");
2596
+ return `<?xml version="1.0"?><?mso-application progid="Excel.Sheet"?><Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"><Styles><Style ss:ID="head"><Font ss:Bold="1"/></Style></Styles><Worksheet ss:Name="${xmltext(name || "dataset").slice(0, 31)}"><Table><Row>${head}</Row>${body}</Table></Worksheet></Workbook>`;
2597
+ }
2598
+
2599
+ // extension/datacommand.ts
2600
+ function builddataset(id, name, grid, at) {
2601
+ return { id, name: name || id, columns: grid.columns, rows: grid.rows, sources: [], at };
2602
+ }
2603
+ function checksum(value) {
2604
+ let hash = 5381;
2605
+ for (let index = 0; index < value.length; index += 1) hash = (hash * 33 ^ value.charCodeAt(index)) >>> 0;
2606
+ return `fnv1a-${hash.toString(16)}`;
2607
+ }
2608
+ function exportcontent(datasetvalue, format, delimiter = ",") {
2609
+ if (format === "json") return tojson(datasetvalue.columns, datasetvalue.rows);
2610
+ if (format === "excel") return toexcel(datasetvalue.columns, datasetvalue.rows, datasetvalue.name);
2611
+ return tocsv(datasetvalue.columns, datasetvalue.rows, delimiter);
2612
+ }
2613
+ function exportartifact(id, datasetvalue, format, stepid, content, at) {
2614
+ const extension = format === "excel" ? "xml" : format;
2615
+ return { id, kind: format, name: `${datasetvalue.name || datasetvalue.id}.${extension}`, stepid, rowcount: datasetvalue.rows.length, content, checksum: checksum(content), at };
2616
+ }
2617
+ function artifactrecordof(artifact) {
2618
+ return { id: artifact.id, kind: artifact.kind, name: artifact.name, stepid: artifact.stepid, at: artifact.at };
2619
+ }
2620
+ function chunkplan(rows, chunk) {
2621
+ const size = Math.max(1, Math.floor(chunk));
2622
+ const chunks = [];
2623
+ for (let from = 0; from < rows || chunks.length === 0; from += size) {
2624
+ const to = Math.min(rows, from + size);
2625
+ chunks.push({ index: chunks.length, from, to });
2626
+ if (to >= rows) break;
2627
+ }
2628
+ return chunks;
2629
+ }
2630
+ function backpressure(written, acknowledged) {
2631
+ return written - acknowledged >= 1;
2632
+ }
2633
+ function advancestream(state, chunk, at, done) {
2634
+ return { datasetid: state.datasetid, name: state.name, chunk: chunk.index + 1, chunks: state.chunks, written: chunk.to, ...done ? { done: true } : {}, at };
2635
+ }
2636
+ function newstream(datasetvalue, chunks, at) {
2637
+ return { datasetid: datasetvalue.id, name: datasetvalue.name, chunk: 0, chunks, written: 0, at };
2638
+ }
2639
+ function advancecursor(sessionvalue, page, rows, at, done) {
2640
+ return {
2641
+ id: sessionvalue.id,
2642
+ datasetid: sessionvalue.datasetid,
2643
+ name: sessionvalue.name,
2644
+ target: sessionvalue.target,
2645
+ next: sessionvalue.next,
2646
+ planned: sessionvalue.planned,
2647
+ pages: [...sessionvalue.pages, page],
2648
+ rows: sessionvalue.rows + rows,
2649
+ cursor: sessionvalue.cursor + 1,
2650
+ ...done || sessionvalue.cursor + 1 >= sessionvalue.planned ? { done: true } : {},
2651
+ startedat: sessionvalue.startedat,
2652
+ updatedat: at
2653
+ };
2654
+ }
2655
+ function newextractsession(id, datasetid, name, target, next, planned, at) {
2656
+ return { id, datasetid, name, target, next, planned, pages: [], rows: 0, cursor: 0, startedat: at, updatedat: at };
2657
+ }
2658
+ function remainingpages(sessionvalue, planned) {
2659
+ if (sessionvalue.done) return 0;
2660
+ return Math.max(0, Math.max(sessionvalue.planned, planned) - sessionvalue.cursor);
2661
+ }
2662
+ function provenancefor(artifact, url, stepid, at) {
2663
+ return { artifact: artifact.id, name: artifact.name, url, stepid, rowstart: artifact.rowcount > 0 ? 1 : 0, rowend: artifact.rowcount, checksum: artifact.checksum, at };
2664
+ }
2665
+ function interpolate(text2, row) {
2666
+ return text2.replace(/\{\{([^}]+)\}\}/g, (_, key) => row[key.trim()] ?? "");
2667
+ }
2668
+ function loopstep(step, row) {
2669
+ return {
2670
+ ...step,
2671
+ ...step.target !== void 0 ? { target: interpolate(step.target, row) } : {},
2672
+ ...step.value !== void 0 ? { value: interpolate(step.value, row) } : {},
2673
+ ...step.options !== void 0 ? { options: interpolate(step.options, row) } : {}
2674
+ };
2675
+ }
2676
+ function loopvariables(row) {
2677
+ return { ...row };
2678
+ }
2679
+ function gridpreview(datasetvalue, sample) {
2680
+ return { datasetid: datasetvalue.id, columns: datasetvalue.columns.map((column) => column.key), rows: datasetvalue.rows.length, sample: datasetvalue.rows.slice(0, Math.max(0, Math.floor(sample))) };
2681
+ }
2682
+ function sheetpayload(datasetvalue, sheet) {
2683
+ return { sheet, columns: datasetvalue.columns.map((column) => column.key), rows: datasetvalue.rows };
2684
+ }
2685
+ function mergetaskrules(existing, taskid, transforms, dedupekeys, at) {
2686
+ return {
2687
+ taskid,
2688
+ transforms: transforms.length > 0 ? transforms : existing?.transforms ?? [],
2689
+ dedupekeys: dedupekeys.length > 0 ? dedupekeys : existing?.dedupekeys ?? [],
2690
+ at
2691
+ };
2692
+ }
2693
+
2216
2694
  // extension/background.ts
2217
2695
  var sessionduration = 15 * 60 * 1e3;
2218
2696
  var freshcheckkinds = /* @__PURE__ */ new Set(["focus", "inspect", "click", "type", "scroll", "select", "hover"]);
@@ -2403,6 +2881,13 @@ function stepauditkind(step, ok) {
2403
2881
  if (step.kind === "submitform" || step.kind === "asksubmit") return "submit";
2404
2882
  if (step.kind === "consentpassword") return "consent";
2405
2883
  if (step.kind === "handoffcaptcha") return "handoff";
2884
+ if (isdatasetkind(step.kind)) {
2885
+ if (step.kind === "exportcsv" || step.kind === "exportjson" || step.kind === "exportexcel" || step.kind === "copytable" || step.kind === "pushsheets") return "export";
2886
+ if (step.kind === "streamdisk") return "stream";
2887
+ if (step.kind === "resumeextract") return "resume";
2888
+ if (step.kind === "logprovenance") return "provenance";
2889
+ return "scrape";
2890
+ }
2406
2891
  if (formfillkinds.has(step.kind)) return "fill";
2407
2892
  if (pointerkinds.has(step.kind)) return "pointer";
2408
2893
  if (watchstepkinds.has(step.kind)) return "watch";
@@ -3585,6 +4070,220 @@ async function executeformstep(step, session, plan, tabid2, origin) {
3585
4070
  }
3586
4071
  }
3587
4072
  }
4073
+ async function loaddataset(datasetid) {
4074
+ const record2 = await memory.getdataset(datasetid);
4075
+ if (!record2) throw new Error(`No dataset ${datasetid} exists yet; scrape or import it first.`);
4076
+ return record2;
4077
+ }
4078
+ function readgridoutput(output) {
4079
+ const grid = output?.details?.grid;
4080
+ if (!grid || typeof grid !== "object") return null;
4081
+ return grid;
4082
+ }
4083
+ async function storeexport(stepid, datasetvalue, format, delimiter, session, planid, origin) {
4084
+ const content = exportcontent(datasetvalue, format, delimiter);
4085
+ const artifact = exportartifact(randomid(), datasetvalue, format, stepid, content, Date.now());
4086
+ await memory.addexport(artifact);
4087
+ await memory.addartifact(artifactrecordof(artifact));
4088
+ await memory.addprovenance(provenancefor(artifact, datasetvalue.sources[0]?.url ?? origin, stepid, Date.now()));
4089
+ await audit("export", `Exported ${artifact.rowcount} row${artifact.rowcount === 1 ? "" : "s"} of dataset ${datasetvalue.name} into the ${format} artifact ${artifact.name} with checksum ${artifact.checksum}; the provenance record keeps the source url, step ref and row range.`, { ...session ? { sessionid: session.id } : {}, planid, stepid });
4090
+ await refreshbadge();
4091
+ return artifact;
4092
+ }
4093
+ async function executedatastep(step, session, plan, tabid2, origin) {
4094
+ const options = stepoptions2(step);
4095
+ const extra = { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id };
4096
+ switch (step.kind) {
4097
+ case "scrapetable": {
4098
+ const output = await dispatchpagestep(step, tabid2, origin, plan);
4099
+ const grid = readgridoutput(output);
4100
+ if (!output?.ok || !grid) return output ?? { ok: false, summary: "The table scrape returned no result." };
4101
+ const id = randomid();
4102
+ const name = typeof options.name === "string" && options.name ? options.name : `dataset-${step.id}`;
4103
+ const datasetvalue = builddataset(id, name, grid, Date.now());
4104
+ await memory.setdataset(datasetvalue);
4105
+ const extract = advancecursor(newextractsession(randomid(), id, name, step.target ?? "", "", 1, Date.now()), origin, datasetvalue.rows.length, Date.now(), true);
4106
+ await memory.setextractsession(extract);
4107
+ await memory.setprogress(recordextraction(await memory.getprogress(), plan.id, step.id, { page: 1, rows: datasetvalue.rows.length, cursor: 1 }, Date.now()));
4108
+ await refreshbadge();
4109
+ return { ok: true, summary: output.summary, details: { ...output.details ?? {}, dataset: { id, name, rows: datasetvalue.rows.length, columns: grid.columns.length } } };
4110
+ }
4111
+ case "paginateextract": {
4112
+ const output = await dispatchpagestep(step, tabid2, origin, plan);
4113
+ const grid = readgridoutput(output);
4114
+ if (!output?.ok || !grid) return output ?? { ok: false, summary: "The paginated extraction returned no result." };
4115
+ const id = randomid();
4116
+ const name = typeof options.name === "string" && options.name ? options.name : `dataset-${step.id}`;
4117
+ const datasetvalue = builddataset(id, name, grid, Date.now());
4118
+ await memory.setdataset(datasetvalue);
4119
+ const planned = typeof options.pages === "number" && Number.isInteger(options.pages) && options.pages > 0 ? options.pages : Number(output.details?.pages ?? 1);
4120
+ const next = output.details?.next === true;
4121
+ const extract = advancecursor(newextractsession(randomid(), id, name, step.target ?? "", typeof options.next === "string" ? options.next : "", planned, Date.now()), origin, datasetvalue.rows.length, Date.now(), !next);
4122
+ await memory.setextractsession(extract);
4123
+ await memory.setprogress(recordextraction(await memory.getprogress(), plan.id, step.id, { page: extract.cursor, rows: datasetvalue.rows.length, cursor: extract.cursor }, Date.now()));
4124
+ await refreshbadge();
4125
+ const extractedpages = Number(output.details?.pages ?? 1);
4126
+ const estimated = Math.max(1, Math.round(datasetvalue.rows.length / Math.max(1, extractedpages) * planned));
4127
+ return { ok: true, summary: output.summary, details: { ...output.details ?? {}, dataset: { id, name, rows: datasetvalue.rows.length, columns: grid.columns.length }, extractsession: { id: extract.id, cursor: extract.cursor, planned, done: extract.done === true }, progressshare: extractionshare(datasetvalue.rows.length, estimated) } };
4128
+ }
4129
+ case "resumeextract": {
4130
+ const sessionid = typeof options.session === "string" ? options.session : "";
4131
+ const extract = (await memory.getextractsessions()).find((item) => item.id === sessionid);
4132
+ if (!extract) throw new Error(`No extract session ${sessionid} is stored yet.`);
4133
+ if (extract.done) return { ok: true, summary: `Extraction ${extract.name} already completed at cursor ${extract.cursor}.`, details: { cursor: extract.cursor, done: true } };
4134
+ const datasetvalue = await loaddataset(extract.datasetid);
4135
+ const remaining = remainingpages(extract, extract.planned);
4136
+ const derived = { id: step.id, kind: "paginateextract", target: extract.target, summary: step.summary, risk: "sensitive", options: JSON.stringify({ next: extract.next, pages: remaining, cursor: extract.cursor, name: extract.name }) };
4137
+ const output = await dispatchpagestep(derived, tabid2, origin, plan);
4138
+ const grid = readgridoutput(output);
4139
+ if (!output?.ok || !grid) return output ?? { ok: false, summary: "The resumed extraction returned no result." };
4140
+ const merged = mergedatasets([{ columns: datasetvalue.columns, rows: datasetvalue.rows }, { columns: grid.columns, rows: grid.rows }]);
4141
+ const updated = { ...datasetvalue, columns: merged.columns, rows: merged.rows, at: Date.now() };
4142
+ await memory.setdataset(updated);
4143
+ const next = output.details?.next === true;
4144
+ const resumed = advancecursor({ ...extract, updatedat: Date.now() }, origin, grid.rows.length, Date.now(), !next);
4145
+ await memory.setextractsession(resumed);
4146
+ await memory.setprogress(recordextraction(await memory.getprogress(), plan.id, step.id, { page: resumed.cursor, rows: grid.rows.length, cursor: resumed.cursor }, Date.now()));
4147
+ await audit("resume", `Extraction ${extract.name} resumed from cursor ${extract.cursor} and collected ${grid.rows.length} further row${grid.rows.length === 1 ? "" : "s"} across ${Number(output.details?.pages ?? 1)} page${Number(output.details?.pages ?? 1) === 1 ? "" : "s"}; the dataset now holds ${updated.rows.length} rows.`, extra);
4148
+ return { ok: true, summary: `Resumed extraction ${extract.name} from cursor ${extract.cursor}; the dataset now holds ${updated.rows.length} rows.`, details: { dataset: { id: updated.id, rows: updated.rows.length }, extractsession: { id: resumed.id, cursor: resumed.cursor, planned: resumed.planned, done: resumed.done === true } } };
4149
+ }
4150
+ case "mergepages": {
4151
+ const ids = Array.isArray(options.datasets) ? options.datasets.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
4152
+ const records = [];
4153
+ for (const id2 of ids) records.push(await loaddataset(id2));
4154
+ const merged = mergedatasets(records);
4155
+ const id = randomid();
4156
+ const datasetvalue = { id, name: typeof options.name === "string" && options.name ? options.name : `merged-${step.id}`, columns: merged.columns, rows: merged.rows, sources: records.flatMap((record2) => record2.sources), at: Date.now() };
4157
+ await memory.setdataset(datasetvalue);
4158
+ await refreshbadge();
4159
+ return { ok: true, summary: `Merged ${records.length} dataset${records.length === 1 ? "" : "s"} into ${datasetvalue.name} with ${merged.columns.length} aligned column${merged.columns.length === 1 ? "" : "s"} and ${merged.rows.length} row${merged.rows.length === 1 ? "" : "s"}.`, details: { dataset: { id, name: datasetvalue.name, rows: merged.rows.length, columns: merged.columns.length }, merged: records.map((record2) => record2.id) } };
4160
+ }
4161
+ case "transformvalues": {
4162
+ const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
4163
+ const rules = (Array.isArray(options.rules) ? options.rules : []).filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item));
4164
+ const applied = transformrows(datasetvalue.rows, rules);
4165
+ await memory.setdataset({ ...datasetvalue, rows: applied.rows, at: Date.now() });
4166
+ const existing = (await memory.gettaskrules()).find((item) => item.taskid === plan.id);
4167
+ await memory.settaskrules(mergetaskrules(existing, plan.id, rules, [], Date.now()));
4168
+ if (applied.errors.length > 0) return { ok: false, summary: `Transform rules surfaced ${applied.errors.length} error${applied.errors.length === 1 ? "" : "s"}: ${applied.errors.join("; ")}.`, details: { errors: applied.errors, rules: rules.length } };
4169
+ return { ok: true, summary: `Applied ${rules.length} reviewed transform rule${rules.length === 1 ? "" : "s"} to ${applied.rows.length} row${applied.rows.length === 1 ? "" : "s"}.`, details: { rules: rules.length, rows: applied.rows.length } };
4170
+ }
4171
+ case "deduperows": {
4172
+ const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
4173
+ const keys = Array.isArray(options.keys) ? options.keys.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
4174
+ const result = dedupebykeys(datasetvalue.rows, keys);
4175
+ await memory.setdataset({ ...datasetvalue, rows: result.kept, at: Date.now() });
4176
+ const existing = (await memory.gettaskrules()).find((item) => item.taskid === plan.id);
4177
+ await memory.settaskrules(mergetaskrules(existing, plan.id, [], keys, Date.now()));
4178
+ return { ok: true, summary: `Deduplicated ${datasetvalue.rows.length} row${datasetvalue.rows.length === 1 ? "" : "s"} by ${keys.join(", ")}: removed ${result.removed} duplicate${result.removed === 1 ? "" : "s"}, kept ${result.kept.length}.`, details: { dedupe: { removed: result.removed, kept: result.kept.length, keys } } };
4179
+ }
4180
+ case "stamplerows": {
4181
+ const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
4182
+ const url = typeof options.url === "string" && options.url ? options.url : origin;
4183
+ const stamped = samplerows(datasetvalue.rows, url, step.id, Date.now());
4184
+ await memory.setdataset({ ...datasetvalue, rows: stamped.rows, sources: stamped.sources, at: Date.now() });
4185
+ return { ok: true, summary: `Stamped ${stamped.rows.length} row${stamped.rows.length === 1 ? "" : "s"} with the source url, timestamp and step ref.`, details: { rows: stamped.rows.length, url } };
4186
+ }
4187
+ case "previewgrid": {
4188
+ const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
4189
+ const sample = typeof options.sample === "number" && Number.isInteger(options.sample) && options.sample > 0 ? options.sample : 10;
4190
+ const preview = gridpreview(datasetvalue, sample);
4191
+ return { ok: true, summary: `Previewed ${preview.rows} row${preview.rows === 1 ? "" : "s"} of dataset ${datasetvalue.name} in the grid with ${preview.columns.length} column${preview.columns.length === 1 ? "" : "s"}.`, details: { preview } };
4192
+ }
4193
+ case "importcsv": {
4194
+ const csv = typeof options.csv === "string" ? options.csv : "";
4195
+ const parsed = parsecsv(csv, typeof options.delimiter === "string" && options.delimiter.length === 1 ? options.delimiter : ",");
4196
+ const mapping = options.mapping && typeof options.mapping === "object" && !Array.isArray(options.mapping) ? options.mapping : {};
4197
+ const columns = mapcolumns(parsed.headers, mapping);
4198
+ const rows = parsed.rows.map((line) => {
4199
+ const row = {};
4200
+ columns.forEach((column, index) => {
4201
+ row[column.key] = line[index] ?? "";
4202
+ });
4203
+ return row;
4204
+ });
4205
+ const id = randomid();
4206
+ const datasetvalue = { id, name: typeof options.name === "string" && options.name ? options.name : `import-${step.id}`, columns, rows, sources: [], at: Date.now() };
4207
+ await memory.addimport(datasetvalue);
4208
+ await refreshbadge();
4209
+ return { ok: true, summary: `Imported ${rows.length} row${rows.length === 1 ? "" : "s"} and ${columns.length} mapped column${columns.length === 1 ? "" : "s"} from the reviewed csv for fill loops.`, details: { dataset: { id, name: datasetvalue.name, rows: rows.length, columns: columns.length } } };
4210
+ }
4211
+ case "looprows": {
4212
+ const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
4213
+ const inner = resolvedinnerstep(step, plan);
4214
+ if (!inner) throw new Error("A reviewed inner step or step id is required in options.");
4215
+ const variable = typeof options.variable === "string" && options.variable ? options.variable : "row";
4216
+ let completed = 0;
4217
+ let failed = 0;
4218
+ for (const [index, row] of datasetvalue.rows.entries()) {
4219
+ const derived = loopstep(inner, row);
4220
+ const output = await dispatchpagestep(derived, tabid2, origin, plan);
4221
+ if (output?.ok) completed += 1;
4222
+ else failed += 1;
4223
+ await memory.setprogress(recordoutcome(await memory.getprogress(), plan.id, { stepid: step.id, ok: Boolean(output?.ok), summary: `Iteration ${index + 1} of ${datasetvalue.rows.length}: ${output?.summary ?? "no result"}`, details: { iteration: index + 1, variable, variables: loopvariables(row) }, at: Date.now() }, Date.now()));
4224
+ }
4225
+ return { ok: failed === 0, summary: `Looped ${datasetvalue.rows.length} dataset row${datasetvalue.rows.length === 1 ? "" : "s"} as ${variable} variables: ${completed} iteration${completed === 1 ? "" : "s"} completed${failed > 0 ? `, ${failed} failed` : ""}.`, details: { iterations: datasetvalue.rows.length, completed, failed, variable } };
4226
+ }
4227
+ case "exportcsv":
4228
+ case "exportjson":
4229
+ case "exportexcel": {
4230
+ const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
4231
+ const format = step.kind === "exportcsv" ? "csv" : step.kind === "exportjson" ? "json" : "excel";
4232
+ const delimiter = typeof options.delimiter === "string" && options.delimiter.length === 1 ? options.delimiter : ",";
4233
+ const artifact = await storeexport(step.id, { ...datasetvalue, ...typeof options.name === "string" && options.name ? { name: options.name } : {} }, format, delimiter, session, plan.id, origin);
4234
+ return { ok: true, summary: `Exported ${artifact.rowcount} row${artifact.rowcount === 1 ? "" : "s"} of dataset ${datasetvalue.name} to ${artifact.name} with checksum ${artifact.checksum}.`, details: { artifact: { id: artifact.id, name: artifact.name, kind: artifact.kind, checksum: artifact.checksum, rowcount: artifact.rowcount } } };
4235
+ }
4236
+ case "copytable": {
4237
+ const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
4238
+ const content = exportcontent(datasetvalue, "csv", typeof options.delimiter === "string" && options.delimiter.length === 1 ? options.delimiter : ",");
4239
+ await navigator.clipboard.writeText(content);
4240
+ await audit("export", `Copied ${datasetvalue.rows.length} row${datasetvalue.rows.length === 1 ? "" : "s"} of dataset ${datasetvalue.name} to the clipboard under the clipboardWrite capability.`, extra);
4241
+ return { ok: true, summary: `Copied ${datasetvalue.rows.length} row${datasetvalue.rows.length === 1 ? "" : "s"} to the clipboard.`, details: { rows: datasetvalue.rows.length } };
4242
+ }
4243
+ case "pushsheets": {
4244
+ const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
4245
+ const sheet = typeof options.sheet === "string" ? options.sheet : "";
4246
+ const config = (await memory.getsheetendpoints()).find((item) => item.endpoint === sheet || item.origin === new URL(sheet).origin);
4247
+ if (!config) throw new Error(`The reviewed sheet endpoint has not been configured; configure ${sheet} from the review panel first.`);
4248
+ const granted = await chrome.permissions.contains({ origins: [hostpattern(config.origin)] });
4249
+ if (!granted) throw new Error(`The sheet endpoint origin ${config.origin} has not received optional permission.`);
4250
+ const payload = sheetpayload(datasetvalue, sheet);
4251
+ const response = await fetch(config.endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload) });
4252
+ await audit("export", `Pushed ${datasetvalue.rows.length} row${datasetvalue.rows.length === 1 ? "" : "s"} of dataset ${datasetvalue.name} to the reviewed sheet endpoint ${config.origin} with response ${response.status}.`, extra);
4253
+ return { ok: response.ok, summary: response.ok ? `Pushed ${datasetvalue.rows.length} row${datasetvalue.rows.length === 1 ? "" : "s"} to the reviewed sheet endpoint ${config.origin}.` : `The reviewed sheet endpoint answered ${response.status}; the push failed.`, details: { endpoint: config.endpoint, status: response.status, rows: datasetvalue.rows.length } };
4254
+ }
4255
+ case "streamdisk": {
4256
+ const datasetvalue = await loaddataset(typeof options.dataset === "string" ? options.dataset : "");
4257
+ const chunk = typeof options.chunk === "number" && Number.isInteger(options.chunk) && options.chunk > 0 ? options.chunk : datasetvalue.rows.length || 1;
4258
+ const planchunks = chunkplan(datasetvalue.rows.length, chunk);
4259
+ const previous = (await memory.getstreams()).find((item) => item.datasetid === datasetvalue.id && item.done !== true);
4260
+ let state = previous && previous.chunks === planchunks.length ? previous : newstream(datasetvalue, planchunks.length, Date.now());
4261
+ let written = 0;
4262
+ let acknowledged = 0;
4263
+ for (const part of planchunks.slice(state.chunk)) {
4264
+ written += 1;
4265
+ if (backpressure(written, acknowledged)) await new Promise((resolve) => setTimeout(resolve, 0));
4266
+ state = advancestream(state, part, Date.now(), part.to >= datasetvalue.rows.length);
4267
+ await memory.setstream(state);
4268
+ acknowledged += 1;
4269
+ }
4270
+ const artifact = await storeexport(step.id, datasetvalue, "csv", ",", session, plan.id, origin);
4271
+ await audit("stream", `Streamed ${datasetvalue.rows.length} row${datasetvalue.rows.length === 1 ? "" : "s"} of dataset ${datasetvalue.name} to disk in ${planchunks.length} reviewed chunk${planchunks.length === 1 ? "" : "s"} of ${chunk} row${chunk === 1 ? "" : "s"} with backpressure; the stream state stays persisted for resume.`, extra);
4272
+ return { ok: true, summary: `Streamed ${datasetvalue.rows.length} row${datasetvalue.rows.length === 1 ? "" : "s"} to disk in ${planchunks.length} chunk${planchunks.length === 1 ? "" : "s"} and landed the artifact ${artifact.name}.`, details: { chunks: planchunks.length, chunk, rows: datasetvalue.rows.length, artifact: { id: artifact.id, name: artifact.name, checksum: artifact.checksum } } };
4273
+ }
4274
+ case "logprovenance": {
4275
+ const reference = typeof options.artifact === "string" ? options.artifact : "";
4276
+ const artifact = (await memory.getexports()).find((item) => item.id === reference || item.name === reference);
4277
+ if (!artifact) throw new Error(`No exported artifact ${reference} exists yet.`);
4278
+ const record2 = provenancefor(artifact, origin, step.id, Date.now());
4279
+ await memory.addprovenance(record2);
4280
+ await audit("provenance", `Provenance of artifact ${artifact.name}: rows ${record2.rowstart} to ${record2.rowend}, checksum ${record2.checksum}, source ${record2.url}.`, extra);
4281
+ return { ok: true, summary: `Logged the provenance of ${artifact.name} for audit: rows ${record2.rowstart} to ${record2.rowend} with checksum ${record2.checksum}.`, details: { provenance: record2 } };
4282
+ }
4283
+ default:
4284
+ return { ok: false, summary: "Unsupported forms and data step." };
4285
+ }
4286
+ }
3588
4287
  async function enforcewindowreview(step, session, plan) {
3589
4288
  const windowid = step.value && /^\d+$/.test(step.value) ? Number.parseInt(step.value, 10) : 0;
3590
4289
  const progress = plan ? await memory.getprogress() : void 0;
@@ -3620,8 +4319,9 @@ async function refreshbadge() {
3620
4319
  const queues = await memory.getnavqueues();
3621
4320
  const badges = await memory.getbadges();
3622
4321
  const prompts = (await memory.gettickets()).filter((ticket) => ticket.approved === void 0).length;
4322
+ const datasets = (await memory.getdatasets()).length;
3623
4323
  const tasktabs2 = new Set(badges.map((badge) => badge.tabid)).size;
3624
- const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts;
4324
+ const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + datasets;
3625
4325
  await chrome.action.setBadgeText({ text: total > 0 ? String(total) : "" }).catch(() => {
3626
4326
  });
3627
4327
  }
@@ -3645,6 +4345,8 @@ async function executestep(stepid) {
3645
4345
  }
3646
4346
  if (istabscommandkind(step.kind)) {
3647
4347
  output = await executetabscommand(step, session, plan, tab.id);
4348
+ } else if (isdatasetkind(step.kind)) {
4349
+ output = await executedatastep(step, session, plan, tab.id, origin);
3648
4350
  } else if (isformkind(step.kind)) {
3649
4351
  output = await executeformstep(step, session, plan, tab.id, origin);
3650
4352
  } else if (isbrowserkind(step.kind)) {
@@ -3752,6 +4454,12 @@ async function grantcapability(permission) {
3752
4454
  await audit("capability", `Capability ${permission} granted by the user.`);
3753
4455
  return refreshcapabilities();
3754
4456
  }
4457
+ async function extractionreportValue() {
4458
+ return extractionreport({ sessions: await memory.getextractsessions() });
4459
+ }
4460
+ async function provenancereportValue() {
4461
+ return provenancereport({ records: await memory.getprovenances() });
4462
+ }
3755
4463
  async function handlerequest(message, sender) {
3756
4464
  if (!extensionpage(sender)) throw new Error("Requests are accepted only from Devthink extension pages.");
3757
4465
  const input = message;
@@ -3804,13 +4512,25 @@ async function handlerequest(message, sender) {
3804
4512
  const captchas = await memory.getcaptchas();
3805
4513
  const detections = await memory.getdetections();
3806
4514
  const codeentry = await memory.getcodevalue();
4515
+ const datasets = await memory.getdatasets();
4516
+ const imports = await memory.getimports();
4517
+ const extractsessions = await memory.getextractsessions();
4518
+ const streams = await memory.getstreams();
4519
+ const exports = (await memory.getexports()).map((artifact) => ({ id: artifact.id, kind: artifact.kind, name: artifact.name, stepid: artifact.stepid, rowcount: artifact.rowcount, checksum: artifact.checksum, at: artifact.at }));
4520
+ const provenances = await memory.getprovenances();
4521
+ const taskrules = await memory.gettaskrules();
4522
+ const sheetendpoints = await memory.getsheetendpoints();
4523
+ const sheetgrants = [];
4524
+ for (const config of sheetendpoints) {
4525
+ sheetgrants.push({ ...config, granted: await chrome.permissions.contains({ origins: [hostpattern(config.origin)] }).catch(() => false) });
4526
+ }
3807
4527
  const clones = clonetabs(tabs);
3808
4528
  const taskgauge = tasktabgauge(tabs.filter((tab) => badges.some((badge) => badge.tabid === tab.tabid)).length, tasktabceiling(await memory.getsettings()));
3809
4529
  const report = await buildtabreport(tabs);
3810
4530
  const livetab = session ? await chrome.tabs.get(session.tabid).catch(() => void 0) : void 0;
3811
4531
  const waitprofile = session ? waitprofiles.find((record2) => record2.origin === session.origin) : void 0;
3812
4532
  const livestate = { phase: livetab?.status === "loading" ? "loading" : "complete", ...navrecords[0] ? { finalurl: navrecords[0].finalurl, redirects: navrecords[0].chain } : {} };
3813
- return { config: await memory.getconfig(), session, plan, progress: plan && progress?.planid === plan.id ? progress : void 0, diagnostic: await memory.getdiagnostic(), audit: await memory.getaudit(), capabilities: await refreshcapabilities(), outcomes: await memory.getoutcomes(), holds: heldkeysreport({ tabid: session?.tabid ?? 0, holds }), dialogs: await memory.getdialogs(), retries: await memory.getretries(), ...signals ? { signals: signalsreport({ signals }) } : { signals: signalsreport({}) }, banners: await memory.getbanners(), mutationevents: await memory.getmutationevents(), focusevents: await memory.getfocusevents(), diffs: await memory.getdiffs(), selectors: await memory.getselectors(), ...a11y ? { a11y } : {}, ...reader ? { reader } : {}, ...map ? { map } : {}, trail: trailreport({ ...session ? { sessionid: session.id } : {}, trail }), navrecords, ratestates, safeties, curated, waitprofiles, auths, navcontrol, navqueues, artifacts, navstate: livestate, ...waitprofile ? { waitprofile } : {}, offline: !navigator.onLine, tabs, windows, layouts: layoutreport({ layouts }), tabgroups, tabmetas, badges, snapshots, closedtabs, tabwatchevents, clones, tasktabgauge: taskgauge, ...controltab ? { controltab } : {}, tabreport: report, profiles, tickets, wizards: wizardreport({ ...session ? { sessionid: session.id } : {}, wizards, picks }), picks, errorreports, captchas, detections, ...codeentry !== void 0 ? { codeentry: true } : {} };
4533
+ return { config: await memory.getconfig(), session, plan, progress: plan && progress?.planid === plan.id ? progress : void 0, diagnostic: await memory.getdiagnostic(), audit: await memory.getaudit(), capabilities: await refreshcapabilities(), outcomes: await memory.getoutcomes(), holds: heldkeysreport({ tabid: session?.tabid ?? 0, holds }), dialogs: await memory.getdialogs(), retries: await memory.getretries(), ...signals ? { signals: signalsreport({ signals }) } : { signals: signalsreport({}) }, banners: await memory.getbanners(), mutationevents: await memory.getmutationevents(), focusevents: await memory.getfocusevents(), diffs: await memory.getdiffs(), selectors: await memory.getselectors(), ...a11y ? { a11y } : {}, ...reader ? { reader } : {}, ...map ? { map } : {}, trail: trailreport({ ...session ? { sessionid: session.id } : {}, trail }), navrecords, ratestates, safeties, curated, waitprofiles, auths, navcontrol, navqueues, artifacts, navstate: livestate, ...waitprofile ? { waitprofile } : {}, offline: !navigator.onLine, tabs, windows, layouts: layoutreport({ layouts }), tabgroups, tabmetas, badges, snapshots, closedtabs, tabwatchevents, clones, tasktabgauge: taskgauge, ...controltab ? { controltab } : {}, tabreport: report, profiles, tickets, wizards: wizardreport({ ...session ? { sessionid: session.id } : {}, wizards, picks }), picks, errorreports, captchas, detections, ...codeentry !== void 0 ? { codeentry: true } : {}, datasets, imports, extractsessions, streams, exports, provenances, taskrules, sheetendpoints: sheetgrants };
3814
4534
  }
3815
4535
  case "capabilities":
3816
4536
  return refreshcapabilities();
@@ -4086,6 +4806,61 @@ async function handlerequest(message, sender) {
4086
4806
  if (!report) throw new Error("No error report has been collected yet.");
4087
4807
  return JSON.parse(errorreportresponse({ report, plan }));
4088
4808
  }
4809
+ case "configuresheet": {
4810
+ const inputsheet = message;
4811
+ const config = normalizeendpoint(inputsheet.endpoint ?? "");
4812
+ const granted = await chrome.permissions.contains({ origins: [hostpattern(config.origin)] });
4813
+ if (!granted) throw new Error("The sheet endpoint origin has not received optional permission.");
4814
+ const record2 = { endpoint: config.endpoint, origin: config.origin, configuredat: Date.now() };
4815
+ await memory.setsheetendpoint(record2);
4816
+ await audit("configure", `Configured the reviewed sheet endpoint ${config.origin} for data pushes; pushes need the explicit reviewed flag.`);
4817
+ return record2;
4818
+ }
4819
+ case "exportdataset": {
4820
+ const session = await memory.getsession();
4821
+ if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Data exports stay behind the consent gate of an active session.");
4822
+ const { tab, origin } = await activecontext();
4823
+ const gate = exportgranted(session, origin);
4824
+ if (!gate.allowed) throw new Error(gate.reason);
4825
+ const inputexport = message;
4826
+ const format = inputexport.format === "json" ? "json" : inputexport.format === "excel" ? "excel" : "csv";
4827
+ const datasetvalue = await loaddataset(inputexport.datasetid ?? "");
4828
+ const artifact = await storeexport("panel", { ...datasetvalue, ...inputexport.name?.trim() ? { name: inputexport.name.trim() } : {} }, format, ",", session, "", origin);
4829
+ void tab;
4830
+ return { id: artifact.id, kind: artifact.kind, name: artifact.name, rowcount: artifact.rowcount, checksum: artifact.checksum, at: artifact.at };
4831
+ }
4832
+ case "importcsv": {
4833
+ const session = await memory.getsession();
4834
+ if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Csv imports stay behind the consent gate of an active session.");
4835
+ const inputimport = message;
4836
+ const parsed = parsecsv(inputimport.csv ?? "");
4837
+ if (parsed.headers.length === 0) throw new Error("The reviewed csv needs a header line.");
4838
+ const columns = mapcolumns(parsed.headers, inputimport.mapping ?? {});
4839
+ const rows = parsed.rows.map((line) => {
4840
+ const row = {};
4841
+ columns.forEach((column, index) => {
4842
+ row[column.key] = line[index] ?? "";
4843
+ });
4844
+ return row;
4845
+ });
4846
+ const datasetvalue = { id: randomid(), name: inputimport.name?.trim() || `import-${Date.now()}`, columns, rows, sources: [], at: Date.now() };
4847
+ await memory.addimport(datasetvalue);
4848
+ await audit("scrape", `The review panel imported ${rows.length} row${rows.length === 1 ? "" : "s"} from the reviewed csv as dataset ${datasetvalue.name} for fill loops.`, { sessionid: session.id });
4849
+ await refreshbadge();
4850
+ return { id: datasetvalue.id, name: datasetvalue.name, rows: rows.length, columns: columns.length };
4851
+ }
4852
+ case "dataset": {
4853
+ const plan = await memory.getplan();
4854
+ if (!plan) throw new Error("No plan is available for a dataset envelope.");
4855
+ const inputdataset = message;
4856
+ const datasetvalue = await memory.getdataset(inputdataset.datasetid ?? "");
4857
+ if (!datasetvalue) throw new Error("No dataset has been captured yet.");
4858
+ return JSON.parse(datasetresponse({ dataset: datasetvalue, plan, ...typeof inputdataset.sample === "number" ? { sample: inputdataset.sample } : {} }));
4859
+ }
4860
+ case "extraction":
4861
+ return extractionreportValue();
4862
+ case "provenance":
4863
+ return provenancereportValue();
4089
4864
  case "stop": {
4090
4865
  const session = await memory.getsession();
4091
4866
  if (session) await memory.setsession({ ...session, stoppedat: Date.now() });