@trilogy-data/trilogy-studio-components 0.1.24 → 0.1.26

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 (116) hide show
  1. package/README.md +5 -0
  2. package/dist/AssetAutoImporter-CBzvBaOt.js +405 -0
  3. package/dist/ChatCreatorModal-CL-jBnc3.js +218 -0
  4. package/dist/ChatModelSelect-Do-VRUFc.js +40 -0
  5. package/dist/{CommunityModels-CsDDXyIL.js → CommunityModels-Cmro0hV3.js} +101 -70
  6. package/dist/{Dashboard-BJfgRfRy.js → Dashboard-BH9ZwMVJ.js} +8 -8
  7. package/dist/{DashboardCreatorInline-wAlx6oH7.js → DashboardCreatorInline-CDU7kLy3.js} +3 -3
  8. package/dist/{DashboardImportSelector-D6ufVZIk.js → DashboardImportSelector-DQSvu9T-.js} +1 -1
  9. package/dist/{DashboardMobile-DoOyBHN7.js → DashboardMobile-BUPtdN2V.js} +4 -4
  10. package/dist/{Editor-Bs-h7edF.js → Editor-DQsYpkwl.js} +6 -6
  11. package/dist/{GlobalChatPanel-YsDZGxg8.js → GlobalChatPanel-DhnOaige.js} +155 -119
  12. package/dist/{IDE-CLF-KH2r.js → IDE-HLa8vybk.js} +9 -9
  13. package/dist/{JobsView-D1pp2xi5.js → JobsView-B97HBoIE.js} +8 -8
  14. package/dist/{LLMChat-CwM_brSg.js → LLMChat-CxJcPLEG.js} +1 -1
  15. package/dist/{LLMView-BiwCZb3Q.js → LLMView-C1MUf0c2.js} +21 -21
  16. package/dist/{MobileIDE-DrWh_OlB.js → MobileIDE-BpqXoaMR.js} +258 -253
  17. package/dist/{ModelCard-CsW4UwZI.js → ModelCard-BhN25jaV.js} +10 -10
  18. package/dist/{ModelCreator-C2douDis.js → ModelCreator-CCdC7EXt.js} +1 -1
  19. package/dist/{ModelView-D7ENFV2U.js → ModelView-DUDA_PtZ.js} +3 -3
  20. package/dist/{PageLoading-C5WK5i5Y.js → PageLoading-BzniJM0C.js} +622 -619
  21. package/dist/{ResultComponent-1MQlbELb.js → ResultComponent-CAn_r3BL.js} +3 -3
  22. package/dist/{Results-DXIYS4iW.js → Results-BJtlwebY.js} +3 -3
  23. package/dist/{SymbolsPane-BM6dUN-h.js → SymbolsPane-KYSwN9Sv.js} +1 -1
  24. package/dist/{TutorialPage-HXbXCznh.js → TutorialPage-NRcee6Dx.js} +8 -8
  25. package/dist/{VegaLiteChart-CS4h_t-K.js → VegaLiteChart-D1slfOUY.js} +11227 -11195
  26. package/dist/{WelcomePage-CLKvAvk-.js → WelcomePage-D5mU2x1L.js} +1 -1
  27. package/dist/components/AssetAutoImporter.vue.d.ts.map +1 -1
  28. package/dist/components/TipsCTA.vue.d.ts +1 -1
  29. package/dist/components/VegaLiteChart.vue.d.ts.map +1 -1
  30. package/dist/components/chartHelpers.d.ts +9 -1
  31. package/dist/components/chartHelpers.d.ts.map +1 -1
  32. package/dist/components/chartRenderManager.d.ts +4 -1
  33. package/dist/components/chartRenderManager.d.ts.map +1 -1
  34. package/dist/components/community/AddStoreModal.vue.d.ts +1 -1
  35. package/dist/components/community/AddStoreModal.vue.d.ts.map +1 -1
  36. package/dist/components/community/CommunityModelCard.vue.d.ts.map +1 -1
  37. package/dist/components/editor/Editor.vue.d.ts.map +1 -1
  38. package/dist/components/jobs/JobsAddStoreModal.vue.d.ts.map +1 -1
  39. package/dist/components/llm/ChatCreatorModal.vue.d.ts +17 -1
  40. package/dist/components/llm/ChatCreatorModal.vue.d.ts.map +1 -1
  41. package/dist/components/llm/ChatModelSelect.vue.d.ts +13 -0
  42. package/dist/components/llm/ChatModelSelect.vue.d.ts.map +1 -0
  43. package/dist/components/llm/GlobalChatPanel.vue.d.ts.map +1 -1
  44. package/dist/components/sidebar/CommunityModelList.vue.d.ts +33 -50
  45. package/dist/components/sidebar/CommunityModelList.vue.d.ts.map +1 -1
  46. package/dist/components/sidebar/CommunityModelListItem.vue.d.ts.map +1 -1
  47. package/dist/components/sidebar/EditorList.vue.d.ts +32 -49
  48. package/dist/components/sidebar/EditorList.vue.d.ts.map +1 -1
  49. package/dist/components/sidebar/Sidebar.vue.d.ts +65 -99
  50. package/dist/components/sidebar/Sidebar.vue.d.ts.map +1 -1
  51. package/dist/dashboard.js +52 -52
  52. package/dist/{dashboardStore-BzmI4OCV.js → dashboardStore-ppM6nGks.js} +33 -33
  53. package/dist/dashboards/layerSpec.d.ts +38 -4
  54. package/dist/dashboards/layerSpec.d.ts.map +1 -1
  55. package/dist/dashboards/lineAreaSpec.d.ts +8 -80
  56. package/dist/dashboards/lineAreaSpec.d.ts.map +1 -1
  57. package/dist/dashboards/mapSpec.d.ts +1 -0
  58. package/dist/dashboards/mapSpec.d.ts.map +1 -1
  59. package/dist/dashboards/spec.d.ts.map +1 -1
  60. package/dist/data/tutorial/documentation.d.ts.map +1 -1
  61. package/dist/data/tutorial/startupScripts.d.ts +3 -0
  62. package/dist/data/tutorial/startupScripts.d.ts.map +1 -0
  63. package/dist/{demoSetup-vmiMeH4_.js → demoSetup-mwa-RDC6.js} +19 -18
  64. package/dist/{editorRefinementRuntime-hC3u8GMO.js → editorRefinementRuntime-DoP9P5J2.js} +892 -871
  65. package/dist/{editorRefinementToolExecutor-Bomrpmns.js → editorRefinementToolExecutor-CVY3FA7k.js} +392 -267
  66. package/dist/{editorStore-Bj3Qnatp.js → editorStore-BhhmXbi-.js} +230 -183
  67. package/dist/entry.dashboard.d.ts +1 -0
  68. package/dist/entry.dashboard.d.ts.map +1 -1
  69. package/dist/{globalChatRuntime-DXI2KyGn.js → globalChatRuntime-DWZxeS57.js} +13 -13
  70. package/dist/{helpers-CFp2QCvv.js → helpers-BLQAgX3C.js} +17 -16
  71. package/dist/llm/chatAgentPrompt.d.ts.map +1 -1
  72. package/dist/llm/data/constants.d.ts +1 -1
  73. package/dist/llm/data/constants.d.ts.map +1 -1
  74. package/dist/llm/data/trilogySyntax.generated.d.ts +7 -1
  75. package/dist/llm/data/trilogySyntax.generated.d.ts.map +1 -1
  76. package/dist/llm/docsIndex.d.ts.map +1 -1
  77. package/dist/llm/openai.d.ts +2 -1
  78. package/dist/llm/openai.d.ts.map +1 -1
  79. package/dist/{llm-BuEsqH9Q.js → llm-BBOVWb-F.js} +7 -5
  80. package/dist/llm.js +3 -3
  81. package/dist/{llmStore-x5lRrKgx.js → llmStore-DJM2JhzY.js} +2 -2
  82. package/dist/{localStorage-C-ZtI6YA.js → localStorage-D9uyOFEO.js} +14 -14
  83. package/dist/models/helpers.d.ts.map +1 -1
  84. package/dist/monaco.js +1 -1
  85. package/dist/{reconcile-E5LR5p_f.js → reconcile-Dyf4UZmj.js} +1 -1
  86. package/dist/{remoteStoreSync-MwuJpHHt.js → remoteStoreSync-Z-yjlYtI.js} +1 -1
  87. package/dist/remotes/displayHelpers.d.ts.map +1 -1
  88. package/dist/remotes/genericStoreMetadata.d.ts +1 -0
  89. package/dist/remotes/genericStoreMetadata.d.ts.map +1 -1
  90. package/dist/remotes/models.d.ts +37 -5
  91. package/dist/remotes/models.d.ts.map +1 -1
  92. package/dist/remotes/staticStorePresets.d.ts +22 -0
  93. package/dist/remotes/staticStorePresets.d.ts.map +1 -0
  94. package/dist/remotes/storeService.d.ts +25 -4
  95. package/dist/remotes/storeService.d.ts.map +1 -1
  96. package/dist/stores/communityApiStore.d.ts +32 -60
  97. package/dist/stores/communityApiStore.d.ts.map +1 -1
  98. package/dist/stores/connectionStore.d.ts.map +1 -1
  99. package/dist/stores/resolver.d.ts +5 -0
  100. package/dist/stores/resolver.d.ts.map +1 -1
  101. package/dist/stores/urlStore.d.ts +1 -0
  102. package/dist/stores/urlStore.d.ts.map +1 -1
  103. package/dist/{stores-D-gHjj-_.js → stores-BT9hTLny.js} +47 -47
  104. package/dist/stores.js +7 -7
  105. package/dist/trilogy-studio-components.css +1 -1
  106. package/dist/{urlStore-Dgmwx1YI.js → urlStore-52HG6lsX.js} +1 -0
  107. package/dist/{useChatWithTools-BlCJrJT7.js → useChatWithTools-B0HZ_DVh.js} +1 -1
  108. package/dist/{useDashboard-CVlbV17V.js → useDashboard-CdSnPe_6.js} +186 -186
  109. package/dist/{useGlobalChatPanel-CNARTFYu.js → useGlobalChatPanel-D_0luajQ.js} +2 -2
  110. package/dist/views/MobileIDE.vue.d.ts.map +1 -1
  111. package/dist/views/TutorialPage.vue.d.ts +32 -49
  112. package/dist/views/TutorialPage.vue.d.ts.map +1 -1
  113. package/dist/views.js +16 -16
  114. package/package.json +1 -1
  115. package/dist/AssetAutoImporter-BD1P91Hq.js +0 -403
  116. package/dist/ChatCreatorModal-BTqY06B6.js +0 -177
@@ -299,7 +299,7 @@ var c = class e {
299
299
  size() {
300
300
  return this.cache.size;
301
301
  }
302
- }, y = class {
302
+ }, y = class e {
303
303
  settingStore;
304
304
  validateCache;
305
305
  formatCache;
@@ -338,17 +338,28 @@ var c = class e {
338
338
  buildResolverUrl(e) {
339
339
  return `${this.settingStore.settings.trilogyResolver.replace(/\/+$/, "")}/${e}`;
340
340
  }
341
- async fetchWithErrorHandling(e, t) {
341
+ static RETRY_STATUSES = /* @__PURE__ */ new Set([503]);
342
+ static MAX_RETRIES = 3;
343
+ static MAX_RETRY_DELAY_MS = 5e3;
344
+ delay(e) {
345
+ return new Promise((t) => setTimeout(t, e));
346
+ }
347
+ retryDelayMs(t, n) {
348
+ let r = Number(t.headers?.get?.("retry-after")), i = Number.isFinite(r) && r > 0 ? r * 1e3 : 500 * 2 ** n;
349
+ return Math.min(i, e.MAX_RETRY_DELAY_MS) * (.75 + Math.random() * .5);
350
+ }
351
+ async fetchWithErrorHandling(t, n) {
342
352
  try {
343
- let n = await fetch(e, t);
344
- if (!n.ok) {
345
- let e = await n.json().catch(() => ({}));
353
+ let r = await fetch(t, n);
354
+ for (let i = 0; !r.ok && e.RETRY_STATUSES.has(r.status) && i < e.MAX_RETRIES; i++) await this.delay(this.retryDelayMs(r, i)), r = await fetch(t, n);
355
+ if (!r.ok) {
356
+ let e = await r.json().catch(() => ({}));
346
357
  throw {
347
- message: `HTTP error ${n.status}`,
358
+ message: `HTTP error ${r.status}`,
348
359
  response: { data: e }
349
360
  };
350
361
  }
351
- return { data: await n.json() };
362
+ return { data: await r.json() };
352
363
  } catch (e) {
353
364
  throw console.log(e), Error(this.getErrorMessage(e));
354
365
  }
@@ -926,9 +937,9 @@ function De(e) {
926
937
  Object.entries(e).forEach(([e, n]) => {
927
938
  t[e] || (t[e] = []), t[e].push(n);
928
939
  });
929
- }), Object.entries(t).map(([e, t]) => t.length > 1 ? `(${t.map((t) => ke(e, t)).join(" OR ")})` : ke(e, t[0])).join(" AND ");
940
+ }), Object.entries(t).map(([e, t]) => t.length > 1 ? `(${t.map((t) => C(e, t)).join(" OR ")})` : C(e, t[0])).join(" AND ");
930
941
  }
931
- return Object.keys(e).length === 0 ? "" : Object.entries(e).map(([e, t]) => ke(e, t)).join(" AND ");
942
+ return Object.keys(e).length === 0 ? "" : Object.entries(e).map(([e, t]) => C(e, t)).join(" AND ");
932
943
  }
933
944
  function Oe(e) {
934
945
  if (e instanceof Date) return `'${e.getFullYear()}-${String(e.getMonth() + 1).padStart(2, "0")}-${String(e.getDate()).padStart(2, "0")}'::date`;
@@ -937,23 +948,23 @@ function Oe(e) {
937
948
  if (typeof e == "boolean") return e ? "TRUE" : "FALSE";
938
949
  if (typeof e == "number") return e.toString();
939
950
  }
940
- function ke(e, t) {
951
+ function C(e, t) {
941
952
  return t === null ? `${e} IS NULL` : typeof t == "string" ? `${e}='''${t.replace(/'/g, "!`")}'''` : Array.isArray(t) ? `${e} between ${Oe(t[0])} and ${Oe(t[t.length - 1])}` : typeof t == "number" || typeof t == "boolean" ? `${e}=${t}` : t === void 0 ? `${e} IS NULL` : `${e}='${JSON.stringify(t).replace(/'/g, "!`")}'`;
942
953
  }
943
954
  //#endregion
944
955
  //#region dashboards/crossFilters.ts
945
- function Ae(e = []) {
956
+ function ke(e = []) {
946
957
  let t = /* @__PURE__ */ new Set();
947
958
  for (let n of e) n?.label && t.add(n.label);
948
959
  return Array.from(t);
949
960
  }
950
- function C(e) {
961
+ function w(e) {
951
962
  return { ...e };
952
963
  }
953
- function w(e) {
964
+ function T(e) {
954
965
  return { ...e };
955
966
  }
956
- function je(e, t) {
967
+ function Ae(e, t) {
957
968
  if (e.op !== t.op) return !1;
958
969
  switch (e.op) {
959
970
  case "is_null": return !0;
@@ -968,31 +979,31 @@ function je(e, t) {
968
979
  }
969
980
  }
970
981
  }
971
- function T(e, t) {
982
+ function E(e, t) {
972
983
  let n = Object.keys(e).sort(), r = Object.keys(t).sort();
973
- return n.length === r.length && n.every((n, i) => n === r[i] && je(e[n], t[n]));
984
+ return n.length === r.length && n.every((n, i) => n === r[i] && Ae(e[n], t[n]));
974
985
  }
975
- function Me(e, t) {
986
+ function je(e, t) {
976
987
  return Array.isArray(e) && Array.isArray(t) ? e.length === t.length && e.every((e, n) => e === t[n]) : e === t;
977
988
  }
978
- function Ne(e, t) {
989
+ function Me(e, t) {
979
990
  let n = Object.keys(e).sort(), r = Object.keys(t).sort();
980
- return n.length === r.length && n.every((n, i) => n === r[i] && Me(e[n], t[n]));
991
+ return n.length === r.length && n.every((n, i) => n === r[i] && je(e[n], t[n]));
981
992
  }
982
- function Pe(e, t, n, r) {
993
+ function Ne(e, t, n, r) {
983
994
  let i = e.filter((e) => e.source !== t), a = e.filter((e) => e.source === t);
984
995
  if (r === "remove") return i;
985
996
  if (r === "add") return [...i, {
986
997
  source: t,
987
- value: w(n)
998
+ value: T(n)
988
999
  }];
989
- let o = a.some((e) => Ne(e.value, n)) ? a.filter((e) => !Ne(e.value, n)) : [...a, {
1000
+ let o = a.some((e) => Me(e.value, n)) ? a.filter((e) => !Me(e.value, n)) : [...a, {
990
1001
  source: t,
991
- value: w(n)
1002
+ value: T(n)
992
1003
  }];
993
1004
  return [...i, ...o];
994
1005
  }
995
- function Fe(e, t, n) {
1006
+ function Pe(e, t, n) {
996
1007
  if (t.size === 0) return n && e.startsWith("local.") ? e.replace(/^local\./, "") : e;
997
1008
  if (t.has(e)) return e;
998
1009
  if (e.startsWith("local.")) {
@@ -1001,7 +1012,7 @@ function Fe(e, t, n) {
1001
1012
  }
1002
1013
  return null;
1003
1014
  }
1004
- function Ie(e, t = [], n = {}) {
1015
+ function Fe(e, t = [], n = {}) {
1005
1016
  let r = new Set(t), i = n.normalizeLocalFields ?? !1;
1006
1017
  return Object.entries(e).reduce((e, [t, n]) => {
1007
1018
  if (typeof n != "object" || !n || ![
@@ -1010,11 +1021,11 @@ function Ie(e, t = [], n = {}) {
1010
1021
  "in",
1011
1022
  "is_null"
1012
1023
  ].includes(n.op)) return e;
1013
- let a = Fe(t, r, i);
1024
+ let a = Pe(t, r, i);
1014
1025
  return a && (e[a] = n), e;
1015
1026
  }, {});
1016
1027
  }
1017
- function E(e) {
1028
+ function D(e) {
1018
1029
  if (!e.length) return {
1019
1030
  filterStrings: [],
1020
1031
  parameters: {}
@@ -1044,8 +1055,8 @@ function E(e) {
1044
1055
  parameters: r
1045
1056
  };
1046
1057
  }
1047
- function D(e) {
1048
- let t = e.conceptFilters || [], n = (e.filters || []).filter((e) => e.source !== "cross"), { filterStrings: r, parameters: i } = E(t), a = r.join(" AND ");
1058
+ function O(e) {
1059
+ let t = e.conceptFilters || [], n = (e.filters || []).filter((e) => e.source !== "cross"), { filterStrings: r, parameters: i } = D(t), a = r.join(" AND ");
1049
1060
  a && n.push({
1050
1061
  source: "cross",
1051
1062
  value: a,
@@ -1054,43 +1065,43 @@ function D(e) {
1054
1065
  let o = JSON.stringify(n) !== JSON.stringify(e.filters || []);
1055
1066
  return e.filters = n, o;
1056
1067
  }
1057
- function Le(e, t, n, r) {
1068
+ function Ie(e, t, n, r) {
1058
1069
  let i = e.filter((e) => e.source !== t), a = e.filter((e) => e.source === t);
1059
1070
  if (r === "remove") return i;
1060
1071
  if (r === "add") return [...i, {
1061
1072
  source: t,
1062
- value: C(n)
1073
+ value: w(n)
1063
1074
  }];
1064
- let o = a.some((e) => T(e.value, n)) ? a.filter((e) => !T(e.value, n)) : [...a, {
1075
+ let o = a.some((e) => E(e.value, n)) ? a.filter((e) => !E(e.value, n)) : [...a, {
1065
1076
  source: t,
1066
- value: C(n)
1077
+ value: w(n)
1067
1078
  }];
1068
1079
  return [...i, ...o];
1069
1080
  }
1070
- function Re(e, t, n, r, i) {
1081
+ function Le(e, t, n, r, i) {
1071
1082
  let a = [];
1072
1083
  return Object.entries(e).forEach(([e, o]) => {
1073
1084
  if (e === t) {
1074
- let e = Pe(o.chartFilters || [], t, r, i);
1085
+ let e = Ne(o.chartFilters || [], t, r, i);
1075
1086
  JSON.stringify(e) !== JSON.stringify(o.chartFilters || []) && (o.chartFilters = e);
1076
1087
  return;
1077
1088
  }
1078
1089
  if (!o.allowCrossFilter) return;
1079
- let s = Le(o.conceptFilters || [], t, n, i), c = JSON.stringify(s) !== JSON.stringify(o.conceptFilters || []);
1090
+ let s = Ie(o.conceptFilters || [], t, n, i), c = JSON.stringify(s) !== JSON.stringify(o.conceptFilters || []);
1080
1091
  c && (o.conceptFilters = s);
1081
- let l = D(o);
1092
+ let l = O(o);
1082
1093
  (c || l) && a.push(e);
1083
1094
  }), a;
1084
1095
  }
1085
- function ze(e, t, n) {
1096
+ function Re(e, t, n) {
1086
1097
  let r = e[t];
1087
1098
  if (!r) return !1;
1088
1099
  let i = (r.conceptFilters || []).filter((e) => e.source !== n), a = JSON.stringify(i) !== JSON.stringify(r.conceptFilters || []);
1089
1100
  a && (r.conceptFilters = i);
1090
- let o = D(r);
1101
+ let o = O(r);
1091
1102
  return a || o;
1092
1103
  }
1093
- function Be(e, t) {
1104
+ function ze(e, t) {
1094
1105
  let n = [];
1095
1106
  return Object.entries(e).forEach(([e, r]) => {
1096
1107
  if (e === t) {
@@ -1099,27 +1110,27 @@ function Be(e, t) {
1099
1110
  }
1100
1111
  let i = (r.conceptFilters || []).filter((e) => e.source !== t), a = JSON.stringify(i) !== JSON.stringify(r.conceptFilters || []);
1101
1112
  a && (r.conceptFilters = i);
1102
- let o = D(r);
1113
+ let o = O(r);
1103
1114
  (a || o) && n.push(e);
1104
1115
  }), n;
1105
1116
  }
1106
- function Ve(e) {
1117
+ function Be(e) {
1107
1118
  let t = [];
1108
1119
  return Object.entries(e).forEach(([e, n]) => {
1109
1120
  let r = !!n.conceptFilters?.length || !!n.chartFilters?.length || !!n.filters?.length;
1110
1121
  n.conceptFilters = [], n.chartFilters = [], n.filters = [], r && t.push(e);
1111
1122
  }), t;
1112
1123
  }
1113
- function He(e = {}) {
1124
+ function Ve(e = {}) {
1114
1125
  let t = /* @__PURE__ */ new Map(), n = () => {
1115
1126
  let t = e.validFields ? o(e.validFields) : [];
1116
1127
  return new Set(t);
1117
1128
  }, r = (t) => {
1118
- let r = Ie(t.filters, n(), { normalizeLocalFields: e.normalizeLocalFields ?? !0 });
1129
+ let r = Fe(t.filters, n(), { normalizeLocalFields: e.normalizeLocalFields ?? !0 });
1119
1130
  return {
1120
1131
  source: t.source,
1121
1132
  filters: r,
1122
- chart: w(t.chart || {}),
1133
+ chart: T(t.chart || {}),
1123
1134
  append: t.append
1124
1135
  };
1125
1136
  };
@@ -1127,8 +1138,8 @@ function He(e = {}) {
1127
1138
  getSelections() {
1128
1139
  return Array.from(t.values()).flat().map((e) => ({
1129
1140
  source: e.source,
1130
- filters: C(e.filters),
1131
- chart: w(e.chart || {}),
1141
+ filters: w(e.filters),
1142
+ chart: T(e.chart || {}),
1132
1143
  append: e.append
1133
1144
  }));
1134
1145
  },
@@ -1141,7 +1152,7 @@ function He(e = {}) {
1141
1152
  let o = t.get(e.source) || [];
1142
1153
  if (a === "remove") return t.delete(e.source), i.filters;
1143
1154
  if (a === "add") return t.set(e.source, [i]), i.filters;
1144
- let s = o.some((e) => T(e.filters, i.filters)) ? o.filter((e) => !T(e.filters, i.filters)) : [...o, i];
1155
+ let s = o.some((e) => E(e.filters, i.filters)) ? o.filter((e) => !E(e.filters, i.filters)) : [...o, i];
1145
1156
  return s.length === 0 ? t.delete(e.source) : t.set(e.source, s), i.filters;
1146
1157
  },
1147
1158
  clearSource(e) {
@@ -1156,14 +1167,14 @@ function He(e = {}) {
1156
1167
  getSqlFilterInputsFor(e) {
1157
1168
  return Array.from(t.entries()).filter(([t]) => t !== e).flatMap(([e, t]) => t.map((t) => ({
1158
1169
  source: e,
1159
- value: C(t.filters)
1170
+ value: w(t.filters)
1160
1171
  })));
1161
1172
  },
1162
1173
  getChartSelectionsFor(e) {
1163
- return (t.get(e) || []).map((e) => w(e.chart || {}));
1174
+ return (t.get(e) || []).map((e) => T(e.chart || {}));
1164
1175
  },
1165
1176
  getFilterExpressionFor(e) {
1166
- let { filterStrings: t } = E(this.getSqlFilterInputsFor(e));
1177
+ let { filterStrings: t } = D(this.getSqlFilterInputsFor(e));
1167
1178
  return t.join(" AND ");
1168
1179
  },
1169
1180
  getSqlFiltersFor(e, t = []) {
@@ -1174,7 +1185,7 @@ function He(e = {}) {
1174
1185
  let n = t.map((e) => ({
1175
1186
  source: "base",
1176
1187
  value: e
1177
- })), { filterStrings: r, parameters: i } = E(this.getSqlFilterInputsFor(e)), a = r.join(" AND ");
1188
+ })), { filterStrings: r, parameters: i } = D(this.getSqlFilterInputsFor(e)), a = r.join(" AND ");
1178
1189
  return a && n.push({
1179
1190
  source: "cross",
1180
1191
  value: a,
@@ -1182,13 +1193,13 @@ function He(e = {}) {
1182
1193
  }), n;
1183
1194
  },
1184
1195
  getSqlParametersFor(e) {
1185
- let { parameters: t } = E(this.getSqlFilterInputsFor(e));
1196
+ let { parameters: t } = D(this.getSqlFilterInputsFor(e));
1186
1197
  return t;
1187
1198
  }
1188
1199
  };
1189
1200
  }
1190
- function Ue(e = {}) {
1191
- let t = a(0), n = He(o(e)), r = () => {
1201
+ function He(e = {}) {
1202
+ let t = a(0), n = Ve(o(e)), r = () => {
1192
1203
  t.value += 1;
1193
1204
  };
1194
1205
  return {
@@ -1234,7 +1245,7 @@ function Ue(e = {}) {
1234
1245
  }
1235
1246
  //#endregion
1236
1247
  //#region dashboards/base.ts
1237
- var O = {
1248
+ var k = {
1238
1249
  CHART: "chart",
1239
1250
  MARKDOWN: "markdown",
1240
1251
  TABLE: "table",
@@ -1245,24 +1256,24 @@ var O = {
1245
1256
  APPENDIX_HEADER: "appendix-header",
1246
1257
  FREEFORM: "freeform"
1247
1258
  };
1248
- function We(e) {
1259
+ function Ue(e) {
1249
1260
  return !!e && typeof e == "object" && "markdown" in e;
1250
1261
  }
1251
- function Ge(e) {
1262
+ function We(e) {
1252
1263
  return !!e && typeof e == "object" && "headline" in e && "verdict" in e && "confidence" in e;
1253
1264
  }
1254
- function Ke(e) {
1265
+ function Ge(e) {
1255
1266
  return !!e && typeof e == "object" && "claim" in e && !("headline" in e);
1256
1267
  }
1257
- function qe(e) {
1268
+ function Ke(e) {
1258
1269
  return !!e && typeof e == "object" && typeof e.html == "string";
1259
1270
  }
1260
- function Je(e) {
1271
+ function qe(e) {
1261
1272
  let t = e.content;
1262
- if (typeof t == "string") return e.type === O.CHART || e.type === O.TABLE ? t : void 0;
1263
- if (qe(t) || Ge(t) || Ke(t) || We(t)) return t.query || void 0;
1273
+ if (typeof t == "string") return e.type === k.CHART || e.type === k.TABLE ? t : void 0;
1274
+ if (Ke(t) || We(t) || Ge(t) || Ue(t)) return t.query || void 0;
1264
1275
  }
1265
- var Ye = class t {
1276
+ var Je = class t {
1266
1277
  id;
1267
1278
  name;
1268
1279
  storage;
@@ -1305,7 +1316,7 @@ var Ye = class t {
1305
1316
  }
1306
1317
  addItem(e, t = 0, n = 0, r = 4, i = null, a = null, o) {
1307
1318
  let s = this.nextId.toString(), c = 10, l = r;
1308
- e === O.MARKDOWN ? c = 3 : e === O.SECTION_HEADER || e === O.APPENDIX_HEADER ? c = 1 : e === O.MEMO ? (c = 8, l = 20) : e === O.CLAIM ? (c = 4, l = 20) : e === O.FREEFORM && (c = 10);
1319
+ e === k.MARKDOWN ? c = 3 : e === k.SECTION_HEADER || e === k.APPENDIX_HEADER ? c = 1 : e === k.MEMO ? (c = 8, l = 20) : e === k.CLAIM ? (c = 4, l = 20) : e === k.FREEFORM && (c = 10);
1309
1320
  let u = n;
1310
1321
  u = this.layout.reduce((e, t) => Math.max(e, t.y + t.h), 0), this.layout.push({
1311
1322
  x: t,
@@ -1316,16 +1327,16 @@ var Ye = class t {
1316
1327
  static: !1
1317
1328
  });
1318
1329
  let d = `Note ${s}`;
1319
- e === O.CHART ? d = `Chart ${s}` : e === O.TABLE ? d = `Table ${s}` : e === O.SECTION_HEADER ? d = "### Section Header" : e === O.MEMO ? d = "Executive Memo" : e === O.CLAIM ? d = `Claim ${s}` : e === O.APPENDIX_HEADER ? d = "### Appendix" : e === O.FREEFORM && (d = `Widget ${s}`);
1330
+ e === k.CHART ? d = `Chart ${s}` : e === k.TABLE ? d = `Table ${s}` : e === k.SECTION_HEADER ? d = "### Section Header" : e === k.MEMO ? d = "Executive Memo" : e === k.CLAIM ? d = `Claim ${s}` : e === k.APPENDIX_HEADER ? d = "### Appendix" : e === k.FREEFORM && (d = `Widget ${s}`);
1320
1331
  let f = o || d, p = "# Markdown Cell\nEnter your markdown content here.";
1321
- e === O.CHART ? p = "SELECT unnest([1,2,3,4]) as value, 'example' as dim" : e === O.TABLE ? p = "SELECT [1,2,3,4] as value, 'example' as dim" : e === O.SECTION_HEADER || e === O.APPENDIX_HEADER ? p = "" : e === O.MEMO ? p = {
1332
+ e === k.CHART ? p = "SELECT unnest([1,2,3,4]) as value, 'example' as dim" : e === k.TABLE ? p = "SELECT [1,2,3,4] as value, 'example' as dim" : e === k.SECTION_HEADER || e === k.APPENDIX_HEADER ? p = "" : e === k.MEMO ? p = {
1322
1333
  headline: "",
1323
1334
  verdict: "inconclusive",
1324
1335
  magnitude: "",
1325
1336
  cause: "",
1326
1337
  action: "",
1327
1338
  confidence: "medium"
1328
- } : e === O.CLAIM ? p = { claim: "" } : e === O.FREEFORM && (p = {
1339
+ } : e === k.CLAIM ? p = { claim: "" } : e === k.FREEFORM && (p = {
1329
1340
  html: ee,
1330
1341
  query: "SELECT unnest(['alpha','beta','gamma']) as dim, random() * 100 as value"
1331
1342
  });
@@ -1336,7 +1347,7 @@ var Ye = class t {
1336
1347
  name: f,
1337
1348
  width: 0,
1338
1349
  height: 0,
1339
- allowCrossFilter: !(e === O.SECTION_HEADER || e === O.APPENDIX_HEADER || e === O.MEMO)
1350
+ allowCrossFilter: !(e === k.SECTION_HEADER || e === k.APPENDIX_HEADER || e === k.MEMO)
1340
1351
  }, this.nextId++, this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0, s;
1341
1352
  }
1342
1353
  setLayoutType(e) {
@@ -1427,22 +1438,22 @@ var Ye = class t {
1427
1438
  }
1428
1439
  updateItemFilters(e) {
1429
1440
  let t = this.gridItems[e];
1430
- t && D(t) && (this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0);
1441
+ t && O(t) && (this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0);
1431
1442
  }
1432
1443
  removeItemCrossFilter(e, t) {
1433
- ze(this.gridItems, e, t) && (this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0);
1444
+ Re(this.gridItems, e, t) && (this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0);
1434
1445
  }
1435
1446
  removeItemCrossFilterSource(e) {
1436
- let t = !!this.gridItems[e]?.chartFilters?.length, n = Be(this.gridItems, e);
1447
+ let t = !!this.gridItems[e]?.chartFilters?.length, n = ze(this.gridItems, e);
1437
1448
  return (n.length > 0 || t) && (this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0), n;
1438
1449
  }
1439
1450
  removeAllFilters() {
1440
- let e = Ve(this.gridItems);
1451
+ let e = Be(this.gridItems);
1441
1452
  for (let e in this.gridItems) this.gridItems[e].parameters = {};
1442
1453
  return e.length > 0 && (this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0), e;
1443
1454
  }
1444
1455
  updateItemCrossFilters(e, t, n, r) {
1445
- let i = Re(this.gridItems, e, t, n, r);
1456
+ let i = Le(this.gridItems, e, t, n, r);
1446
1457
  return this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0, i;
1447
1458
  }
1448
1459
  updateItemName(e, t) {
@@ -1547,7 +1558,7 @@ var Ye = class t {
1547
1558
  children: n.children || []
1548
1559
  });
1549
1560
  }
1550
- }, Xe = .5, Ze = 1e4, Qe = 2e5, $e = [
1561
+ }, Ye = .5, Xe = 1e4, Ze = 2e5, Qe = [
1551
1562
  {
1552
1563
  value: "demo",
1553
1564
  label: "Demo (limited messages)"
@@ -1572,13 +1583,13 @@ var Ye = class t {
1572
1583
  value: "deepseek",
1573
1584
  label: "DeepSeek"
1574
1585
  }
1575
- ], et = Object.fromEntries($e.map((e) => [e.value, e.label])), tt = {
1586
+ ], $e = Object.fromEntries(Qe.map((e) => [e.value, e.label])), et = {
1576
1587
  anthropic: "sk-ant-...",
1577
1588
  openai: "sk-...",
1578
1589
  google: "AIza...",
1579
1590
  openrouter: "sk-or-...",
1580
1591
  deepseek: "sk-..."
1581
- }, k = "# Trilogy Syntax\n\nTrilogy statements define a semantic model or a query; collections of statements\nare written as `.preql` files. A statement returns data (SELECT) or defines\nconcepts (key | property | auto | metric), datasources, or parameters.\n\nimport <model> as <alias>; makes a model's fields available. <model> is a DOT-SEPARATED MODULE PATH relative to the project root (where trilogy.toml lives), never a file path: the file `raw/store_sales.preql` is imported as `import raw.store_sales as ss;` — replace each `/` with `.`, drop the `.preql`, keep the folder segment. When an imported model imports others (a fact with imported dimensions), those are dot paths: `enroll.student.name`. Typical usage imports ONE fact and reaches dimensions through it; nested dimensions carry ALL values (order.customers includes customers without orders).\n\nkey | property | auto | metric; define new concepts. `auto x <- ...` is a DEFINITION, not a precomputed value: each reference expands and re-evaluates in the consuming query's scope.\n\nparameter NAME TYPE [default <literal>]; — a runtime value supplied via `--param NAME=VALUE` (repeatable); append `?` to the type to allow null.\n\n## Combining models\n\n| Goal | Use |\n|---|---|\n| Typical query | no merge, no join: access all fields through dot-paths |\n| Blend two models on shared keys inside one query | scoped `subset\\|union join` |\n| Make a connection universal to a whole file | `merge` |\n| Stack subsets/channels as rows | `union(...)` |\n| Rows in A but never in B (set difference) | `except(...)` |\n| Rows present in every source (set intersection) | `intersect(...)` |\n\n### Query-scoped join\n\nA typical fact already has its dimensions merged in, so no join is needed. Blending fact models or rowset outputs takes a scoped join, placed right after the select list, which DECLARES how the key domains relate: `subset join a = b` (a's values are contained in b's, a ⊆ b; b authoritative for the key) or `union join a = b` (neither contains the other; the key is the coalesce of both sides and unmatched rows from BOTH sides are kept).\n\nTwo rules decide whether the numbers are right, so apply them before reaching for the example:\n\n- Joins NEVER drop a row and NULL keys match null-safely; there is NO inner join. An intersection is an explicit `where <optional-side attr> is not null`.\n- JOIN ON THE FULL GRAIN: `explore` prints each fact's grain as @<k1, k2>; write one clause PER key. Matching only one key of a multi-key grain causes duplication.\n\nChaining, expression keys, union/subset mixing, multi-key and self-pair shapes: trilogy agent-info syntax example scoped-join.\n\nmerge <a> into ~<b>; is the persistent (whole-file) equivalent of `subset join a = b`; plain `merge a into b;` declares EXACT domain equivalence. Standalone statements; prefer a scoped join unless the connection is universal.\n\n### union / except / intersect (row set operations)\n\n`union(...)` row-stacks self-contained select arms positionally (SQL UNION ALL) into one named rowset (`with combined as union((armA), (armB), ...) -> (out1, out2, ...)`); `except(...)` / `intersect(...)` share that arm shape but are SQL SET operators (DISTINCT rows, null-safe whole-row compare, and except subtracts later arms from the FIRST, so arm order matters). Prefer except over a multi-column `not in` for \"in A but never in B\". Each arm dedups to its own output grain BEFORE stacking, so aggregate INSIDE each arm or the total silently undersums. Examples: trilogy agent-info syntax example union-stack-channels / except-intersect-setops.\n\n## Query structure\n\nTop-level statements first (each `;`-terminated), then a query with clauses in this EXACT order — all optional except select:\n\n```\n<top-level statements>; # import / key / auto / rowset / merge / def / parameter\nwith <name> as # optional: name the select as a reusable rowset (CTE)\nwhere <row condition> # 1. filters INPUT rows, BEFORE aggregation\nselect <col>, <agg> as name, # 2. projection — grouping is AUTOMATIC by the\n # non-aggregated columns; never write GROUP BY\n subset|union join a = b (= c)? # 3. blend models; one clause per key, right after the select list\nby rollup|cube|grouping sets (...) # 4. optional multi-level grouping for the whole select\nhaving <result condition> # 5. filters aggregated/joined RESULTS\norder by <col> asc|desc # 6. sort\nlimit <n>; # 7. cap rows\n```\n\nA rowset (`with <name> as where ... select ...;`) is a standalone statement, evaluated in isolation — it does NOT respond to the consuming query's context. All its outputs are namespaced: output `abc.def` of rowset `foo` is referenced as `foo.abc.def`, and joined back like any concept (`subset join foo.key = other.key`). Alias every reused expression with `as`.\n\nFull annotated example: trilogy agent-info syntax example query-structure.\n\n### Not SQL — what to never write\n\n- **No FROM, GROUP BY, DISTINCT, SELECT \\*, or SQL-style set operators between selects.** Stack rows with `union(...)`; set difference/intersection with `except(...)`/`intersect(...)`; blend fact models with a scoped join.\n- **Grouping is automatic** by the non-aggregated fields in the SELECT — never write GROUP BY. Aggregates inherit the select's grain wherever they appear (where/select/having). Override per-aggregate with `agg(x) by <dims>`; `by *` aggregates across all data (single row). To weight a related dimension property once per fact row, project it to the fact grain first: `auto row_birth_year <- group(enroll.student.birth_year) by enroll.student_id, enroll.course_id;` then aggregate that.\n- **Output rows are deduplicated to the select grain.** To preserve legitimate duplicates (one row per matching fact), include the fact's grain keys in the select — hidden with the PREFIX `--` if they shouldn't appear in the output.\n- **Never write `distinct`.** `count(<key>)` is already distinct because keys are unique; `count_distinct(<property>)` counts distinct values of a non-key property.\n- **Count a COMPOSITE grain with `grain(...)`.** `count(grain(order_id, item.id))` counts order+item combinations; `grain(...)` is NEVER NULL. Never count one column of a multi-key grain — a coarser key counts its own distinct values and undercounts. `count_distinct(a, b)` is accepted as sugar for `count(grain(a, b))` (distinct combinations, NULL members included).\n- **An aggregate over a group with no qualifying rows is NULL, never 0.** `sum(x ? cond) by k` yields NULL for groups where nothing matches, so `= 0` matches nothing. Test emptiness with `count(key ? cond) by k = 0` or wrap: `coalesce(sum(x ? cond), 0)`.\n- **`count(1)` is invalid** — a constant does not identify rows. Use the row key, `count(grain(...))`, or `sum(1) by <grain>`.\n- **One-column expression subqueries only.** `(select ...)` is allowed where a scalar or membership set is expected (exactly one projected column) — never as a SQL-style FROM-subquery. For related-entity filters prefer the dot-path: `where enrollments.student.state = 'TN'`, not a subselect.\n- **`--` is a HIDDEN-field prefix, not a comment** (it still changes query structure). Comments use `#` only.\n- **Aliases**: always use the full dot-path (`enroll.student.id`); alias every new expression with `as`; a select alias is usable in `having`/`order by` but NOT inside other select expressions or `where`, and must not rename a field back to an existing concept name.\n\n## Filtering\n\n`where` filters input rows before aggregation; `having` filters results after aggregation/windows. The inline filter `x ? cond` filters the immediate prior expression: `sum(credits ? enrolled = true)`.\n\nAggregates/windows in WHERE do not filter each other's inputs. Use inline filters, or a staged `then where` chain: each `then where` stage's aggregates compute over only the rows passing earlier stages (`where x = 5 then where sum(y) > 10` ≡ `where x = 5 and sum(y ? x = 5) > 10`). One rule: the SAME cross-row expression cannot gate two different stages — give one an inline filter.\n\nWindows need all rows in their range: filter AFTER the window (in `having`) rather than in `where` when the window must see the full set.\n\n## Membership (semijoins)\n\n`in` / `not in` cross models without any declared relationship — the semijoin IS a scoped intersection. The right side is a concept or expression (never a subselect); build computed sets with an inline filter: `auto big_zip <- student.zip ? (count(student.id ? student.honors = true) by student.zip) > 10; ... where school.zip in big_zip`.\n\nMembership is IDENTITY matching, not SQL three-valued logic: NULL matches NULL; the result is always TRUE or FALSE (safe as a projected flag); `not in` is the exact complement (a NULL in the set never empties the result). For strict SQL behavior, add `x is not null`.\n\nA fact model contains ALL dimensional members, so `where students.id not in enroll.student_id` is typically a tautology — use `where enroll.id is null` instead. Tuple membership `(a, b) in (m.a, m.b)` requires both right-side concepts to resolve to ONE source (model or rowset); to test against a cross-model pair, stage it through a rowset first. Examples: existence-anti-join, staged-membership.\n\n## Aggregation\n\nAn aggregate without an explicit `by` is RESPONSIVE — its grain is set where it is consumed, not where it is declared (`auto avg_credits <- avg(enroll.credits);` becomes per-department when selected with `enroll.department`). Pin a grain inline: `sum(metric) by dim1, dim2 as name`; `by` accepts bare identifiers or parenthesized expressions: `avg(price) by (substring(phone, 1, 2))`.\n\nMulti-level grouping (`by rollup|cube|grouping sets (...)`) is a clause of the WHOLE select, placed before `having`: it computes every no-explicit-grain aggregate at every level in one pass (`by rollup (d1, d2)` → leaf, per-d1 subtotal, grand total). `grouping(<field>)` = 1 on rolled-up rows — use it to label levels rather than testing for NULL. Full detail: trilogy agent-info syntax example rollup.\n\n## Window functions\n\nDefault to windows for self-referential queries (period-over-period, running totals, rank, share of group). SQL-style syntax: `rank(<key>) over (partition by <g> order by <expr> desc)` (`dense_rank`/`row_number` same shape; multi-key `rank(a, b)`); `sum(x) over (partition by g order by t)` for running totals; `lag(<field>, <offset>) over (...)` / `lead(...)` for offsets (default 1). `partition by` is optional and accepts expressions.\n\n## Data types\n\nScalars: `string`, `bytes`, `bool`, `int`, `bigint`, `float`, `double`, `number`, `numeric(p,s)` (alias `decimal`; use for exact money), `date`, `datetime`, `timestamp`, `geography`, `any`. Composites: `array<T>`, `map<K, V>`, `struct<name: T, ...>`, `enum<T>[v1, v2, ...]`. Cast with `::type` or `cast(x as type)`: `\"2020-01-01\"::date`.\n\n`?` after a type marks it nullable (`property user.middle_name string?;`). `~` prefixing a concept marks the reference as a PARTIAL subset of the concept's domain — on a select item (`~customer.id`) or in `merge a into ~b` (a ⊆ b); it tells the planner this side is not authoritative.\n\n## Expression gotchas\n\n- `x ? cond` binds to a primary — wrap arithmetic: `(a - b) ? cond`, NOT `a - b ? cond`.\n- `a || b` is NULL if either side is NULL; `concat(...)` skips NULL arguments; `concat_ws(sep, ...)` joins with sep, skipping NULLs.\n- Date parts are unquoted: `date_part(d, year)`, never `'year'`; prefer `year(d)`. `date_diff(start, end, unit)` = end − start (a shipping lag is `date_diff(sold_date, ship_date, day)`).\n- Zero-argument functions still take parentheses: `current_date()`.\n- Factor repeated calculations into a `def` macro (invoked `@name(...)`) or reusable `auto` concepts.\n\n\nFunctions (the `aggregate` family doubles as the Aggregate Functions list):\naggregate: count|count_distinct|sum|max|min|avg|stddev|variance|array_agg|bool_or|bool_and|any|grouping(<arg1>) ; grouping_id(<arg1>, <arg2>, ..., <argN>)\nstring: concat|concat_strict|concat_ws(<arg1>, <arg2>, ..., <argN>) ; split|strpos|contains|trim|regexp_contains|format_time|parse_time(<arg1>, <arg2>) ; len|lower|upper|ltrim|rtrim|hex(<arg1>) ; substring|replace|regexp_extract|regexp_replace(<arg1>, <arg2>, <arg3>)\ndate/time: date|datetime|timestamp|second|minute|hour|day|day_of_week|week|month|quarter|year|month_name|day_name|unix_to_timestamp(<arg1>) ; date_spine(<arg1>, <arg2>) ; current_date|current_datetime()\n date_part(<arg1>, <arg2>) e.g. date_part('2020-01-01'::date, year)\n date_truncate(<arg1>, <arg2>) e.g. date_trunc('2020-01-01'::date, month)\n date_add(<arg1>, <arg2>, <arg3>) e.g. date_add('2020-01-01'::date, month, 1)\n date_sub(<arg1>, <arg2>, <arg3>) e.g. date_sub('2020-01-01'::date, day, 1)\n date_diff(<arg1>, <arg2>, <arg3>) e.g. date_diff('2020-01-01'::date, '2020-01-02'::date, day)\n current_timestamp() e.g. now()\narray/map/struct: unnest|array_distinct|array_sum|map_keys|map_values(<arg1>) ; struct|array(<arg1>, <arg2>, ..., <argN>) ; array_sort|array_to_string(<arg1>, <arg2>) ; array_transform|array_filter|generate_array(<arg1>, <arg2>, <arg3>)\nmath: mod|round|log|power|hash(<arg1>, <arg2>) ; abs|sqrt|random|floor|ceil(<arg1>)\nconditional/cast: case|simple_case|coalesce|greatest|least(<arg1>, <arg2>, ..., <argN>) ; cast|nullif(<arg1>, <arg2>) ; isnull(<arg1>)\ngeo: geo_from_text|geo_x|geo_y|geo_centroid(<arg1>) ; geo_point|geo_distance(<arg1>, <arg2>) ; geo_transform(<arg1>, <arg2>, <arg3>)\nother: is_not_distinct(<arg1>, <arg2>)\n\nSome types carry trait metadata that adds meaning: 'latitude', 'longitude' and 'currency' are all 'float' with extra semantics.\n\nAdditional syntax examples:\nThese less-common patterns have complete, copy-pasteable examples. Do NOT guess\nthe syntax — print the full example on demand with\n`trilogy agent-info syntax example <name>`:\n- `python-datasource` - run a local Python script as a datasource: wrap a function in `trilogy.io.run`, which writes the Arrow IPC stream to stdout for you from a table, dataframe, or list of dicts; declare concepts, map script columns in `datasource (...)`, use `grain (...) file `path.py`;`, then reference locally declared concepts WITHOUT the datasource name as a prefix\n- `query-structure` - the exact clause order of a query, and how to define a rowset\n- `chart` - drawing a result rather than tabulating it: `chart layer <type> ( <role> <- <expr> as <name>, ... )` is a STATEMENT that wraps its own query - the bindings ARE the select; layers, roles, scales, reference lines and `from <select>`\n- `filtered-aggregate` - aggregating only the rows matching a condition, and counting rows correctly\n- `row-level-where-vs-having` - choosing between WHERE and HAVING for a threshold condition\n- `nested-aggregate-group-average` - the \"above 1.2x the group norm\" ask: inner `sum(x) by entity, group`, outer `avg(inner) by group`; use WHERE for the comparison when a separate output-only filter must not narrow the benchmark\n- `pivot-columns` - turn distinct values of a dimension into columns with a reusable `def` macro + filtered aggregate\n- `case-when` - `case when <cond> then <val> ... else <val> end` to relabel codes or bucket a measure into a derived value, then group by it\n- `window-period-over-period` - `lead`/`lag` over (order by ...) to compare a row to an offset period; build a scoped set with `<col> ? <cond>` and filter with `in`\n- `union-stack-channels` - stacking several sources/channels together as rows\n- `except-intersect-setops` - set difference or co-occurrence across sources\n- `scoped-join` - blending a second fact model or rowset into one query\n- `existence-anti-join` - keeping or excluding rows by whether a key matches another model\n- `set-intersect-difference` - count tuples present in / absent from other groups via per-group presence flags - the multi-column INTERSECT/EXCEPT idiom\n- `dedup-before-aggregate` - a `rowset <- select <cols>` collapses identical tuples to one row (SELECT-grain group by) before a downstream sum - the `distinct`/UNION substitute\n- `rollup` - subtotals and a grand total in one pass, and labeling the levels\n- `rank-over-rollup` - rank rollup subtotals/leaves with a SINGLE `rank(a,b) over (partition by level, parent ...)` - not separate ranks per level\n- `staged-membership` - filtering a fine-grain query by a set computed at a coarser grain\n- `correlated-exists-via-grouped-counts` - translate `EXISTS other` / `NOT EXISTS other matching` over the same model into two `count(...) by <grain>` compared in `where` (`> 1` = another exists, `= 1` = no other matches) - pin the correlation grain with `by`", nt = "# Trilogy Syntax\n\nTrilogy statements define a semantic model or a query; collections of statements\nare written as `.preql` files. A statement returns data (SELECT) or defines\nconcepts (key | property | auto | metric), datasources, or parameters.\n\nimport <model> as <alias>; makes a model's fields available. <model> is a DOT-SEPARATED MODULE PATH relative to the project root (where trilogy.toml lives), never a file path: the file `raw/store_sales.preql` is imported as `import raw.store_sales as ss;` — replace each `/` with `.`, drop the `.preql`, keep the folder segment. When an imported model imports others (a fact with imported dimensions), those are dot paths: `enroll.student.name`. Typical usage imports ONE fact and reaches dimensions through it; nested dimensions carry ALL values (order.customers includes customers without orders).\n\nkey | property | auto | metric; define new concepts. `auto x <- ...` is a DEFINITION, not a precomputed value: each reference expands and re-evaluates in the consuming query's scope.\n\nparameter NAME TYPE [default <literal>]; — a runtime value supplied via `--param NAME=VALUE` (repeatable); append `?` to the type to allow null.\n\n## Combining models\n\n| Goal | Use |\n|---|---|\n| Typical query | no merge, no join: access all fields through dot-paths |\n| Blend two models on shared keys inside one query | scoped `subset\\|union join` |\n| Make a connection universal to a whole file | `merge` |\n| Stack subsets/channels as rows | `union(...)` |\n| Rows in A but never in B (set difference) | `except(...)` |\n| Rows present in every source (set intersection) | `intersect(...)` |\n\n### Query-scoped join\n\nA typical fact already has its dimensions merged in, so no join is needed. Blending fact models or rowset outputs takes a scoped join, placed right after the select list, which DECLARES how the key domains relate: `subset join a = b` (a's values are contained in b's, a ⊆ b; b authoritative for the key) or `union join a = b` (neither contains the other; the key is the coalesce of both sides and unmatched rows from BOTH sides are kept).\n\nTwo rules decide whether the numbers are right, so apply them before reaching for the example:\n\n- Joins NEVER drop a row and NULL keys match null-safely; there is NO inner join. An intersection is an explicit `where <optional-side attr> is not null`.\n- JOIN ON THE FULL GRAIN: `explore` prints each fact's grain as @<k1, k2>; write one clause PER key. Matching only one key of a multi-key grain causes duplication.\n\nChaining, expression keys, union/subset mixing, multi-key and self-pair shapes: trilogy agent-info syntax example scoped-join.\n\nmerge <a> into ~<b>; is the persistent (whole-file) equivalent of `subset join a = b`; plain `merge a into b;` declares EXACT domain equivalence. Standalone statements; prefer a scoped join unless the connection is universal.\n\n### union / except / intersect (row set operations)\n\n`union(...)` row-stacks self-contained select arms positionally (SQL UNION ALL) into one named rowset (`with combined as union((armA), (armB), ...) -> (out1, out2, ...)`); `except(...)` / `intersect(...)` share that arm shape but are SQL SET operators (DISTINCT rows, null-safe whole-row compare, and except subtracts later arms from the FIRST, so arm order matters). Prefer except over a multi-column `not in` for \"in A but never in B\". Each arm dedups to its own output grain BEFORE stacking, so aggregate INSIDE each arm or the total silently undersums. Examples: trilogy agent-info syntax example union-stack-channels / except-intersect-setops.\n\n## Query structure\n\nTop-level statements first (each `;`-terminated), then a query with clauses in this EXACT order — all optional except select:\n\n```\n<top-level statements>; # import / key / auto / rowset / merge / def / parameter\nwith <name> as # optional: name the select as a reusable rowset (CTE)\nwhere <row condition> # 1. filters INPUT rows, BEFORE aggregation\nselect <col>, <agg> as name, # 2. projection — grouping is AUTOMATIC by the\n # non-aggregated columns; never write GROUP BY\n subset|union join a = b (= c)? # 3. blend models; one clause per key, right after the select list\nby rollup|cube|grouping sets (...) # 4. optional multi-level grouping for the whole select\nhaving <result condition> # 5. filters aggregated/joined RESULTS\norder by <col> asc|desc # 6. sort\nlimit <n>; # 7. cap rows\n```\n\nA rowset (`with <name> as where ... select ...;`) is a standalone statement, evaluated in isolation — it does NOT respond to the consuming query's context. All its outputs are namespaced: output `abc.def` of rowset `foo` is referenced as `foo.abc.def`, and joined back like any concept (`subset join foo.key = other.key`). Alias every reused expression with `as`.\n\nFull annotated example: trilogy agent-info syntax example query-structure.\n\n### Not SQL — what to never write\n\n- **No FROM, GROUP BY, DISTINCT, SELECT \\*, or SQL-style set operators between selects.** Stack rows with `union(...)`; set difference/intersection with `except(...)`/`intersect(...)`; blend fact models with a scoped join.\n- **Grouping is automatic** by the non-aggregated fields in the SELECT — never write GROUP BY. Aggregates inherit the select's grain wherever they appear (where/select/having). Override per-aggregate with `agg(x) by <dims>`; `by *` aggregates across all data (single row). To weight a related dimension property once per fact row, project it to the fact grain first: `auto row_birth_year <- group(enroll.student.birth_year) by enroll.student_id, enroll.course_id;` then aggregate that.\n- **Output rows are deduplicated to the select grain.** To preserve legitimate duplicates (one row per matching fact), include the fact's grain keys in the select — hidden with the PREFIX `--` if they shouldn't appear in the output.\n- **Never write `distinct`.** `count(<key>)` is already distinct because keys are unique; `count_distinct(<property>)` counts distinct values of a non-key property.\n- **Count a COMPOSITE grain with `grain(...)`.** `count(grain(order_id, item.id))` counts order+item combinations; `grain(...)` is NEVER NULL. Never count one column of a multi-key grain — a coarser key counts its own distinct values and undercounts. `count_distinct(a, b)` is accepted as sugar for `count(grain(a, b))` (distinct combinations, NULL members included).\n- **An aggregate over a group with no qualifying rows is NULL, never 0.** `sum(x ? cond) by k` yields NULL for groups where nothing matches, so `= 0` matches nothing. Test emptiness with `count(key ? cond) by k = 0` or wrap: `coalesce(sum(x ? cond), 0)`.\n- **`count(1)` is invalid** — a constant does not identify rows. Use the row key, `count(grain(...))`, or `sum(1) by <grain>`.\n- **One-column expression subqueries only.** `(select ...)` is allowed where a scalar or membership set is expected (exactly one projected column) — never as a SQL-style FROM-subquery. For related-entity filters prefer the dot-path: `where enrollments.student.state = 'TN'`, not a subselect.\n- **`--` is a HIDDEN-field prefix, not a comment** (it still changes query structure). Comments use `#` only.\n- **Aliases**: always use the full dot-path (`enroll.student.id`); alias every new expression with `as`; a select alias is usable in `having`/`order by` but NOT inside other select expressions or `where`, and must not rename a field back to an existing concept name.\n\n## Filtering\n\n`where` filters input rows before aggregation; `having` filters results after aggregation/windows. The inline filter `x ? cond` filters the immediate prior expression: `sum(credits ? enrolled = true)`.\n\nAggregates/windows in WHERE do not filter each other's inputs. Use inline filters, or a staged `then where` chain: each `then where` stage's aggregates compute over only the rows passing earlier stages (`where x = 5 then where sum(y) > 10` ≡ `where x = 5 and sum(y ? x = 5) > 10`). One rule: the SAME cross-row expression cannot gate two different stages — give one an inline filter.\n\nWindows need all rows in their range: filter AFTER the window (in `having`) rather than in `where` when the window must see the full set.\n\n## Membership (semijoins)\n\n`in` / `not in` cross models without any declared relationship — the semijoin IS a scoped intersection. The right side is a concept or expression (never a subselect); build computed sets with an inline filter: `auto big_zip <- student.zip ? (count(student.id ? student.honors = true) by student.zip) > 10; ... where school.zip in big_zip`.\n\nMembership is IDENTITY matching, not SQL three-valued logic: NULL matches NULL; the result is always TRUE or FALSE (safe as a projected flag); `not in` is the exact complement (a NULL in the set never empties the result). For strict SQL behavior, add `x is not null`.\n\nA fact model contains ALL dimensional members, so `where students.id not in enroll.student_id` is typically a tautology — use `where enroll.id is null` instead. Tuple membership `(a, b) in (m.a, m.b)` requires both right-side concepts to resolve to ONE source (model or rowset); to test against a cross-model pair, stage it through a rowset first. Examples: existence-anti-join, staged-membership.\n\n## Aggregation\n\nAn aggregate without an explicit `by` is RESPONSIVE — its grain is set where it is consumed, not where it is declared (`auto avg_credits <- avg(enroll.credits);` becomes per-department when selected with `enroll.department`). Pin a grain inline: `sum(metric) by dim1, dim2 as name`; `by` accepts bare identifiers or parenthesized expressions: `avg(price) by (substring(phone, 1, 2))`.\n\nMulti-level grouping (`by rollup|cube|grouping sets (...)`) is a clause of the WHOLE select, placed before `having`: it computes every no-explicit-grain aggregate at every level in one pass (`by rollup (d1, d2)` → leaf, per-d1 subtotal, grand total). `grouping(<field>)` = 1 on rolled-up rows — use it to label levels rather than testing for NULL. Full detail: trilogy agent-info syntax example rollup.\n\n## Window functions\n\nDefault to windows for self-referential queries (period-over-period, running totals, rank, share of group). SQL-style syntax: `rank(<key>) over (partition by <g> order by <expr> desc)` (`dense_rank`/`row_number` same shape; multi-key `rank(a, b)`); `sum(x) over (partition by g order by t)` for running totals; `lag(<field>, <offset>) over (...)` / `lead(...)` for offsets (default 1). `partition by` is optional and accepts expressions.\n\n## Data types\n\nScalars: `string`, `bytes`, `bool`, `int`, `bigint`, `float`, `double`, `number`, `numeric(p,s)` (alias `decimal`; use for exact money), `date`, `datetime`, `timestamp`, `geography`, `any`. Composites: `array<T>`, `map<K, V>`, `struct<name: T, ...>`, `enum<T>[v1, v2, ...]`. Cast with `::type` or `cast(x as type)`: `\"2020-01-01\"::date`.\n\n`?` after a type marks it nullable (`property user.middle_name string?;`). `~` prefixing a concept marks the reference as a PARTIAL subset of the concept's domain — on a select item (`~customer.id`) or in `merge a into ~b` (a ⊆ b); it tells the planner this side is not authoritative.\n\n## Expression gotchas\n\n- `x ? cond` binds to a primary — wrap arithmetic: `(a - b) ? cond`, NOT `a - b ? cond`.\n- `a || b` is NULL if either side is NULL; `concat(...)` skips NULL arguments; `concat_ws(sep, ...)` joins with sep, skipping NULLs.\n- Date parts are unquoted: `date_part(d, year)`, never `'year'`; prefer `year(d)`. `date_diff(start, end, unit)` = end − start (a shipping lag is `date_diff(sold_date, ship_date, day)`).\n- Zero-argument functions still take parentheses: `current_date()`.\n- Factor repeated calculations into a `def` macro (invoked `@name(...)`) or reusable `auto` concepts.", rt = ["aggregate: count|count_distinct|sum|max|min|avg|stddev|variance|array_agg|bool_or|bool_and|any|grouping(<arg1>) ; grouping_id(<arg1>, <arg2>, ..., <argN>)"], it = [
1592
+ }, A = "# Trilogy Syntax\n\nTrilogy statements define a semantic model or a query; collections of statements\nare written as `.preql` files. A statement returns data (SELECT) or defines\nconcepts (key | property | auto | metric), datasources, or parameters.\n\nimport <model> as <alias>; makes a model's fields available. <model> is a DOT-SEPARATED MODULE PATH relative to the project root (where trilogy.toml lives), never a file path: the file `raw/store_sales.preql` is imported as `import raw.store_sales as ss;` — replace each `/` with `.`, drop the `.preql`, keep the folder segment. When an imported model imports others (a fact with imported dimensions), those are dot paths: `enroll.student.name`. Typical usage imports ONE fact and reaches dimensions through it; nested dimensions carry ALL values (order.customers includes customers without orders).\n\nkey | property | auto | metric; define new concepts. `auto x <- ...` is a DEFINITION, not a precomputed value: each reference expands and re-evaluates in the consuming query's scope.\n\nparameter NAME TYPE [default <literal>]; — a runtime value supplied via `--param NAME=VALUE` (repeatable); append `?` to the type to allow null.\n\n## Combining models\n\n| Goal | Use |\n|---|---|\n| Typical query | no merge, no join: access all fields through dot-paths |\n| Blend two models on shared keys inside one query | scoped `subset\\|union join` |\n| Make a connection universal to a whole file | `merge` |\n| Stack subsets/channels as rows | `union(...)` |\n| Rows in A but never in B (set difference) | `except(...)` |\n| Rows present in every source (set intersection) | `intersect(...)` |\n\n### Query-scoped join\n\nA typical fact already has its dimensions merged in, so no join is needed. Blending fact models or rowset outputs takes a scoped join, placed right after the select list, which DECLARES how the key domains relate: `subset join a = b` (a's values are contained in b's, a ⊆ b; b authoritative for the key) or `union join a = b` (neither contains the other; the key is the coalesce of both sides and unmatched rows from BOTH sides are kept).\n\nTwo rules decide whether the numbers are right, so apply them before reaching for the example:\n\n- Joins NEVER drop a row and NULL keys match null-safely; there is NO inner join. An intersection is an explicit `where <optional-side attr> is not null`.\n- JOIN ON THE FULL GRAIN: `explore` prints each fact's grain as @<k1, k2>; write one clause PER key. Matching only one key of a multi-key grain causes duplication.\n\nChaining, expression keys, union/subset mixing, multi-key and self-pair shapes: trilogy agent-info syntax example scoped-join.\n\nmerge <a> into ~<b>; is the persistent (whole-file) equivalent of `subset join a = b`; plain `merge a into b;` declares EXACT domain equivalence. Standalone statements; prefer a scoped join unless the connection is universal.\n\n### union / except / intersect (row set operations)\n\n`union(...)` row-stacks self-contained select arms positionally (SQL UNION ALL) into one named rowset (`with combined as union((armA), (armB), ...) -> (out1, out2, ...)`); `except(...)` / `intersect(...)` share that arm shape but are SQL SET operators (DISTINCT rows, null-safe whole-row compare, and except subtracts later arms from the FIRST, so arm order matters). Prefer except over a multi-column `not in` for \"in A but never in B\". Each arm dedups to its own output grain BEFORE stacking, so aggregate INSIDE each arm or the total silently undersums. Examples: trilogy agent-info syntax example union-stack-channels / except-intersect-setops.\n\n## Query structure\n\nTop-level statements first (each `;`-terminated), then a query with clauses in this EXACT order — all optional except select:\n\n```\n<top-level statements>; # import / key / auto / rowset / merge / def / parameter\nwith <name> as # optional: name the select as a reusable rowset (CTE)\nwhere <row condition> # 1. filters INPUT rows, BEFORE aggregation\nselect <col>, <agg> as name, # 2. projection — grouping is AUTOMATIC by the\n # non-aggregated columns; never write GROUP BY\n subset|union join a = b (= c)? # 3. blend models; one clause per key, right after the select list\nby rollup|cube|grouping sets (...) # 4. optional multi-level grouping for the whole select\nhaving <result condition> # 5. filters aggregated/joined RESULTS\norder by <col> asc|desc # 6. sort\nlimit <n>; # 7. cap rows\n```\n\nA rowset (`with <name> as where ... select ...;`) is a standalone statement, evaluated in isolation — it does NOT respond to the consuming query's context. All its outputs are namespaced: output `abc.def` of rowset `foo` is referenced as `foo.abc.def`, and joined back like any concept (`subset join foo.key = other.key`). Alias every reused expression with `as`.\n\nFull annotated example: trilogy agent-info syntax example query-structure.\n\n### Not SQL — what to never write\n\n- **No FROM, GROUP BY, DISTINCT, SELECT \\*, or SQL-style set operators between selects.** Stack rows with `union(...)`; set difference/intersection with `except(...)`/`intersect(...)`; blend fact models with a scoped join.\n- **Grouping is automatic** by the non-aggregated fields in the SELECT — never write GROUP BY. Aggregates inherit the select's grain wherever they appear (where/select/having). Override per-aggregate with `agg(x) by <dims>`; `by *` aggregates across all data (single row). To weight a related dimension property once per fact row, project it to the fact grain first: `auto row_birth_year <- group(enroll.student.birth_year) by enroll.student_id, enroll.course_id;` then aggregate that.\n- **Output rows are deduplicated to the select grain.** To preserve legitimate duplicates (one row per matching fact), include the fact's grain keys in the select — hidden with the PREFIX `--` if they shouldn't appear in the output.\n- **Never write `distinct`.** `count(<key>)` is already distinct because keys are unique; `count_distinct(<property>)` counts distinct values of a non-key property.\n- **Count a COMPOSITE grain with `grain(...)`.** `count(grain(order_id, item.id))` counts order+item combinations; `grain(...)` is NEVER NULL. Never count one column of a multi-key grain — a coarser key counts its own distinct values and undercounts. `count_distinct(a, b)` is accepted as sugar for `count(grain(a, b))` (distinct combinations, NULL members included).\n- **An aggregate over a group with no qualifying rows is NULL, never 0.** `sum(x ? cond) by k` yields NULL for groups where nothing matches, so `= 0` matches nothing. Test emptiness with `count(key ? cond) by k = 0` or wrap: `coalesce(sum(x ? cond), 0)`.\n- **`count(1)` is invalid** — a constant does not identify rows. Use the row key, `count(grain(...))`, or `sum(1) by <grain>`.\n- **One-column expression subqueries only.** `(select ...)` is allowed where a scalar or membership set is expected (exactly one projected column) — never as a SQL-style FROM-subquery. For related-entity filters prefer the dot-path: `where enrollments.student.state = 'TN'`, not a subselect.\n- **`--` is a HIDDEN-field prefix, not a comment** (it still changes query structure). Comments use `#` only.\n- **Aliases**: always use the full dot-path (`enroll.student.id`); alias every new expression with `as`; a select alias is usable in `having`/`order by` but NOT inside other select expressions or `where`, and must not rename a field back to an existing concept name.\n\n## Filtering\n\n`where` filters input rows before aggregation; `having` filters results after aggregation/windows. The inline filter `x ? cond` filters the immediate prior expression: `sum(credits ? enrolled = true)`.\n\nAggregates/windows in WHERE do not filter each other's inputs. Use inline filters, or a staged `then where` chain: each `then where` stage's aggregates compute over only the rows passing earlier stages (`where x = 5 then where sum(y) > 10` ≡ `where x = 5 and sum(y ? x = 5) > 10`). One rule: the SAME cross-row expression cannot gate two different stages — give one an inline filter.\n\nWindows need all rows in their range: filter AFTER the window (in `having`) rather than in `where` when the window must see the full set.\n\n## Membership (semijoins)\n\n`in` / `not in` cross models without any declared relationship — the semijoin IS a scoped intersection. The right side is a concept or expression (never a subselect); build computed sets with an inline filter: `auto big_zip <- student.zip ? (count(student.id ? student.honors = true) by student.zip) > 10; ... where school.zip in big_zip`.\n\nMembership is IDENTITY matching, not SQL three-valued logic: NULL matches NULL; the result is always TRUE or FALSE (safe as a projected flag); `not in` is the exact complement (a NULL in the set never empties the result). For strict SQL behavior, add `x is not null`.\n\nA fact model contains ALL dimensional members, so `where students.id not in enroll.student_id` is typically a tautology — use `where enroll.id is null` instead. Tuple membership `(a, b) in (m.a, m.b)` requires both right-side concepts to resolve to ONE source (model or rowset); to test against a cross-model pair, stage it through a rowset first. Examples: existence-anti-join, staged-membership.\n\n## Aggregation\n\nAn aggregate without an explicit `by` is RESPONSIVE — its grain is set where it is consumed, not where it is declared (`auto avg_credits <- avg(enroll.credits);` becomes per-department when selected with `enroll.department`). Pin a grain inline: `sum(metric) by dim1, dim2 as name`; `by` accepts bare identifiers or parenthesized expressions: `avg(price) by (substring(phone, 1, 2))`.\n\nMulti-level grouping (`by rollup|cube|grouping sets (...)`) is a clause of the WHOLE select, placed before `having`: it computes every no-explicit-grain aggregate at every level in one pass (`by rollup (d1, d2)` → leaf, per-d1 subtotal, grand total). `grouping(<field>)` = 1 on rolled-up rows — use it to label levels rather than testing for NULL. Full detail: trilogy agent-info syntax example rollup.\n\n## Window functions\n\nDefault to windows for self-referential queries (period-over-period, running totals, rank, share of group). SQL-style syntax: `rank(<key>) over (partition by <g> order by <expr> desc)` (`dense_rank`/`row_number` same shape; multi-key `rank(a, b)`); `sum(x) over (partition by g order by t)` for running totals; `lag(<field>, <offset>) over (...)` / `lead(...)` for offsets (default 1). `partition by` is optional and accepts expressions.\n\n## Data types\n\nScalars: `string`, `bytes`, `bool`, `int`, `bigint`, `float`, `double`, `number`, `numeric(p,s)` (alias `decimal`; use for exact money), `date`, `datetime`, `timestamp`, `geography`, `any`. Composites: `array<T>`, `map<K, V>`, `struct<name: T, ...>`, `enum<T>[v1, v2, ...]`. Cast with `::type` or `cast(x as type)`: `\"2020-01-01\"::date`.\n\n`?` after a type marks it nullable (`property user.middle_name string?;`). `~` prefixing a concept marks the reference as a PARTIAL subset of the concept's domain — on a select item (`~customer.id`) or in `merge a into ~b` (a ⊆ b); it tells the planner this side is not authoritative.\n\n## Expression gotchas\n\n- `x ? cond` binds to a primary — wrap arithmetic: `(a - b) ? cond`, NOT `a - b ? cond`.\n- `a || b` is NULL if either side is NULL; `concat(...)` skips NULL arguments; `concat_ws(sep, ...)` joins with sep, skipping NULLs.\n- Date parts are unquoted: `date_part(d, year)`, never `'year'`; prefer `year(d)`. `date_diff(start, end, unit)` = end − start (a shipping lag is `date_diff(sold_date, ship_date, day)`).\n- Zero-argument functions still take parentheses: `current_date()`.\n- Factor repeated calculations into a `def` macro (invoked `@name(...)`) or reusable `auto` concepts.\n\n\nFunctions (the `aggregate` family doubles as the Aggregate Functions list):\naggregate: count|count_distinct|sum|max|min|avg|stddev|variance|array_agg|bool_or|bool_and|any|grouping(<arg1>) ; grouping_id(<arg1>, <arg2>, ..., <argN>)\nstring: concat|concat_strict|concat_ws(<arg1>, <arg2>, ..., <argN>) ; split|strpos|contains|trim|regexp_contains|format_time|parse_time(<arg1>, <arg2>) ; len|lower|upper|ltrim|rtrim|hex(<arg1>) ; substring|replace|regexp_extract|regexp_replace(<arg1>, <arg2>, <arg3>)\ndate/time: date|datetime|timestamp|second|minute|hour|day|day_of_week|week|month|quarter|year|month_name|day_name|unix_to_timestamp(<arg1>) ; date_spine(<arg1>, <arg2>) ; current_date|current_datetime()\n date_part(<arg1>, <arg2>) e.g. date_part('2020-01-01'::date, year)\n date_truncate(<arg1>, <arg2>) e.g. date_trunc('2020-01-01'::date, month)\n date_add(<arg1>, <arg2>, <arg3>) e.g. date_add('2020-01-01'::date, month, 1)\n date_sub(<arg1>, <arg2>, <arg3>) e.g. date_sub('2020-01-01'::date, day, 1)\n date_diff(<arg1>, <arg2>, <arg3>) e.g. date_diff('2020-01-01'::date, '2020-01-02'::date, day)\n current_timestamp() e.g. now()\narray/map/struct: unnest|array_distinct|array_sum|map_keys|map_values(<arg1>) ; struct|array(<arg1>, <arg2>, ..., <argN>) ; array_sort|array_to_string(<arg1>, <arg2>) ; array_transform|array_filter|generate_array(<arg1>, <arg2>, <arg3>)\nmath: mod|round|log|power|hash(<arg1>, <arg2>) ; abs|sqrt|random|floor|ceil(<arg1>)\nconditional/cast: case|simple_case|coalesce|greatest|least(<arg1>, <arg2>, ..., <argN>) ; cast|nullif(<arg1>, <arg2>) ; isnull(<arg1>)\ngeo: geo_from_text|geo_x|geo_y|geo_centroid(<arg1>) ; geo_point|geo_distance(<arg1>, <arg2>) ; geo_transform(<arg1>, <arg2>, <arg3>)\nother: is_not_distinct(<arg1>, <arg2>)\n\nSome types carry trait metadata that adds meaning: 'latitude', 'longitude' and 'currency' are all 'float' with extra semantics.\n\nAdditional syntax examples:\nThese less-common patterns have complete, copy-pasteable examples. Do NOT guess\nthe syntax — print the full example on demand with\n`trilogy agent-info syntax example <name>`:\n- `python-datasource` - run a local Python script as a datasource: wrap a function in `trilogy.io.run`, which writes the Arrow IPC stream to stdout for you from a table, dataframe, or list of dicts; declare concepts, map script columns in `datasource (...)`, use `grain (...) file `path.py`;`, then reference locally declared concepts WITHOUT the datasource name as a prefix\n- `query-structure` - the exact clause order of a query, and how to define a rowset\n- `chart` - drawing a result rather than tabulating it: `chart layer <type> ( <role> <- <expr> as <name>, ... )` is a STATEMENT that wraps its own query - the bindings ARE the select; layers, roles, scales, reference lines and `from <select>`\n- `filtered-aggregate` - aggregating only the rows matching a condition, and counting rows correctly\n- `row-level-where-vs-having` - choosing between WHERE and HAVING for a threshold condition\n- `nested-aggregate-group-average` - the \"above 1.2x the group norm\" ask: inner `sum(x) by entity, group`, outer `avg(inner) by group`; use WHERE for the comparison when a separate output-only filter must not narrow the benchmark\n- `pivot-columns` - turn distinct values of a dimension into columns with a reusable `def` macro + filtered aggregate\n- `case-when` - `case when <cond> then <val> ... else <val> end` to relabel codes or bucket a measure into a derived value, then group by it\n- `window-period-over-period` - `lead`/`lag` over (order by ...) to compare a row to an offset period; build a scoped set with `<col> ? <cond>` and filter with `in`\n- `union-stack-channels` - stacking several sources/channels together as rows\n- `except-intersect-setops` - set difference or co-occurrence across sources\n- `scoped-join` - blending a second fact model or rowset into one query\n- `existence-anti-join` - keeping or excluding rows by whether a key matches another model\n- `set-intersect-difference` - count tuples present in / absent from other groups via per-group presence flags - the multi-column INTERSECT/EXCEPT idiom\n- `dedup-before-aggregate` - a `rowset <- select <cols>` collapses identical tuples to one row (SELECT-grain group by) before a downstream sum - the `distinct`/UNION substitute\n- `rollup` - subtotals and a grand total in one pass, and labeling the levels\n- `rank-over-rollup` - rank rollup subtotals/leaves with a SINGLE `rank(a,b) over (partition by level, parent ...)` - not separate ranks per level\n- `staged-membership` - filtering a fine-grain query by a set computed at a coarser grain\n- `correlated-exists-via-grouped-counts` - translate `EXISTS other` / `NOT EXISTS other matching` over the same model into two `count(...) by <grain>` compared in `where` (`> 1` = another exists, `= 1` = no other matches) - pin the correlation grain with `by`", tt = "# Trilogy Syntax\n\nTrilogy statements define a semantic model or a query; collections of statements\nare written as `.preql` files. A statement returns data (SELECT) or defines\nconcepts (key | property | auto | metric), datasources, or parameters.\n\nimport <model> as <alias>; makes a model's fields available. <model> is a DOT-SEPARATED MODULE PATH relative to the project root (where trilogy.toml lives), never a file path: the file `raw/store_sales.preql` is imported as `import raw.store_sales as ss;` — replace each `/` with `.`, drop the `.preql`, keep the folder segment. When an imported model imports others (a fact with imported dimensions), those are dot paths: `enroll.student.name`. Typical usage imports ONE fact and reaches dimensions through it; nested dimensions carry ALL values (order.customers includes customers without orders).\n\nkey | property | auto | metric; define new concepts. `auto x <- ...` is a DEFINITION, not a precomputed value: each reference expands and re-evaluates in the consuming query's scope.\n\nparameter NAME TYPE [default <literal>]; — a runtime value supplied via `--param NAME=VALUE` (repeatable); append `?` to the type to allow null.\n\n## Combining models\n\n| Goal | Use |\n|---|---|\n| Typical query | no merge, no join: access all fields through dot-paths |\n| Blend two models on shared keys inside one query | scoped `subset\\|union join` |\n| Make a connection universal to a whole file | `merge` |\n| Stack subsets/channels as rows | `union(...)` |\n| Rows in A but never in B (set difference) | `except(...)` |\n| Rows present in every source (set intersection) | `intersect(...)` |\n\n### Query-scoped join\n\nA typical fact already has its dimensions merged in, so no join is needed. Blending fact models or rowset outputs takes a scoped join, placed right after the select list, which DECLARES how the key domains relate: `subset join a = b` (a's values are contained in b's, a ⊆ b; b authoritative for the key) or `union join a = b` (neither contains the other; the key is the coalesce of both sides and unmatched rows from BOTH sides are kept).\n\nTwo rules decide whether the numbers are right, so apply them before reaching for the example:\n\n- Joins NEVER drop a row and NULL keys match null-safely; there is NO inner join. An intersection is an explicit `where <optional-side attr> is not null`.\n- JOIN ON THE FULL GRAIN: `explore` prints each fact's grain as @<k1, k2>; write one clause PER key. Matching only one key of a multi-key grain causes duplication.\n\nChaining, expression keys, union/subset mixing, multi-key and self-pair shapes: trilogy agent-info syntax example scoped-join.\n\nmerge <a> into ~<b>; is the persistent (whole-file) equivalent of `subset join a = b`; plain `merge a into b;` declares EXACT domain equivalence. Standalone statements; prefer a scoped join unless the connection is universal.\n\n### union / except / intersect (row set operations)\n\n`union(...)` row-stacks self-contained select arms positionally (SQL UNION ALL) into one named rowset (`with combined as union((armA), (armB), ...) -> (out1, out2, ...)`); `except(...)` / `intersect(...)` share that arm shape but are SQL SET operators (DISTINCT rows, null-safe whole-row compare, and except subtracts later arms from the FIRST, so arm order matters). Prefer except over a multi-column `not in` for \"in A but never in B\". Each arm dedups to its own output grain BEFORE stacking, so aggregate INSIDE each arm or the total silently undersums. Examples: trilogy agent-info syntax example union-stack-channels / except-intersect-setops.\n\n## Query structure\n\nTop-level statements first (each `;`-terminated), then a query with clauses in this EXACT order — all optional except select:\n\n```\n<top-level statements>; # import / key / auto / rowset / merge / def / parameter\nwith <name> as # optional: name the select as a reusable rowset (CTE)\nwhere <row condition> # 1. filters INPUT rows, BEFORE aggregation\nselect <col>, <agg> as name, # 2. projection — grouping is AUTOMATIC by the\n # non-aggregated columns; never write GROUP BY\n subset|union join a = b (= c)? # 3. blend models; one clause per key, right after the select list\nby rollup|cube|grouping sets (...) # 4. optional multi-level grouping for the whole select\nhaving <result condition> # 5. filters aggregated/joined RESULTS\norder by <col> asc|desc # 6. sort\nlimit <n>; # 7. cap rows\n```\n\nA rowset (`with <name> as where ... select ...;`) is a standalone statement, evaluated in isolation — it does NOT respond to the consuming query's context. All its outputs are namespaced: output `abc.def` of rowset `foo` is referenced as `foo.abc.def`, and joined back like any concept (`subset join foo.key = other.key`). Alias every reused expression with `as`.\n\nFull annotated example: trilogy agent-info syntax example query-structure.\n\n### Not SQL — what to never write\n\n- **No FROM, GROUP BY, DISTINCT, SELECT \\*, or SQL-style set operators between selects.** Stack rows with `union(...)`; set difference/intersection with `except(...)`/`intersect(...)`; blend fact models with a scoped join.\n- **Grouping is automatic** by the non-aggregated fields in the SELECT — never write GROUP BY. Aggregates inherit the select's grain wherever they appear (where/select/having). Override per-aggregate with `agg(x) by <dims>`; `by *` aggregates across all data (single row). To weight a related dimension property once per fact row, project it to the fact grain first: `auto row_birth_year <- group(enroll.student.birth_year) by enroll.student_id, enroll.course_id;` then aggregate that.\n- **Output rows are deduplicated to the select grain.** To preserve legitimate duplicates (one row per matching fact), include the fact's grain keys in the select — hidden with the PREFIX `--` if they shouldn't appear in the output.\n- **Never write `distinct`.** `count(<key>)` is already distinct because keys are unique; `count_distinct(<property>)` counts distinct values of a non-key property.\n- **Count a COMPOSITE grain with `grain(...)`.** `count(grain(order_id, item.id))` counts order+item combinations; `grain(...)` is NEVER NULL. Never count one column of a multi-key grain — a coarser key counts its own distinct values and undercounts. `count_distinct(a, b)` is accepted as sugar for `count(grain(a, b))` (distinct combinations, NULL members included).\n- **An aggregate over a group with no qualifying rows is NULL, never 0.** `sum(x ? cond) by k` yields NULL for groups where nothing matches, so `= 0` matches nothing. Test emptiness with `count(key ? cond) by k = 0` or wrap: `coalesce(sum(x ? cond), 0)`.\n- **`count(1)` is invalid** — a constant does not identify rows. Use the row key, `count(grain(...))`, or `sum(1) by <grain>`.\n- **One-column expression subqueries only.** `(select ...)` is allowed where a scalar or membership set is expected (exactly one projected column) — never as a SQL-style FROM-subquery. For related-entity filters prefer the dot-path: `where enrollments.student.state = 'TN'`, not a subselect.\n- **`--` is a HIDDEN-field prefix, not a comment** (it still changes query structure). Comments use `#` only.\n- **Aliases**: always use the full dot-path (`enroll.student.id`); alias every new expression with `as`; a select alias is usable in `having`/`order by` but NOT inside other select expressions or `where`, and must not rename a field back to an existing concept name.\n\n## Filtering\n\n`where` filters input rows before aggregation; `having` filters results after aggregation/windows. The inline filter `x ? cond` filters the immediate prior expression: `sum(credits ? enrolled = true)`.\n\nAggregates/windows in WHERE do not filter each other's inputs. Use inline filters, or a staged `then where` chain: each `then where` stage's aggregates compute over only the rows passing earlier stages (`where x = 5 then where sum(y) > 10` ≡ `where x = 5 and sum(y ? x = 5) > 10`). One rule: the SAME cross-row expression cannot gate two different stages — give one an inline filter.\n\nWindows need all rows in their range: filter AFTER the window (in `having`) rather than in `where` when the window must see the full set.\n\n## Membership (semijoins)\n\n`in` / `not in` cross models without any declared relationship — the semijoin IS a scoped intersection. The right side is a concept or expression (never a subselect); build computed sets with an inline filter: `auto big_zip <- student.zip ? (count(student.id ? student.honors = true) by student.zip) > 10; ... where school.zip in big_zip`.\n\nMembership is IDENTITY matching, not SQL three-valued logic: NULL matches NULL; the result is always TRUE or FALSE (safe as a projected flag); `not in` is the exact complement (a NULL in the set never empties the result). For strict SQL behavior, add `x is not null`.\n\nA fact model contains ALL dimensional members, so `where students.id not in enroll.student_id` is typically a tautology — use `where enroll.id is null` instead. Tuple membership `(a, b) in (m.a, m.b)` requires both right-side concepts to resolve to ONE source (model or rowset); to test against a cross-model pair, stage it through a rowset first. Examples: existence-anti-join, staged-membership.\n\n## Aggregation\n\nAn aggregate without an explicit `by` is RESPONSIVE — its grain is set where it is consumed, not where it is declared (`auto avg_credits <- avg(enroll.credits);` becomes per-department when selected with `enroll.department`). Pin a grain inline: `sum(metric) by dim1, dim2 as name`; `by` accepts bare identifiers or parenthesized expressions: `avg(price) by (substring(phone, 1, 2))`.\n\nMulti-level grouping (`by rollup|cube|grouping sets (...)`) is a clause of the WHOLE select, placed before `having`: it computes every no-explicit-grain aggregate at every level in one pass (`by rollup (d1, d2)` → leaf, per-d1 subtotal, grand total). `grouping(<field>)` = 1 on rolled-up rows — use it to label levels rather than testing for NULL. Full detail: trilogy agent-info syntax example rollup.\n\n## Window functions\n\nDefault to windows for self-referential queries (period-over-period, running totals, rank, share of group). SQL-style syntax: `rank(<key>) over (partition by <g> order by <expr> desc)` (`dense_rank`/`row_number` same shape; multi-key `rank(a, b)`); `sum(x) over (partition by g order by t)` for running totals; `lag(<field>, <offset>) over (...)` / `lead(...)` for offsets (default 1). `partition by` is optional and accepts expressions.\n\n## Data types\n\nScalars: `string`, `bytes`, `bool`, `int`, `bigint`, `float`, `double`, `number`, `numeric(p,s)` (alias `decimal`; use for exact money), `date`, `datetime`, `timestamp`, `geography`, `any`. Composites: `array<T>`, `map<K, V>`, `struct<name: T, ...>`, `enum<T>[v1, v2, ...]`. Cast with `::type` or `cast(x as type)`: `\"2020-01-01\"::date`.\n\n`?` after a type marks it nullable (`property user.middle_name string?;`). `~` prefixing a concept marks the reference as a PARTIAL subset of the concept's domain — on a select item (`~customer.id`) or in `merge a into ~b` (a ⊆ b); it tells the planner this side is not authoritative.\n\n## Expression gotchas\n\n- `x ? cond` binds to a primary — wrap arithmetic: `(a - b) ? cond`, NOT `a - b ? cond`.\n- `a || b` is NULL if either side is NULL; `concat(...)` skips NULL arguments; `concat_ws(sep, ...)` joins with sep, skipping NULLs.\n- Date parts are unquoted: `date_part(d, year)`, never `'year'`; prefer `year(d)`. `date_diff(start, end, unit)` = end − start (a shipping lag is `date_diff(sold_date, ship_date, day)`).\n- Zero-argument functions still take parentheses: `current_date()`.\n- Factor repeated calculations into a `def` macro (invoked `@name(...)`) or reusable `auto` concepts.", nt = ["aggregate: count|count_distinct|sum|max|min|avg|stddev|variance|array_agg|bool_or|bool_and|any|grouping(<arg1>) ; grouping_id(<arg1>, <arg2>, ..., <argN>)"], rt = [
1582
1593
  "aggregate: count|count_distinct|sum|max|min|avg|stddev|variance|array_agg|bool_or|bool_and|any|grouping(<arg1>) ; grouping_id(<arg1>, <arg2>, ..., <argN>)",
1583
1594
  "string: concat|concat_strict|concat_ws(<arg1>, <arg2>, ..., <argN>) ; split|strpos|contains|trim|regexp_contains|format_time|parse_time(<arg1>, <arg2>) ; len|lower|upper|ltrim|rtrim|hex(<arg1>) ; substring|replace|regexp_extract|regexp_replace(<arg1>, <arg2>, <arg3>)",
1584
1595
  "date/time: date|datetime|timestamp|second|minute|hour|day|day_of_week|week|month|quarter|year|month_name|day_name|unix_to_timestamp(<arg1>) ; date_spine(<arg1>, <arg2>) ; current_date|current_datetime()",
@@ -1593,7 +1604,7 @@ var Ye = class t {
1593
1604
  "conditional/cast: case|simple_case|coalesce|greatest|least(<arg1>, <arg2>, ..., <argN>) ; cast|nullif(<arg1>, <arg2>) ; isnull(<arg1>)",
1594
1605
  "geo: geo_from_text|geo_x|geo_y|geo_centroid(<arg1>) ; geo_point|geo_distance(<arg1>, <arg2>) ; geo_transform(<arg1>, <arg2>, <arg3>)",
1595
1606
  "other: is_not_distinct(<arg1>, <arg2>)"
1596
- ], at = [
1607
+ ], it = [
1597
1608
  "string",
1598
1609
  "bytes",
1599
1610
  "bool",
@@ -1615,6 +1626,121 @@ var Ye = class t {
1615
1626
  "unix_seconds",
1616
1627
  "unknown",
1617
1628
  "any"
1629
+ ], at = [
1630
+ {
1631
+ name: "case-when",
1632
+ title: "Conditional values with case (relabel codes / bucket a measure)",
1633
+ summary: "`case when <cond> then <val> ... else <val> end` to relabel codes or bucket a measure into a derived value, then group by it",
1634
+ body: "# `case when <cond> then <val> ... else <val> end` builds a derived value.\n# Assign it to an `auto` concept, then group/filter by it like any other.\n# Use `else null` when unmatched rows should drop out of downstream grouping.\nimport iris as iris;\n\nauto petal_size <- case\n when iris.petal_length < 2.0 then 'small'\n when iris.petal_length < 5.0 then 'medium'\n else 'large'\nend;\n\nselect\n petal_size,\n count(iris.petal_length) as flower_count,\n avg(iris.petal_width) as avg_petal_width,\norder by flower_count desc;\n"
1635
+ },
1636
+ {
1637
+ name: "chart",
1638
+ title: "Chart statement - layers, roles, settings and reference lines",
1639
+ summary: "drawing a result rather than tabulating it: `chart layer <type> ( <role> <- <expr> as <name>, ... )` is a STATEMENT that wraps its own query - the bindings ARE the select; layers, roles, scales, reference lines and `from <select>`",
1640
+ body: "# `chart` is a top-level statement, like `select` - it wraps a query AND says\n# how to draw it. It renders wherever results render: a trilogy code block in a\n# `trilogy render` report, a studio cell, terminal output, and\n# `copy into png|svg|html|pdf '<path>' from chart ...` for a standalone image.\nimport enrollments as enroll;\n\n# --- 1. Minimal form --------------------------------------------------------\n# `layer <type> ( <role> <- <expr> [as <name>], ... )`. The bindings ARE the\n# select: grouping is automatic by the non-aggregated ones, exactly as in a\n# query. NEVER write a separate `select` for a chart.\nchart layer bar ( x_axis <- enroll.department, y_axis <- sum(enroll.credits) as credits );\n\n# A COMPUTED binding must be aliased: `y_axis <- sum(enroll.credits) as credits`.\n# A bare concept reference needs no alias (`x_axis <- enroll.department`) but may\n# take one as a display label. `y_axis <- sum(...)` with no `as` is a parse error.\n#\n# GRAIN: every binding is a select column, so binding a FINER concept to any\n# role (color, group, annotation, ...) refines the grain and splits the marks -\n# exactly as adding that column to a select would. Aggregate annotations at the\n# mark's own grain (`count(enroll.id) as ...`) instead of binding a detail field.\n\n# --- 2. Every part of the statement -----------------------------------------\n# Components may appear in any order after `chart`; at least one `layer` is\n# required, and multiple `layer`s compose into one overlaid chart.\nchart\n set show_title # title from the value-axis label\n set hide_legend\n set scale_y: log # scale_x | scale_y : linear | log | sqrt\n layer bar (\n x_axis <- enroll.department,\n y_axis <- sum(enroll.credits) as credits,\n color <- enroll.completed, # one legend series per value\n annotation <- count(enroll.id) as enrollments # text label on each mark\n )\n order by credits desc # category order follows ORDER BY\n limit 10 # (ascending when there is no ORDER BY)\n place hline at 5 as target; # reference rule; `place vline at ...`\n # too. `at` takes a literal; `as <label>`\n # is optional\n\n# --- 3. `from <select>` - when the bindings cannot express the query ---------\n# Bindings alone have no `where` / `having` / hidden columns. Add `from` plus a\n# COMPLETE select, which carries its own clauses in the usual order:\nchart layer line (\n x_axis <- enroll.year,\n y_axis <- completions,\n color <- enroll.department\n )\n from\n where enroll.year >= 2016\n select\n enroll.year,\n enroll.department,\n count(enroll.id ? enroll.completed = true) as completions\n order by enroll.year asc;\n# With `from select`, EVERY binding must be a bare reference to one of the\n# select's outputs - put transformations inside the select - and layer-level\n# `order by` / `limit` are rejected: they belong to the select.\n\n# --- 4. Types ---------------------------------------------------------------\n# bar vertical bars: x_axis = category, y_axis = measure\n# barh horizontal bars: x_axis = MEASURE, y_axis = category (roles swap)\n# line x_axis = the ordered axis (date/year), y_axis = measure\n# point scatter; `size <- <measure>` scales the marks\n# area line, filled\n# headline big KPI number - needs only `x_axis <- <measure> as <name>`\n# donut the numeric binding sizes the slices; the other positional one -\n# or `color` - names them. No annotations.\n# heatmap x_axis and y_axis are BOTH categories, `color` is the measure\n# boxplot x_axis = category, y_axis = the distribution being summarized\n# `treemap` parses but no renderer implements it; the error names what does.\nchart layer barh ( x_axis <- sum(enroll.credits) as credits, y_axis <- enroll.department );\nchart layer headline ( x_axis <- count(enroll.student_id) as students );\nchart layer donut ( x_axis <- enroll.department, y_axis <- sum(enroll.credits) as credits );\nchart layer heatmap (\n x_axis <- enroll.year,\n y_axis <- enroll.department,\n color <- sum(enroll.credits) as credits\n);\n# A boxplot summarizes RAW rows, so its select has to carry the fact's row grain\n# - hidden with `--` - or output dedup collapses the distribution to the set of\n# distinct values.\nchart layer boxplot ( x_axis <- enroll.department, y_axis <- enroll.credits )\n from select --enroll.id, enroll.department, enroll.credits;\n\n# --- 5. Roles ---------------------------------------------------------------\n# x_axis, y_axis position\n# color one series per value, WITH a legend\n# size mark size (point)\n# group side-by-side bars, or a per-series split on\n# line/point/area - no legend\n# x_trellis / y_trellis small multiples (columns / rows)\n# annotation per-mark text label\n# geo reserved - parses, then raises\n# Each role may be bound at most once per layer; an unknown role name is a parse\n# error. Trellis roles cannot combine with multiple layers, placements, or\n# annotations (Vega-Lite forbids facets inside layered charts).\nchart layer bar (\n x_axis <- enroll.year,\n y_axis <- sum(enroll.credits) as credits,\n group <- enroll.department # side-by-side bars, no legend\n);\n\n# EXPLICIT SERIES COLORS: select a `string::hex` column (the trait comes from\n# `import std.color;`) alongside a `color` binding and each color value takes the\n# hex code found on its rows (missing -> gray); bind the hex column itself to\n# `color` to use the codes directly.\n"
1641
+ },
1642
+ {
1643
+ name: "correlated-exists-via-grouped-counts",
1644
+ title: "Correlated EXISTS / NOT-EXISTS over rows of the SAME model",
1645
+ summary: "translate `EXISTS other` / `NOT EXISTS other matching` over the same model into two `count(...) by <grain>` compared in `where` (`> 1` = another exists, `= 1` = no other matches) - pin the correlation grain with `by`",
1646
+ body: "# A self-referential EXISTS / NOT-EXISTS (e.g. \"the ONLY enrollee who completed\n# a course that had more than one enrollee\") becomes TWO filtered counts pinned\n# to the correlation grain, compared in `where`:\n# count(k) by <grain> > 1 # EXISTS another row in the group\n# count(k ? cond) by <grain> = 1 # NOT EXISTS another row matching cond\n# (exactly one matches: the row itself)\n# An aggregate condition in `where` groups by the SELECT grain unless pinned.\n# When the correlation grain DIFFERS from the select grain - as here\n# (correlate by course, select per student) - you MUST pin it with `by <grain>`;\n# a bare `count(...) > 0` would group by the select grain and give wrong rows.\n# SCOPE: the pin fixes only the GROUPS. A `where` aggregate always computes\n# over ALL rows of the model; the other `where` predicates never narrow its\n# input rows - exactly right for whole-table EXISTS checks like this one. If\n# the aggregate should see only already-filtered rows, use `having` or stage\n# it with `then where` (a stage's aggregates see only rows passing earlier\n# stages).\nimport enrollments as enroll;\n\nauto enrollees_per_course <- count(enroll.student_id) by enroll.course;\nauto completers_per_course <- count(enroll.student_id ? enroll.completed = true) by enroll.course;\n\nwhere\n enroll.completed = true\n and enrollees_per_course > 1 # the course had at least one OTHER enrollee\n and completers_per_course = 1 # and no OTHER enrollee completed it\nselect\n enroll.student_id,\n count(enroll.course) as sole_completions\norder by sole_completions desc\nlimit 100;\n"
1647
+ },
1648
+ {
1649
+ name: "dedup-before-aggregate",
1650
+ title: "Deduplicate identical rows before aggregating (UNION DISTINCT)",
1651
+ summary: "a `rowset <- select <cols>` collapses identical tuples to one row (SELECT-grain group by) before a downstream sum - the `distinct`/UNION substitute",
1652
+ body: "# `distinct` and UNION are not available. To collapse identical tuples to one\n# row BEFORE aggregating (so duplicates don't double-count), materialize the\n# tuple in a `rowset <- select <cols>`: a rowset select groups at its own grain,\n# emitting one row per distinct tuple. Then aggregate over the rowset's columns.\n# - ALIAS every rowset column you reference downstream with `as`. Without `as`,\n# the column keeps its FULL source path under the rowset name - `select\n# sales.item.brand_id` is reachable as `rs.sales.item.brand_id`, NOT\n# `rs.brand_id`. `as brand_id` makes it `rs.brand_id`.\nimport enrollments as enroll;\n\n# Collapse repeat (student, course) enrollments to one row each (a student who\n# took a course in multiple years counts once).\nrowset unique_sc <- select\n enroll.student_id as sid,\n enroll.course as course,\n;\n\n# Count over the deduplicated rows - identical (student, course) pairs counted once.\nselect\n unique_sc.course,\n count(unique_sc.sid) as distinct_students,\norder by unique_sc.course asc;\n"
1653
+ },
1654
+ {
1655
+ name: "except-intersect-setops",
1656
+ title: "Set difference / co-occurrence across sources with except(...) / intersect(...)",
1657
+ summary: "set difference or co-occurrence across sources",
1658
+ body: "# `except(...)` and `intersect(...)` are relational table-valued functions with the\n# SAME shape as `union(...)`: TWO OR MORE inline self-contained select arms, matched\n# by COLUMN POSITION to the trailing `-> (...)` output list. They are SQL SET\n# operators, which differ from the union stack in three ways:\n# - the output is DEDUPLICATED (distinct rows);\n# - whole rows are compared with NULL-SAFE equality (NULL matches NULL - the\n# same identity semantics as `in` / `not in` membership);\n# - for `except(...)` ARM ORDER MATTERS: the FIRST arm is the base population and\n# every later arm is subtracted from it, left to right.\n# Use `except` for \"combinations in A that never appear in B (or C)\" and\n# `intersect` for \"combinations present in every source\".\nimport enrollments as enroll;\n\n# Distinct (course, year) combinations Biology offered that Chemistry did not.\nwith bio_only as except(\n (where enroll.department = 'Biology'\n select enroll.course as course, enroll.year as year),\n (where enroll.department = 'Chemistry'\n select enroll.course as course, enroll.year as year)\n) -> (course, year);\n\nselect\n bio_only.course,\n bio_only.year,\norder by bio_only.course asc nulls first, bio_only.year asc nulls first\nlimit 100;\n\n# Counting the surviving combinations is the idiomatic anti-membership count\n# (e.g. \"customers who bought in store but never online\"):\n# from except((...store combos...), (...web combos...)) -> (last, first, day)\n# select count(day) as combos;\n# ---------------------------------------------------------------------------\n# NOTES:\n# - Same arm rules as `union(...)`: inline arms only, positional binding, equal\n# column count/order/types per arm; reference outputs as `<rowset>.<output>`.\n# - `except((a), (b), (c))` means (a EXCEPT b) EXCEPT c - subtract left to right.\n# `intersect(...)` keeps rows present in EVERY arm (order does not matter).\n# - Outputs are DISTINCT rows: aggregate them directly (`count(x)`) for \"how many\n# unique combinations\" - no extra dedup step needed.\n"
1659
+ },
1660
+ {
1661
+ name: "existence-anti-join",
1662
+ title: "Existence / anti-join across models",
1663
+ summary: "keeping or excluding rows by whether a key matches another model",
1664
+ body: "# `in` / `not in` against another model's key is the semi-join / anti-join\n# idiom - no manual JOIN. Use it instead of `count(...) is null` hacks.\n# Membership is IDENTITY matching and total: NULL matches NULL, the result is\n# never NULL, and `not in` is the EXACT complement of `in` - a NULL in the set\n# does not empty an anti-join (no SQL NOT-IN footgun), and NULL-keyed rows land\n# in exactly one of the two sides. For strict SQL parity, filter NULLs\n# explicitly (`where key is not null and key in other.key`).\nimport students as students;\nimport enrollments as enroll;\n\n# semi-join: majors with at least one student enrolled in 'Biology 101'\nauto bio_students <- enroll.student_id ? enroll.course = 'Biology 101';\nwhere students.id in bio_students\nselect students.major, count(students.id) as enrolled_students;\n\n# anti-join: students who never enrolled in ANY course\nwhere students.id not in enroll.student_id\nselect students.major, count(students.id) as never_enrolled;\n\n# NULLABLE FLAG: a flag that is true only when a match exists and NULL otherwise\n# (e.g. `enroll.completed` set only for graded rows) needs `is null` for the\n# \"never matched\" case - `not flag` evaluates NULL→dropped, losing those rows.\nwhere enroll.completed is null\nselect enroll.student_id;\n\n# COMPOSITE (multi-column) membership: to test a MULTI-PART key against a set,\n# wrap both sides in matching tuples - `(a, b) in (set.a, set.b)`. This matches\n# ROW-WISE (the whole tuple must appear as one row of the set), so it is NOT the\n# same as `a in set.a and b in set.b`, which matches the parts independently and\n# lets unrelated cross-pairs through. Build the set as a `rowset` selecting the\n# key columns, then test the tuple with `in` / `not in`. Prefer this over\n# concatenating the parts into a single string key.\nrowset multi_course_pairs <- select\n enroll.department as dept,\n enroll.course as course,\nhaving count_distinct(enroll.year) > 1; # (dept, course) offered in >1 year\n\nwhere (enroll.department, enroll.course)\n in (multi_course_pairs.dept, multi_course_pairs.course)\nselect enroll.student_id, count(enroll.id) as enrollments;\n"
1665
+ },
1666
+ {
1667
+ name: "filtered-aggregate",
1668
+ title: "Filtered aggregate - aggregate only the rows matching a condition",
1669
+ summary: "aggregating only the rows matching a condition, and counting rows correctly",
1670
+ body: "# `?` is a local filter - a filter that applies only to the prior expression-\n# several differently-filtered measures can share one select. Add `by <grain>`\n# to pin an aggregate to a grain other than the select's.\nimport iris as iris;\n\nselect\n iris.species,\n avg(iris.petal_length) as avg_petal,\n avg(iris.petal_length ? iris.petal_width > 1.5) as avg_petal_wide_only,\n count(iris.petal_length ? iris.sepal_length > 6.0) as long_sepal_count,\norder by iris.species asc;\n\n# COUNTING ROWS: `count(k ? cond)` counts DISTINCT values of k - `count` is\n# distinct-by-argument. To count matching ROWS, count the model's unique grain /\n# row key (often named `row_key`), NOT a non-unique sub-key. e.g. counting line\n# items by `count(line_number ? cond)` collapses to the number of distinct line\n# numbers (1..7) and undercounts - use `count(row_key ? cond)`.\n\n# COMPOSITE GRAIN: when SEVERAL keys together identify the thing you are counting,\n# name them with `grain(...)` instead of picking one:\n# count(grain(sales.order_id, sales.item_id)) # order+item lines\n# count_distinct(grain(cust.first_name, cust.last_name, sales.date))\n# `grain(...)` is NEVER NULL, so a combination with a missing member (a customer\n# with no last name) still counts - whereas `count(last_name)` silently drops it.\n# It composes with `?` like any expression: count(grain(a, b) ? cond).\n"
1671
+ },
1672
+ {
1673
+ name: "nested-aggregate-group-average",
1674
+ title: "Compare a per-entity total to the group average of those totals",
1675
+ summary: "the \"above 1.2x the group norm\" ask: inner `sum(x) by entity, group`, outer `avg(inner) by group`; use WHERE for the comparison when a separate output-only filter must not narrow the benchmark",
1676
+ body: "# Compare each entity's total to the GROUP AVERAGE of those per-entity totals\n# (the \"above 1.2x the group norm\" pattern). Give EACH grain its own `by`: the\n# inner total is per (entity, group); the outer average re-aggregates those\n# totals up to the group. \n# since the aggregates are in the having, both are aggregates *after* the id filter is applied\nimport enrollments as enroll;\n\nauto student_dept_credits <- sum(enroll.credits) by enroll.student_id, enroll.department;\nauto dept_avg_credits <- avg(student_dept_credits) by enroll.department;\n\nwhere enroll.student_id > 103\nselect\n enroll.student_id,\nhaving student_dept_credits > 1.2 * dept_avg_credits;\n\n# BENCHMARK OVER FULL POPULATION + FILTERED OUTPUT: if the department average must span\n# EVERY student but the output should contain only target students (id > 103), put the aggregate\n# comparison in WHERE alongside the output-only filter. Aggregate conditions in WHERE do\n# not filter one another's inputs, so `student_id > 103` does not narrow the department\n# benchmark. Do NOT put the comparison in HAVING after the output filter; that computes a\n# target-students-only department average.\nwhere\n student_dept_credits > 1.2 * dept_avg_credits\n and enroll.student_id > 103\nselect\n enroll.student_id,\n enroll.department,\n student_dept_credits,\n;\n\n# SCOPED INPUT POPULATION: when the totals and their group average must use only\n# rows from a specific period, bind that condition inside the inner aggregate.\n# A separate output-only filter still belongs in the final WHERE and does not\n# narrow the rows feeding either aggregate.\nauto student_dept_credits_2024 <- sum(enroll.credits ? enroll.year = 2024)\n by enroll.student_id, enroll.department;\nauto dept_avg_credits_2024 <- avg(student_dept_credits_2024) by enroll.department;\n\nwhere\n student_dept_credits_2024 > 1.2 * dept_avg_credits_2024\n and enroll.student_id > 103\nselect\n enroll.student_id,\n enroll.department,\n student_dept_credits_2024,\n;\n\n# HIDDEN GROUP KEY: if the requested output is one row per (student, department)\n# but displays only student_id, keep department in the select with the `--`\n# PREFIX. Otherwise identical displayed student rows are deduplicated and the\n# required per-department row grain is lost.\nwhere student_dept_credits_2024 > 1.2 * dept_avg_credits_2024\nselect\n enroll.student_id,\n --enroll.department,\n;\n\n# For more complex boundaries, compute the full-population benchmark in a separate rowset\n# with no output filter, then apply the subset filter only in the final selection.\n"
1677
+ },
1678
+ {
1679
+ name: "pivot-columns",
1680
+ title: "Pivot dimension values into columns",
1681
+ summary: "turn distinct values of a dimension into columns with a reusable `def` macro + filtered aggregate",
1682
+ body: "# A `def` macro expands inline at each `@call`. Pair it with a filtered\n# aggregate to project one column per category value (a pivot). With no row\n# dimension in the select this yields a single summary row.\nimport iris as iris;\n\ndef species_avg(s) -> avg(iris.petal_length ? iris.species = s);\n\nselect\n @species_avg('setosa') as setosa_avg_petal,\n @species_avg('versicolor') as versicolor_avg_petal,\n @species_avg('virginica') as virginica_avg_petal,\n;\n"
1683
+ },
1684
+ {
1685
+ name: "python-datasource",
1686
+ title: "Python script datasource - Arrow IPC producer plus Trilogy model",
1687
+ summary: "run a local Python script as a datasource: wrap a function in `trilogy.io.run`, which writes the Arrow IPC stream to stdout for you from a table, dataframe, or list of dicts; declare concepts, map script columns in `datasource (...)`, use `grain (...) file `path.py`;`, then reference locally declared concepts WITHOUT the datasource name as a prefix",
1688
+ body: "# Python datasources require `enable_python_datasources = true` under\n# `[engine.config]` in trilogy.toml. Create these TWO files side by side.\n#\n# File 1: fibonacci.py\n# ---------------------------------------------------------------------------\n# #!/usr/bin/env -S uv run\n# # /// script\n# # requires-python = \">=3.11\"\n# # dependencies = [\"pyarrow>=16\", \"pytrilogy\"]\n# # ///\n#\n# from trilogy.io import run\n#\n#\n# def fibonacci():\n# values = [0, 1]\n# while len(values) < 20:\n# values.append(values[-1] + values[-2])\n# return [{\"index\": i, \"value\": v} for i, v in enumerate(values)]\n#\n#\n# if __name__ == \"__main__\":\n# raise SystemExit(run(fibonacci))\n#\n# `run` converts whatever you return -- a pyarrow table, a pandas/polars frame,\n# a list of dicts, or an iterator of those -- and gives the script a CLI\n# (`--limit`, `--filter`, `--columns`, `--describe`). Run\n# `trilogy source describe ./fibonacci.py` to print the datasource block below\n# instead of writing it against a guessed schema.\n#\n# IMPORTANT: stdout must contain ONLY the Arrow IPC stream. Send diagnostics\n# to stderr. A PEP 723 block lets `uv run` install script dependencies.\n#\n# File 2: query.preql\n# ---------------------------------------------------------------------------\nkey index int;\nproperty index.value int;\n\n# The left side is the physical Arrow column; the right side is the Trilogy\n# concept declared above. The file suffix identifies this as a Python source.\ndatasource fibonacci_numbers (\n index: index,\n value: value,\n)\ngrain (index)\nfile `./fibonacci.py`;\n\n# A datasource declaration does NOT create a namespace. Use local concepts as\n# `index` and `value`, NOT `fibonacci_numbers.index` / `.value`.\nselect\n index,\n value,\norder by index;\n"
1689
+ },
1690
+ {
1691
+ name: "query-structure",
1692
+ title: "Full query structure - every clause and where it goes (+ rowsets)",
1693
+ summary: "the exact clause order of a query, and how to define a rowset",
1694
+ body: "# Every Trilogy query has the SAME fixed skeleton. Top-level statements come\n# first (each ends with `;`), then ONE query whose clauses appear in this exact\n# order - each clause is OPTIONAL except `select`:\n#\n# <top-level statements>; # import / key / property / auto / metric /\n# # rowset / merge / def / datasource / parameter\n# where <row condition> # 1. filter INPUT rows (BEFORE aggregation)\n# select <col>, <agg> as name, # 2. the projection - grouping is AUTOMATIC by the\n# # non-aggregated columns (never write GROUP BY)\n# <subset|union join a = b (=c)?>* # 3. blend models (zero or more joins; one per concept RIGHT AFTER the\n# # select list - the SQL-like spot)\n# having <result condition> # 4. filter on an AGGREGATED / joined RESULT\n# order by <col> asc|desc # 5. sort\n# limit <n> # 6. cap rows\nimport enrollments as enroll;\nimport students as students;\n\n# --- TOP-LEVEL DEFINITIONS (reusable; all live ABOVE the query) -------------\n# An `auto`/`metric`/`property`/`key` defines a field. It expands inline wherever\n# referenced (a macro), re-evaluated in the referencing query's scope.\nauto completed_credits <- sum(enroll.credits ? enroll.completed = true);\n\n# A ROWSET is a NAMED select: `rowset <name> <- <a full select>;`. It runs as its\n# own scoped query (its own where/select/having) and exposes its outputs as\n# `<name>.<col>`. ALIAS every column you reuse downstream with `as`.\nrowset dept_totals <- select\n enroll.department as department,\n sum(enroll.credits) as total_credits,\n;\n\n# --- THE QUERY - clauses in the fixed order above ---------------------------\nwhere enroll.year >= 2015 # 1. WHERE: per-row filter, before aggregation\nselect # 2. SELECT: grouped automatically by students.major\n students.major,\n count(enroll.id) as enrollments,\n --completed_credits, # a leading `--` HIDES a column from the output\nsubset join enroll.student_id = students.id # 3. JOIN: blend students onto enrollments (after the select list)\nhaving completed_credits > 0 # 4. HAVING: condition on an aggregated RESULT\norder by enrollments desc nulls first # 5. ORDER BY\nlimit 100; # 6. LIMIT\n\n# Reference a rowset's outputs as `<rowset>.<output>` in a later query.\nselect\n dept_totals.department,\n dept_totals.total_credits,\norder by dept_totals.total_credits desc nulls first\nlimit 100;\n\n# ---------------------------------------------------------------------------\n# NOTES:\n# - Joins sit RIGHT AFTER the select list (the SQL-like, preferred spot). They\n# may also appear before `select` - both parse - but keep them after the\n# select list for consistency.\n# - `where` filters INPUT rows (before aggregation). To filter on something only\n# known AFTER the join / aggregation (e.g. comparing two joined rowsets'\n# counts), select that value (hide it with `--`) and test it in `having` -\n# `having` is the post-aggregation/output filter. There is only ONE `where`.\n# - Output rows dedupe to the select grain. To keep legitimate duplicates (one\n# row per matching fact), select the fact's grain keys - hide with `--` if\n# they shouldn't appear in the output.\n# - No FROM, GROUP BY, DISTINCT, SELECT *, subqueries, or SQL-style set/JOIN\n# operators. Grouping is automatic; blend with the scoped `join` above.\n"
1695
+ },
1696
+ {
1697
+ name: "rank-over-rollup",
1698
+ title: "Rank within each rollup level (one window over the rollup)",
1699
+ summary: "rank rollup subtotals/leaves with a SINGLE `rank(a,b) over (partition by level, parent ...)` - not separate ranks per level",
1700
+ body: "# When ranking the rows of a ROLLUP within each hierarchy level, use ONE\n# multi-key window over the rollup output. `grouping()` gives the level; the\n# window partitions by (level, parent) so leaves rank within their parent and\n# subtotals rank among themselves - all in one pass. Do NOT write separate rank\n# concepts per level (they mis-rank across the wrong row set). The aggregates and\n# grouping() level concepts are plain - the select's `by rollup (…)` clause stamps\n# the grouping onto all of them.\nimport enrollments as enroll;\n\nauto total <- count(enroll.id);\nauto g_course <- grouping(enroll.course);\nauto g_year <- grouping(enroll.year);\nauto level <- g_course + g_year; # 0 = leaf, 1 = course subtotal, 2 = grand total\nauto parent <- case when g_year = 0 then enroll.course else null end;\nauto rnk <- rank(enroll.course, enroll.year)\n over (partition by level, parent order by total desc);\n\nselect\n enroll.course,\n enroll.year,\n total,\n level,\n --parent,\n rnk,\nby rollup (enroll.course, enroll.year)\norder by level desc nulls first, parent asc nulls first, rnk asc nulls first\nlimit 100;\n\n# ---------------------------------------------------------------------------\n# NOTE - a composite aggregate over a rollup (a difference/ratio of sums) needs\n# NO special handling: `sum(profit) - sum(loss) as net` rolls up both operands\n# to the same levels, because `by rollup (…)` is a property of the whole select,\n# not of one aggregate.\n"
1701
+ },
1702
+ {
1703
+ name: "rollup",
1704
+ title: "Subtotals + grand total in one pass with `by rollup (…)`",
1705
+ summary: "subtotals and a grand total in one pass, and labeling the levels",
1706
+ body: "# A `by rollup (d1, d2)` clause AFTER the select list (before having/order by)\n# computes the whole select at MULTIPLE grain levels in one pass - the grouping\n# sets (d1, d2), (d1), () - i.e. per-(d1,d2) leaf rows, a subtotal per d1, and a\n# grand total. (`by cube (d1, d2)` = every combination; `by rollup ()` over the\n# select's own grain.) It applies to EVERY aggregate in the select, so all\n# measures share one consistent grouping.\nimport enrollments as enroll;\n\n# `grouping(d)` = 1 when d is ROLLED UP on a row (a subtotal/grand-total row), 0 on\n# a leaf - use it to LABEL those rows: the rolled-up dimension is otherwise NULL\n# and would collide with any genuine NULL in the data.\nselect\n case when grouping(enroll.department) = 1 then 'ALL DEPARTMENTS'\n else enroll.department end as department,\n case when grouping(enroll.course) = 1 then 'all courses'\n else enroll.course end as course,\n sum(enroll.credits) as total_credits\nby rollup (enroll.department, enroll.course)\nhaving total_credits > 0\norder by department asc nulls first, course asc nulls first\nlimit 100;\n\n# ---------------------------------------------------------------------------\n# NOTES:\n# - CLAUSE ORDER matches SQL: the `by rollup (…)` grouping clause comes BEFORE\n# `having` (select list -> by rollup -> having -> order by -> limit). The\n# HAVING filters EVERY grouping level - subtotal and grand-total rows too.\n# - SORT: use EXACTLY the sort the question asks for. Rollup rows are ordinary rows\n# and sort with everything else (their rolled-up dimensions are NULL, hence\n# `nulls first`/`nulls last` decides where they land). Sorting by LEVEL is OPTIONAL\n# and only when you actually want leaves/subtotals/totals grouped together: ORDER BY\n# cannot compute a fresh aggregate, so PROJECT the level (`grouping(a) + grouping(b)`)\n# as a HIDDEN (`--`) column and sort by its alias:\n# --grouping(enroll.department) + grouping(enroll.course) as _level\n# order by _level asc, department asc nulls first\n# Do NOT add `_level` to a sort the question already specified - a leading sort key\n# changes WHICH rows survive a `limit`, so it can silently drop the totals.\n# - COMPOSITE measures just work: `sum(a) - sum(b) as net` rolls up BOTH operands\n# to the same levels because the `by rollup (…)` clause covers the whole select.\n# To zero-fill a measure, wrap it: `coalesce(sum(a), 0) as a`.\n# - FILTER-BEFORE-ROLLUP vs `having`: `by rollup (…) having x > t` filters EVERY level AFTER\n# the rollup, so the subtotal/total rows still AGGREGATE the below-threshold leaves (they\n# just aren't displayed). To keep only leaf groups meeting a threshold and roll up JUST\n# those survivors, filter the leaves in a rowset FIRST (`rowset kept <- select … having x >\n# t`), then `by rollup ()` over `kept`. `having` on the rollup itself is the wrong tool when\n# subtotals must exclude the filtered-out leaves.\n"
1707
+ },
1708
+ {
1709
+ name: "row-level-where-vs-having",
1710
+ title: "Row-level filter (WHERE) vs aggregate filter (HAVING) - don't wrap a per-row threshold in max()/min()",
1711
+ summary: "choosing between WHERE and HAVING for a threshold condition",
1712
+ body: "# A non-aggregate, per-ROW condition belongs in WHERE - it filters the rows that\n# feed every aggregate, BEFORE grouping. HAVING is ONLY for filtering on an\n# aggregate RESULT. These are different operations; do not substitute one for the\n# other.\nimport iris as iris;\n\n# RIGHT - keep only large-sepal FLOWERS, then average their petals.\n# `sepal_length > 6.0` is a per-row test on a column -> WHERE.\nwhere iris.sepal_length > 6.0\nselect\n iris.species,\n avg(iris.petal_length) as avg_petal,\norder by iris.species asc;\n\n# WRONG - `having max(iris.sepal_length) by iris.species > 6.0` does NOT keep\n# large-sepal rows. It keeps whole SPECIES whose single largest flower exceeds\n# 6.0, then averages ALL of that species' flowers (small ones included). Wrapping\n# a per-row threshold in max()/min() in HAVING silently changes the meaning, and\n# AND-ing it with another filter (e.g. a rank cutoff) often yields 0 rows because\n# the two now select disjoint populations.\n#\n# Pick by what the condition is ABOUT:\n# per-row value of a column -> WHERE `where x.amount > 10000`\n# an aggregate's result -> HAVING `having total_amount > 10000`\n# (select it, hide with `--`)\n# aggregate just SOME rows -> inline `?` `sum(x.amount ? x.amount > 10000)`\n#\n# An aggregate condition CAN go in WHERE as a pre-filter (`where count(x) > 0`);\n# it groups by the SELECT grain unless pinned with `by <grain>` (`by *` = a\n# whole-table total). SCOPE: a WHERE aggregate computes over ALL rows of the\n# model - the other WHERE predicates never narrow its input (HAVING, selected,\n# and inline-`?` aggregates DO see the filtered rows). `by` pins the groups,\n# never the input rows; for an aggregate over the filtered rows use `having`\n# or a `then where` stage.\n"
1713
+ },
1714
+ {
1715
+ name: "scoped-join",
1716
+ title: "Blend two models in one query with a scoped subset/union join",
1717
+ summary: "blending a second fact model or rowset into one query",
1718
+ body: "# A query-scoped `join` blends a second model into ONE select without editing the\n# model files (the query-local equivalent of `merge`). A join DECLARES how the\n# two keys' value domains relate - it never drops rows (rendering is always\n# row-preserving; restrict rows with explicit `is not null` filters):\n# subset join a = b -> declares a's values are contained in b's (a is the\n# narrow side, b superset, all values in a in b)\n# union join a = b -> neither domain contains the other; the key coalesces\n# and unmatched rows from EITHER side are kept\n# Place the clause(s) RIGHT AFTER the `select` list (the SQL-like spot).\nimport enrollments as enroll;\nimport students as students;\n\n# (1) SINGLE-KEY blend: bring students onto enrollments by the shared student\n# key. Enrollment student ids are a SUBSET of all student ids, so declare that;\n# majors with no enrollments are preserved with a NULL count.\nselect\n students.major,\n count(enroll.id) as enrollments,\nsubset join enroll.student_id = students.id\norder by enrollments desc nulls first\nlimit 100;\n\n# The select plus its join clause(s) is ONE statement, so it must end with `;`\n# even when no `order by`/`limit` follows the join - a quick probe that stops\n# right after the join clause still needs the terminator:\nselect\n students.major,\n count(enroll.id) as enrollments,\nsubset join enroll.student_id = students.id;\n\n# ---------------------------------------------------------------------------\n# (2) MULTI-KEY blend - JOIN ON THE FULL GRAIN. When facts share a COMPOSITE\n# grain, write ONE join clause per key. `trilogy explore` shows each fact's grain\n# as `@<k1, k2>` (e.g. a sales/returns fact keyed `@<department, course>`);\n# matching only ONE key fans out and DOUBLE-COUNTS. Two rowsets, joined on both\n# of their shared keys:\nrowset completed_by <- where enroll.completed = true\n select enroll.department as dept, enroll.course as course, count(enroll.id) as completed_cnt;\nrowset incomplete_by <- where enroll.completed = false\n select enroll.department as dept, enroll.course as course, count(enroll.id) as incomplete_cnt;\nrowset null_completed <- where enroll.completed is null\n select enroll.department as dept, enroll.course as course, count(enroll.id) as null_completed_cnt;\n\nselect\n completed_by.dept,\n completed_by.course,\n completed_by.completed_cnt,\n incomplete_by.incomplete_cnt,\n null_completed.null_completed_cnt,\nunion join completed_by.dept = incomplete_by.dept = null_completed.dept\nunion join completed_by.course = incomplete_by.course = null_completed.course\norder by completed_by.dept asc nulls first\nlimit 100;\n\n# ---------------------------------------------------------------------------\n# (3) SELF-PAIR across two periods/sets (period-over-period) - a useful shape\n# for \"this year vs last year\": build one rowset per period (each aggregated to\n# the SAME keys), filter to restrict to output to one side or the other\n# in having clause.\n\nrowset y2020 <- where enroll.year = 2020 select enroll.department as dept, count(enroll.id) as cnt;\nrowset y2021 <- where enroll.year = 2021 select enroll.department as dept, count(enroll.id) as cnt;\n\nselect\n y2020.dept,\n y2020.cnt as cnt_2020,\n y2021.cnt as cnt_2021,\n y2021.cnt - y2020.cnt as yoy_diff,\nunion join y2020.dept = y2021.dept\nhaving y2021.cnt is not null\norder by yoy_diff desc nulls first\nlimit 100;\n\n# ---------------------------------------------------------------------------\n# (4) EXPRESSION join keys - a key may be ANY expression (arithmetic, an\n# aggregate, a window), not just a bare field. Here each year is matched to the\n# PRIOR year by joining on an OFFSET key (`prev_year.yr + 1`), so the prior\n# year's count lands on the current row without a self-merge. Only `=` equality\n# is supported (a key below comparison level - wrap a comparison in parens).\nrowset cur_year <- select enroll.year as yr, count(enroll.id) as cnt;\nrowset prev_year <- select enroll.year as yr, count(enroll.id) as cnt;\n\nselect\n cur_year.yr,\n cur_year.cnt as this_year,\n prev_year.cnt as prior_year,\nunion join cur_year.yr = prev_year.yr + 1\norder by cur_year.yr asc\nlimit 100;\n\n# NOTES:\n# - JOIN ON THE FULL GRAIN: one `join` clause per key in the two facts' shared\n# `@<...>` grain. Under-joining (one key of a multi-key grain) can cause\n# duplication. Chain `= c` to also pull a base key into the join\n# group so its properties stay reachable (`union join a.k = b.k = base.k`).\n# - Stacked clauses of the SAME type can share one prefix via `and`:\n# `union join a.k1 = b.k1 and a.k2 = b.k2` == two stacked `union join` clauses.\n# `and` joins distinct KEY-EQUALITY groups (not filters); `= c` chains keys into\n# ONE group - both compose: `union join a.k = b.k = base.k and a.k2 = b.k2`.\n# A COMMA never separates join groups (it is a select-list separator only):\n# `union join a.k1 = b.k1, a.k2 = b.k2` is an error - use `and`.\n# - Terminate the statement with `;` after the LAST clause, whatever that clause\n# is (join, `having`, `order by`, `limit`).\n# - A join key may be ANY expression (a computed/offset key, an aggregate, a\n# window), not only a bare field - see (4). Only `=` equality is supported.\n# - Joins NEVER drop rows: for an intersection (rows present in BOTH sides),\n# add explicit conditions - `where <side_a attr> is not null and <side_b\n# attr> is not null`. A one-sided `is not null` keeps the other side's\n# exclusive rows. Either spelling reads as \"did this side match?\": the\n# side's own JOIN KEY (`where a.k is not null`, tested per side even though\n# the projected key is the coalesced group axis) or any of its measures /\n# attributes (`where a.cnt is not null`). Same in `having`.\n# - `left`/`full`/`inner`/`right`/`cross join` are NOT valid query-scoped joins\n# (they error) - only `subset`/`union`. Map an old `left join a = b` to\n# `subset join b = a` (swap operands) and `full join a = b` to\n# `union join a = b`. A union key-group must be ALL union (can't mix with\n# subset on the SAME key; `union join a = b = c` chains one union group);\n"
1719
+ },
1720
+ {
1721
+ name: "set-intersect-difference",
1722
+ title: "Set intersection / difference on a multi-column key (presence per group)",
1723
+ summary: "count tuples present in / absent from other groups via per-group presence flags - the multi-column INTERSECT/EXCEPT idiom",
1724
+ body: "# To intersect or difference rows on a MULTI-COLUMN key (the SQL INTERSECT /\n# EXCEPT of several columns), group by the full key tuple and build a presence\n# flag per set with `sum(case when <set> then 1 else 0 end)`. Grouping keeps\n# NULL key parts as their own group (correct INTERSECT/EXCEPT semantics).\n# (Avoid concatenating nullable fields into a single key: `||` over a NULL\n# yields NULL, and `concat()`/`concat_ws()` skip NULLs, which conflates\n# distinct tuples - tuple grouping handles NULL parts correctly.)\nimport enrollments as enroll;\n\n# Presence of each (student_id, course) tuple among completed vs open enrollments.\nauto in_completed <- sum(case when enroll.completed = true then 1 else 0 end)\n by enroll.student_id, enroll.course;\nauto in_open <- sum(case when enroll.completed = false then 1 else 0 end)\n by enroll.student_id, enroll.course;\n\nselect\n # intersection: tuples appearing in BOTH sets\n sum(case when in_completed > 0 and in_open > 0 then 1 else 0 end) as in_both,\n # difference: tuples completed but never open (completed EXCEPT open)\n sum(case when in_completed > 0 and in_open = 0 then 1 else 0 end) as completed_only,\n;\n"
1725
+ },
1726
+ {
1727
+ name: "staged-membership",
1728
+ title: "Stage a coarse-grain membership set, then filter a fine-grain query",
1729
+ summary: "filtering a fine-grain query by a set computed at a coarser grain",
1730
+ body: "# When a query filters on a condition computed at a COARSER grain (e.g. \"items\n# sold in all 3 channels\", \"courses offered in more than one year\"), compute that\n# membership set in its own `rowset` and filter the fine-grain query with `in`.\n# Staging keeps the coarse aggregate from entangling with the fine-grain\n# aggregates (which otherwise re-aggregate / over-count).\nimport enrollments as enroll;\n\n# Coarse stage: courses offered in more than one year.\nrowset multi_year <- select\n enroll.course as course,\n --count_distinct(enroll.year) as year_count,\nhaving year_count > 1\n;\n\n# Fine stage: enrollments per course, only for those courses.\nwhere enroll.course in multi_year.course\nselect\n enroll.course,\n count(enroll.id) as enrollments,\norder by enrollments desc\nlimit 100;\n"
1731
+ },
1732
+ {
1733
+ name: "union-stack-channels",
1734
+ title: "Stack rows from several sources/channels with the union(...) TVF",
1735
+ summary: "stacking several sources/channels together as rows",
1736
+ body: "# `union(...)` is a relational table-valued function: it STACKS the rows of TWO OR\n# MORE self-contained `select` arms, positionally (like SQL `UNION ALL`), into one\n# named result. Use it to combine CHANNELS / SOURCES / labeled populations - ONE\n# ARM PER SOURCE, to any amount. It\n# is a row STACK, NOT a key-join: arms are matched by COLUMN POSITION, so every arm\n# must project the same number of columns, in the same order and types, as the\n# trailing `-> (...)` output list.\nimport enrollments as enroll;\n\n# IMPORTANT: each arm is an INLINE, self-contained select written directly inside\n# the `union(...)` - you CANNOT pass the name of a previously-defined rowset as an\n# arm; put the full select logic (its own `where`, its own aggregation) in the arm.\n# The arms' i-th column maps to the i-th name in the `-> (...)` output list; the\n# surface names inside each arm need not match - only POSITION does. Each arm may\n# group by its own dims (here `course`), so an arm can emit MANY rows.\nwith by_channel as union(\n (where enroll.department = 'Biology'\n select\n 'Biology' as channel,\n enroll.course as course,\n count(enroll.id) as enrollments,\n ),\n (where enroll.department = 'Chemistry'\n select\n 'Chemistry' as channel,\n enroll.course as course,\n count(enroll.id) as enrollments,\n ),\n (where enroll.department = 'Physics'\n select\n 'Physics' as channel,\n enroll.course as course,\n count(enroll.id) as enrollments,\n )\n) -> (channel, course, enrollments);\n\n# Reference the stacked outputs as `<rowset>.<output>`.\nselect\n by_channel.channel,\n by_channel.course,\n by_channel.enrollments,\norder by by_channel.channel, by_channel.enrollments desc nulls first\nlimit 100;\n\n# ---------------------------------------------------------------------------\n# NOTES:\n# - Pass TWO OR MORE arms - add a fourth/fifth the same way; one arm per source.\n# - The trailing `-> (channel, course, enrollments)` NAMES the positional outputs;\n# column order / count / type must line up across every arm.\n# - Every union output is treated as a KEY (grain component). To RE-AGGREGATE the\n# stack (e.g. a grand total across channels), wrap it in an outer aggregate:\n# select sum(by_channel.enrollments) by rollup by_channel.channel as total;\n# - An arm may carry its OWN query-scoped join (`select ... subset join b = a`\n# after its select list) - localize each source's join to the arm that needs it.\n# - This is NOT the forbidden SQL `UNION` keyword between two selects; it is the\n# `union(...)` function form - the cleanest way to stack rows from several\n# sources.\n# - AGGREGATING across a union/set-op: an arm's `select` dedups to its own output\n# grain BEFORE stacking (all selects are select distinct), \n# so stacking raw per-row values (a measure, a `1` flag)\n# and summing/counting OUTSIDE the union silently undercounts whenever two source\n# rows in one arm project the same tuple (e.g. two same-priced sales in a week).\n# Pre-aggregate INSIDE each arm (`sum(sales) as v`, `sum(1) as cnt` - as this\n# example's `count(enroll.id)` does), or pull the arm's grain key through as an\n# extra output column so every fact row stays distinct (downstream selects can\n# just ignore it). And count a GUARANTEED-non-null key, never a nullable display\n# column: `except`/`intersect` keep all-null rows (NULL-safe), which\n# `count(nullable_col)` then silently skips.\n"
1737
+ },
1738
+ {
1739
+ name: "window-period-over-period",
1740
+ title: "Period-over-period ratio with a window function",
1741
+ summary: "`lead`/`lag` over (order by ...) to compare a row to an offset period; build a scoped set with `<col> ? <cond>` and filter with `in`",
1742
+ body: "# `<col> ? <cond>` builds a filtered set of values you can match with `in`.\n# lead()/lag() over (order by ...) compares each row to an offset row - here\n# each year's enrollment count divided by the PRIOR year's.\nimport enrollments as enroll;\n\nauto recent_years <- enroll.year ? enroll.year >= 2015;\n\ndef vs_prior_year(amt) -> round(amt / (lag(amt, 1) over (order by enroll.year asc)), 2);\n\nwhere enroll.year in recent_years\nselect\n enroll.year,\n @vs_prior_year(count(enroll.id)) as enrollment_growth,\nhaving\n enrollment_growth is not null\norder by enroll.year asc nulls first;\n"
1743
+ }
1618
1744
  ];
1619
1745
  //#endregion
1620
1746
  //#region dashboards/prompts.ts
@@ -1688,25 +1814,25 @@ function ft(e) {
1688
1814
  }
1689
1815
  var pt = `You are a world-class assistant for generating queries in Trilogy, a SQL inspired language with similar syntax, Use the the syntax description below and field information to answer the user request. Return your answer with a short reasoning and a valid Trilogy query in triple double quotes.
1690
1816
 
1691
- ${k}
1817
+ ${A}
1692
1818
 
1693
1819
  For any response to the user, use this format -> put your actual response within triple double quotes with thinking and justification before it, in this format (replace placeholders with relevant content): Reasoning: {{reasoning}} """{{ response }}"""
1694
1820
  `;
1695
- function A(e) {
1821
+ function j(e) {
1696
1822
  return ft(e).map((e) => `[name: ${e.name} | type:${e.type}${e.calculation ? " | alias_for:" + e.calculation : ""}${e.description ? " | description:" + e.description : ""}]`).join(", ");
1697
1823
  }
1698
1824
  function mt(e, t) {
1699
- return `${pt}. Using these base and aliased calculations, derivations thereof created with valid Trilogy, and any extra context you have: ${A(t)}, create the best valid Trilogy query to answer the following user input: "${e}" Return the query within triple double quotes with your thinking and justification before it, so of this form as a jinja template: Reasoning: {{reasoning_placeholder}} """{{ trilogy }}""". Example: Because the user asked for sales by year, and revenue is the best sales related field available, we can aggregate revenue by year: """SELECT order.year, sum(revenue) as year_revenue order by order.year asc;"""`;
1825
+ return `${pt}. Using these base and aliased calculations, derivations thereof created with valid Trilogy, and any extra context you have: ${j(t)}, create the best valid Trilogy query to answer the following user input: "${e}" Return the query within triple double quotes with your thinking and justification before it, so of this form as a jinja template: Reasoning: {{reasoning_placeholder}} """{{ trilogy }}""". Example: Because the user asked for sales by year, and revenue is the best sales related field available, we can aggregate revenue by year: """SELECT order.year, sum(revenue) as year_revenue order by order.year asc;"""`;
1700
1826
  }
1701
1827
  function ht(e, t) {
1702
- return `${pt}. Using these base and aliased calculations, derivations thereof created with valid Trilogy, and any extra context you have: ${A(t)}, create the closest matching syntactically valid WHERE CLAUSE to filter a dashboard from this request """${e}""". Return only the portion of a SQL query representing the where clause, excluding any ordering. Do not include comments. After you generate the code, review it carefully to make sure it is only the partial where clause of a select, starting with WHERE (imagine if you put a "select 1 ..." in front of it.), with no semicolon at the end. Return your generated code within triple double quotes with thinking and justification before it in the form (as a jinja example) Reasoning: {{reasoning}} """{{ trilogy }}""". Example: Reasoning: Because the user asked for rainbow colors, we can filter to where the color is a primary color """where color in ('red', 'blue', 'violet')"""`;
1828
+ return `${pt}. Using these base and aliased calculations, derivations thereof created with valid Trilogy, and any extra context you have: ${j(t)}, create the closest matching syntactically valid WHERE CLAUSE to filter a dashboard from this request """${e}""". Return only the portion of a SQL query representing the where clause, excluding any ordering. Do not include comments. After you generate the code, review it carefully to make sure it is only the partial where clause of a select, starting with WHERE (imagine if you put a "select 1 ..." in front of it.), with no semicolon at the end. Return your generated code within triple double quotes with thinking and justification before it in the form (as a jinja example) Reasoning: {{reasoning}} """{{ trilogy }}""". Example: Reasoning: Because the user asked for rainbow colors, we can filter to where the color is a primary color """where color in ('red', 'blue', 'violet')"""`;
1703
1829
  }
1704
1830
  function gt(e, t) {
1705
- return ct(e, A(t));
1831
+ return ct(e, j(t));
1706
1832
  }
1707
1833
  //#endregion
1708
1834
  //#region llm/sharedToolSchemas.ts
1709
- var _t = [
1835
+ var M = [
1710
1836
  "line",
1711
1837
  "bar",
1712
1838
  "barh",
@@ -1720,7 +1846,7 @@ var _t = [
1720
1846
  "boxplot",
1721
1847
  "treemap",
1722
1848
  "beeswarm"
1723
- ], vt = {
1849
+ ], _t = {
1724
1850
  chartType: "bar",
1725
1851
  xField: "category",
1726
1852
  yField: "revenue",
@@ -1737,13 +1863,13 @@ var _t = [
1737
1863
  scaleX: "linear",
1738
1864
  scaleY: "linear",
1739
1865
  linkY2: !1
1740
- }, yt = {
1866
+ }, vt = {
1741
1867
  type: "object",
1742
1868
  description: "One layer of a layered chart, drawn over the same result set.",
1743
1869
  properties: {
1744
1870
  chartType: {
1745
1871
  type: "string",
1746
- enum: _t,
1872
+ enum: M,
1747
1873
  description: "Type of chart to render"
1748
1874
  },
1749
1875
  xField: {
@@ -1776,7 +1902,7 @@ var _t = [
1776
1902
  description: "Which side to draw this layer value axis on (optional)"
1777
1903
  }
1778
1904
  }
1779
- }, bt = {
1905
+ }, yt = {
1780
1906
  chartType: "bar",
1781
1907
  showTitle: !0,
1782
1908
  layers: [{
@@ -1793,13 +1919,13 @@ var _t = [
1793
1919
  value: 1e3,
1794
1920
  label: "target"
1795
1921
  }]
1796
- }, j = {
1922
+ }, N = {
1797
1923
  type: "object",
1798
1924
  description: "Chart configuration object",
1799
1925
  properties: {
1800
1926
  chartType: {
1801
1927
  type: "string",
1802
- enum: _t,
1928
+ enum: M,
1803
1929
  description: "Type of chart to render"
1804
1930
  },
1805
1931
  xField: {
@@ -1874,7 +2000,7 @@ var _t = [
1874
2000
  },
1875
2001
  layers: {
1876
2002
  type: "array",
1877
- items: yt,
2003
+ items: vt,
1878
2004
  description: "Draw several marks over one result set, e.g. bars for a total with a line for an average. When set, the chart type and field bindings above are ignored and each layer supplies its own; the settings above (title, legend, scales) still apply to the chart as a whole. Only line, bar, barh, area, point, boxplot, donut and heatmap can be layers. Cannot be combined with trellisField/trellisRowField. Leave unset for an ordinary single-mark chart."
1879
2005
  },
1880
2006
  placements: {
@@ -1904,7 +2030,7 @@ var _t = [
1904
2030
  description: "Force layers to share one y-axis (true) or get independent ones (false). Omit to let the chart decide from the layers value formats (optional)"
1905
2031
  }
1906
2032
  }
1907
- }, xt = _t.map((e) => `'${e}'`).join(", "), M = `Chart type is auto-detected based on data shape unless you specify a chartConfig. Only provide chartConfig if the user specifically requests a chart type or configuration; otherwise let auto-detection handle it.
2033
+ }, bt = M.map((e) => `'${e}'`).join(", "), P = `Chart type is auto-detected based on data shape unless you specify a chartConfig. Only provide chartConfig if the user specifically requests a chart type or configuration; otherwise let auto-detection handle it.
1908
2034
 
1909
2035
  [Important!] Hiding fields used only for filtering/query structure from output - such as ranks - with the "--" trilogy select syntax may be useful to get the right auto-formatting.
1910
2036
 
@@ -1913,12 +2039,12 @@ You DO NOT need to specify colorField as the hexField if it is included; it will
1913
2039
  You can also add a default available Trilogy import "import std.color;" at the top of a query and cast a string case statement of hex values to ::string::hex to create mappings yourself.
1914
2040
  Do this if the user asks for specific coloring.
1915
2041
 
1916
- Example chartConfig: ${JSON.stringify(vt)}
2042
+ Example chartConfig: ${JSON.stringify(_t)}
1917
2043
 
1918
2044
  To draw more than one mark over the same results — bars for a total with a line for an average, say — set 'layers' instead of the top-level chart type and fields. Each layer names its own chartType and fields; title, legend and scale settings stay on the parent. Add 'placements' for constant reference lines.
1919
- Example layered chartConfig: ${JSON.stringify(bt)}
2045
+ Example layered chartConfig: ${JSON.stringify(yt)}
1920
2046
 
1921
- Available chartTypes: ${xt}`, N = {
2047
+ Available chartTypes: ${bt}`, F = {
1922
2048
  name: "connect_data_connection",
1923
2049
  description: "Connect or reconnect a data connection that is not currently active. Use this when a query fails because the connection is not active, or when you need to establish a connection before running queries.",
1924
2050
  input_schema: {
@@ -1932,16 +2058,16 @@ Available chartTypes: ${xt}`, N = {
1932
2058
  };
1933
2059
  //#endregion
1934
2060
  //#region llm/chatAgentPrompt.ts
1935
- function St(e) {
2061
+ function xt(e) {
1936
2062
  return e({
1937
- trilogySyntaxReference: k,
1938
- rulesInput: nt,
1939
- aggFunctions: rt,
1940
- functions: it,
1941
- datatypes: at
2063
+ trilogySyntaxReference: A,
2064
+ rulesInput: tt,
2065
+ aggFunctions: nt,
2066
+ functions: rt,
2067
+ datatypes: it
1942
2068
  });
1943
2069
  }
1944
- var Ct = {
2070
+ var I = {
1945
2071
  name: "return_to_user",
1946
2072
  description: "Signal that you are done and return control to the user. You MUST call this tool when you have completed all requested tasks and are ready for the user's next input. Provide a brief summary of what was accomplished. Never end a turn with plain text — always call this tool when finished.",
1947
2073
  input_schema: {
@@ -1952,7 +2078,7 @@ var Ct = {
1952
2078
  } },
1953
2079
  required: ["message"]
1954
2080
  }
1955
- }, wt = [
2081
+ }, St = [
1956
2082
  {
1957
2083
  name: "run_trilogy_query",
1958
2084
  description: "Execute a Trilogy query and return tabular results. Use this for data exploration, aggregations, and when the user wants to see raw data in a table format.",
@@ -1973,7 +2099,7 @@ var Ct = {
1973
2099
  },
1974
2100
  {
1975
2101
  name: "chart_trilogy_query",
1976
- description: `Execute a Trilogy query and display results as a chart. ${M}`,
2102
+ description: `Execute a Trilogy query and display results as a chart. ${P}`,
1977
2103
  input_schema: {
1978
2104
  type: "object",
1979
2105
  properties: {
@@ -1986,7 +2112,7 @@ var Ct = {
1986
2112
  description: "Data connection name to run the query against"
1987
2113
  },
1988
2114
  chartConfig: {
1989
- ...j,
2115
+ ...N,
1990
2116
  description: "Optional chart configuration. Only provide if user specifically requests a chart type or configuration."
1991
2117
  }
1992
2118
  },
@@ -2014,7 +2140,7 @@ var Ct = {
2014
2140
  required: []
2015
2141
  }
2016
2142
  },
2017
- N,
2143
+ F,
2018
2144
  {
2019
2145
  name: "create_markdown",
2020
2146
  description: "Create a rich markdown artifact to display formatted text, summaries, reports, or data-driven content. Supports standard markdown syntax (headers, lists, bold, italic, links, tables, code blocks). When a query is provided, data from the query results can be referenced in the markdown template. Use this for narrative reports, executive summaries, annotated insights, or any rich text content.",
@@ -2107,7 +2233,7 @@ var Ct = {
2107
2233
  description: "New title for the artifact"
2108
2234
  },
2109
2235
  chartConfig: {
2110
- ...j,
2236
+ ...N,
2111
2237
  description: "New chart configuration (for chart/results artifacts)"
2112
2238
  }
2113
2239
  },
@@ -2140,28 +2266,28 @@ var Ct = {
2140
2266
  required: ["artifact_ids"]
2141
2267
  }
2142
2268
  },
2143
- Ct
2269
+ I
2144
2270
  ];
2145
- function Tt(e, t) {
2271
+ function Ct(e, t) {
2146
2272
  if (!t || t.length === 0) return e;
2147
2273
  let n = new Set(t);
2148
2274
  return e.filter((e) => !n.has(e.name));
2149
2275
  }
2150
- function Et(e, t) {
2276
+ function wt(e, t) {
2151
2277
  if (!t || t.length === 0) return e;
2152
2278
  let n = new Set(e.map((e) => e.name)), r = /* @__PURE__ */ new Set(), i = t.map((e) => {
2153
2279
  let t = e.definition.name;
2154
2280
  if (n.has(t)) throw Error(`Host tool '${t}' shadows a built-in chat tool`);
2155
2281
  if (r.has(t)) throw Error(`Host tool '${t}' is declared twice`);
2156
2282
  return r.add(t), e.definition;
2157
- }), a = e.findIndex((e) => e.name === Ct.name);
2283
+ }), a = e.findIndex((e) => e.name === I.name);
2158
2284
  return a === -1 ? [...e, ...i] : [
2159
2285
  ...e.slice(0, a),
2160
2286
  ...i,
2161
2287
  ...e.slice(a)
2162
2288
  ];
2163
2289
  }
2164
- var Dt = [
2290
+ var Tt = [
2165
2291
  { text: "- Every query and chart tool call returns an artifact ID. Use these IDs to reference, update, or remove artifacts later." },
2166
2292
  {
2167
2293
  tool: "create_markdown",
@@ -2187,13 +2313,13 @@ var Dt = [
2187
2313
  tool: "create_markdown",
2188
2314
  text: "- When the user asks for a summary, report, or narrative, prefer create_markdown over just text responses - it renders in the artifacts panel."
2189
2315
  }
2190
- ], Ot = [{
2316
+ ], Et = [{
2191
2317
  tool: "search_docs",
2192
2318
  text: "- When you are unsure how to express something in Trilogy — window functions, date handling, filtering on aggregates, an unfamiliar keyword — call search_docs (kind: language or function) and then read_doc, rather than guessing at syntax. The reference above is a summary; the docs carry the full idioms."
2193
2319
  }, {
2194
2320
  tool: "read_doc",
2195
2321
  text: "- read_doc returns the full article for an id from search_docs; quote the relevant part back to the user when they ask how the language works."
2196
- }], kt = [
2322
+ }], Dt = [
2197
2323
  {
2198
2324
  tool: "list_artifacts",
2199
2325
  text: "Call list_artifacts to see everything currently in the panel."
@@ -2212,21 +2338,21 @@ var Dt = [
2212
2338
  },
2213
2339
  { text: "The artifact panel should tell a coherent story that directly answers the user's latest request — not accumulate a growing pile from every prior turn." }
2214
2340
  ];
2215
- function P(e, t) {
2341
+ function L(e, t) {
2216
2342
  return e.filter((e) => !e.tool || !t.includes(e.tool));
2217
2343
  }
2218
- function At(e) {
2219
- let { dataConnectionName: t, availableConnections: n, availableConcepts: r, activeImports: i = [], availableImportsForConnection: a = [], isDataConnectionActive: o = !0, disabledTools: s = [] } = e, c = P(Dt, s).map((e) => e.text).join("\n"), l = P(kt, s).map((e, t) => `${t + 1}. ${e.text}`).join("\n"), u = P(Ot, s), d = u.length > 0 ? `\n\nDOCUMENTATION:\n${u.map((e) => e.text).join("\n")}` : "", f = r && r.length > 0 ? `\n\nAVAILABLE FIELDS FOR QUERIES:\n${A(r)}` : "", p = t ? `ACTIVE DATA CONNECTION: ${t}${t && !o ? " (NOT CONNECTED - use connect_data_connection tool to connect before running queries)" : ""}` : "No data connection currently selected. Ask the user which connection to use.", m = i.length > 0 ? `\nACTIVE DATA SOURCE: ${i[0].name}` : "\nNo data source currently selected. Use select_active_import to select a data source, or use list_available_imports to see what is available.", h = a.length > 0 ? `\n\nAVAILABLE DATA SOURCES:\n${a.map((e) => `- ${e.name}${i.some((t) => t.id === e.id) ? " (active)" : ""}`).join("\n")}` : "";
2344
+ function Ot(e) {
2345
+ let { dataConnectionName: t, availableConnections: n, availableConcepts: r, activeImports: i = [], availableImportsForConnection: a = [], isDataConnectionActive: o = !0, disabledTools: s = [] } = e, c = L(Tt, s).map((e) => e.text).join("\n"), l = L(Dt, s).map((e, t) => `${t + 1}. ${e.text}`).join("\n"), u = L(Et, s), d = u.length > 0 ? `\n\nDOCUMENTATION:\n${u.map((e) => e.text).join("\n")}` : "", f = r && r.length > 0 ? `\n\nAVAILABLE FIELDS FOR QUERIES:\n${j(r)}` : "", p = !s.includes("connect_data_connection"), m = t && !o ? p ? " (NOT CONNECTED - use connect_data_connection tool to connect before running queries)" : " (NOT CONNECTED - queries will fail; tell the user the data connection is not available and call return_to_user)" : "", h = p ? "\n8. If the data connection is not active, use connect_data_connection to establish the connection before running queries" : "", g = t ? `ACTIVE DATA CONNECTION: ${t}${m}` : "No data connection currently selected. Ask the user which connection to use.", _ = i.length > 0 ? `\nACTIVE DATA SOURCE: ${i[0].name}` : "\nNo data source currently selected. Use select_active_import to select a data source, or use list_available_imports to see what is available.", v = a.length > 0 ? `\n\nAVAILABLE DATA SOURCES:\n${a.map((e) => `- ${e.name}${i.some((t) => t.id === e.id) ? " (active)" : ""}`).join("\n")}` : "";
2220
2346
  return `You are a data analysis assistant with access to Trilogy query capabilities.
2221
2347
 
2222
2348
  You have access to tools that can select datasources, execute queries and generate visualizations. When a user asks about data, analyze their request, make sure you are looking at the right source, write a valid Trilogy query, and use the appropriate tool.
2223
2349
 
2224
- ${p}
2350
+ ${g}
2225
2351
  AVAILABLE DATA CONNECTIONS: ${n.length > 0 ? n.join(", ") : "None configured - user needs to set up a data connection first"}
2226
- ${m}${h}
2352
+ ${_}${v}
2227
2353
 
2228
2354
  TRILOGY LANGUAGE REFERENCE:
2229
- ${k}
2355
+ ${A}
2230
2356
  ${f}
2231
2357
 
2232
2358
  IMPORTANT GUIDELINES:
@@ -2236,8 +2362,7 @@ IMPORTANT GUIDELINES:
2236
2362
  4. When showing data, prefer tables for detailed exploration and charts for trends/comparisons
2237
2363
  5. Use the full field path (e.g., 'order.product.id') - never use FROM clauses
2238
2364
  6. Remember: No GROUP BY clause - grouping is implicit by non-aggregated fields in SELECT
2239
- 7. If the user question needs fields that are not in the same source, use select_active_import to switch to a different data source (only one can be active at a time). Always consider this when they change topics.
2240
- 8. If the data connection is not active, use connect_data_connection to establish the connection before running queries${d}
2365
+ 7. If the user question needs fields that are not in the same source, use select_active_import to switch to a different data source (only one can be active at a time). Always consider this when they change topics.${h}${d}
2241
2366
 
2242
2367
  ARTIFACT MANAGEMENT:
2243
2368
  ${c}
@@ -2254,9 +2379,9 @@ COMPLETING YOUR RESPONSE:
2254
2379
  }
2255
2380
  //#endregion
2256
2381
  //#region llm/subchatSummarize.ts
2257
- var jt = "You are summarizing a subchat that has just finished. Read the transcript below (messages and tool calls) and produce a 2–4 sentence summary of what was accomplished, what (if anything) failed or remains, and any concrete artifacts (file names, queries, errors). Be specific. The overseer will use this report to update the user — there is no other source of truth, so do not omit important results.", Mt = "You are summarizing a subchat's progress for an overseer agent. Read the transcript below (messages and tool calls) and produce a concise 2–4 sentence summary of what has happened so far, what has been accomplished, and what (if anything) remains. Be specific about file names, queries, or errors. Do not editorialize.";
2258
- async function Nt(e, t, n = "completion") {
2259
- let r = Pt(t.messages), i = n === "completion" ? jt : Mt, a = e.getFastModel(), o = e.model, s = !!a && a !== o;
2382
+ var kt = "You are summarizing a subchat that has just finished. Read the transcript below (messages and tool calls) and produce a 2–4 sentence summary of what was accomplished, what (if anything) failed or remains, and any concrete artifacts (file names, queries, errors). Be specific. The overseer will use this report to update the user — there is no other source of truth, so do not omit important results.", At = "You are summarizing a subchat's progress for an overseer agent. Read the transcript below (messages and tool calls) and produce a concise 2–4 sentence summary of what has happened so far, what has been accomplished, and what (if anything) remains. Be specific about file names, queries, or errors. Do not editorialize.";
2383
+ async function jt(e, t, n = "completion") {
2384
+ let r = Mt(t.messages), i = n === "completion" ? kt : At, a = e.getFastModel(), o = e.model, s = !!a && a !== o;
2260
2385
  try {
2261
2386
  return s && (e.model = a), (await e.generateCompletion({
2262
2387
  prompt: "",
@@ -2269,42 +2394,42 @@ async function Nt(e, t, n = "completion") {
2269
2394
  s && (e.model = o);
2270
2395
  }
2271
2396
  }
2272
- function Pt(e) {
2397
+ function Mt(e) {
2273
2398
  return e.map((e, t) => {
2274
- let n = [`[${t}] ${e.role}: ${Ft(e.content || "", 800)}`];
2275
- if (e.toolCalls && e.toolCalls.length > 0) for (let t of e.toolCalls) n.push(` → ${t.name}(${Ft(JSON.stringify(t.input), 200)})`);
2276
- if (e.toolResults && e.toolResults.length > 0) for (let t of e.toolResults) n.push(` ← ${t.toolName}: ${Ft(t.result, 300)}`);
2399
+ let n = [`[${t}] ${e.role}: ${Nt(e.content || "", 800)}`];
2400
+ if (e.toolCalls && e.toolCalls.length > 0) for (let t of e.toolCalls) n.push(` → ${t.name}(${Nt(JSON.stringify(t.input), 200)})`);
2401
+ if (e.toolResults && e.toolResults.length > 0) for (let t of e.toolResults) n.push(` ← ${t.toolName}: ${Nt(t.result, 300)}`);
2277
2402
  return n.join("\n");
2278
2403
  }).join("\n\n");
2279
2404
  }
2280
- function Ft(e, t) {
2405
+ function Nt(e, t) {
2281
2406
  return e.length <= t ? e : e.slice(0, t - 1) + "…";
2282
2407
  }
2283
- var It = 4, Lt = 8, Rt = 1600;
2284
- function F(e) {
2408
+ var Pt = 4, Ft = 8, It = 1600;
2409
+ function R(e) {
2285
2410
  return e ? Math.ceil(e.length / 4) : 0;
2286
2411
  }
2287
- function zt(e) {
2288
- let t = It + F(e.content);
2412
+ function Lt(e) {
2413
+ let t = Pt + R(e.content);
2289
2414
  for (let n of e.toolCalls ?? []) {
2290
- t += Lt + F(n.name);
2415
+ t += Ft + R(n.name);
2291
2416
  try {
2292
- t += F(JSON.stringify(n.input));
2417
+ t += R(JSON.stringify(n.input));
2293
2418
  } catch {}
2294
2419
  }
2295
- for (let n of e.toolResults ?? []) t += Lt + F(n.toolName), t += F(n.result), n.imageData && (t += Rt);
2420
+ for (let n of e.toolResults ?? []) t += Ft + R(n.toolName), t += R(n.result), n.imageData && (t += It);
2296
2421
  return t;
2297
2422
  }
2298
- function Bt(e) {
2299
- return e.reduce((e, t) => e + zt(t), 0);
2423
+ function Rt(e) {
2424
+ return e.reduce((e, t) => e + Lt(t), 0);
2300
2425
  }
2301
2426
  //#endregion
2302
2427
  //#region llm/toolLoopCore.ts
2303
- var Vt = "<user_input>", Ht = "</user_input>", I = "<system_input>", L = "</system_input>";
2304
- function R(e) {
2428
+ var zt = "<user_input>", Bt = "</user_input>", z = "<system_input>", B = "</system_input>";
2429
+ function Vt(e) {
2305
2430
  return e.replace(/<\/?(?:user_input|system_input)>/gi, "");
2306
2431
  }
2307
- function Ut(e) {
2432
+ function Ht(e) {
2308
2433
  let t = e?.data;
2309
2434
  return !Array.isArray(t) || t.length <= 100 ? {
2310
2435
  head: t ?? [],
@@ -2318,11 +2443,11 @@ function Ut(e) {
2318
2443
  cutCount: t.length - 100
2319
2444
  };
2320
2445
  }
2321
- function Wt(e) {
2446
+ function Ut(e) {
2322
2447
  if (e.success) if (e.artifact) {
2323
2448
  let t = e.artifact.config, n = e.artifact.data, r = e.artifact.id, i = "";
2324
2449
  if (n) {
2325
- let e = typeof n.toJSON == "function" ? n.toJSON() : n, a = t?.resultSize ?? (Array.isArray(e?.data) ? e.data.length : 0), { head: o, tail: s, cutCount: c } = Ut(e);
2450
+ let e = typeof n.toJSON == "function" ? n.toJSON() : n, a = t?.resultSize ?? (Array.isArray(e?.data) ? e.data.length : 0), { head: o, tail: s, cutCount: c } = Ht(e);
2326
2451
  if (c > 0) {
2327
2452
  let t = JSON.stringify({
2328
2453
  ...e,
@@ -2340,7 +2465,7 @@ function Wt(e) {
2340
2465
  else return "Success.";
2341
2466
  else return `Error: ${e.error}`;
2342
2467
  }
2343
- function Gt(e) {
2468
+ function Wt(e) {
2344
2469
  if (e instanceof Error) return e.message;
2345
2470
  if (typeof e == "string") return e;
2346
2471
  try {
@@ -2349,18 +2474,18 @@ function Gt(e) {
2349
2474
  return String(e);
2350
2475
  }
2351
2476
  }
2352
- async function Kt(e, t, n, r, i, a, o) {
2477
+ async function Gt(e, t, n, r, i, a, o) {
2353
2478
  let s = o.maxIterations ?? 20, c = r.getMessages().slice(0, -1).map((e) => e.role === "user" && !e.hidden && e.content ? {
2354
2479
  ...e,
2355
- content: `${Vt}${R(e.content)}${Ht}`
2356
- } : e), l = `${Vt}${R(e)}${Ht}`, u = l, d = !1, f = 0;
2480
+ content: `${zt}${Vt(e.content)}${Bt}`
2481
+ } : e), l = `${zt}${Vt(e)}${Bt}`, u = l, d = !1, f = 0;
2357
2482
  for (let e = 0; e < s; e++) {
2358
2483
  if (a.checkAborted()) return e === 0 && r.addMessage({
2359
2484
  role: "assistant",
2360
2485
  content: "(Response in progress when stopped)"
2361
2486
  }), r.addMessage({
2362
2487
  role: "user",
2363
- content: `${I}[User requested pause - conversation can be continued]${L}`,
2488
+ content: `${z}[User requested pause - conversation can be continued]${B}`,
2364
2489
  hidden: !0
2365
2490
  }), {
2366
2491
  terminated: !1,
@@ -2373,7 +2498,7 @@ async function Kt(e, t, n, r, i, a, o) {
2373
2498
  content: "(Response in progress when stopped)"
2374
2499
  }), r.addMessage({
2375
2500
  role: "user",
2376
- content: `${I}[User requested pause - conversation can be continued]${L}`,
2501
+ content: `${z}[User requested pause - conversation can be continued]${B}`,
2377
2502
  hidden: !0
2378
2503
  }), {
2379
2504
  terminated: !1,
@@ -2390,7 +2515,7 @@ async function Kt(e, t, n, r, i, a, o) {
2390
2515
  content: g
2391
2516
  }), r.addMessage({
2392
2517
  role: "user",
2393
- content: `${I}[User requested pause - conversation can be continued]${L}`,
2518
+ content: `${z}[User requested pause - conversation can be continued]${B}`,
2394
2519
  hidden: !0
2395
2520
  }), {
2396
2521
  terminated: !1,
@@ -2411,7 +2536,7 @@ async function Kt(e, t, n, r, i, a, o) {
2411
2536
  };
2412
2537
  let e = o.noToolCallReminder ?? "You must call a tool to proceed. If you are finished, call the appropriate completion tool to return control to the user. Do not respond with text only.";
2413
2538
  console.log("[toolLoopCore] No tool call — re-prompting agent to use a tool");
2414
- let t = `${I}${e}${L}`;
2539
+ let t = `${z}${e}${B}`;
2415
2540
  r.addMessage({
2416
2541
  role: "user",
2417
2542
  content: t,
@@ -2449,7 +2574,7 @@ async function Kt(e, t, n, r, i, a, o) {
2449
2574
  hidden: !0
2450
2575
  }), r.addMessage({
2451
2576
  role: "user",
2452
- content: `${I}[User requested pause - conversation can be continued]${L}`,
2577
+ content: `${z}[User requested pause - conversation can be continued]${B}`,
2453
2578
  hidden: !0
2454
2579
  }), {
2455
2580
  terminated: !1,
@@ -2463,7 +2588,7 @@ async function Kt(e, t, n, r, i, a, o) {
2463
2588
  } catch (n) {
2464
2589
  t = {
2465
2590
  success: !1,
2466
- error: `Tool "${e.name}" failed: ${Gt(n)}`
2591
+ error: `Tool "${e.name}" failed: ${Wt(n)}`
2467
2592
  };
2468
2593
  }
2469
2594
  let n = {
@@ -2520,7 +2645,7 @@ async function Kt(e, t, n, r, i, a, o) {
2520
2645
  finalMessage: t.message
2521
2646
  };
2522
2647
  }
2523
- let i = Wt(t);
2648
+ let i = Ut(t);
2524
2649
  v.push({
2525
2650
  toolCallId: e.id,
2526
2651
  toolName: e.name,
@@ -2567,26 +2692,26 @@ async function Kt(e, t, n, r, i, a, o) {
2567
2692
  }
2568
2693
  //#endregion
2569
2694
  //#region llm/compaction.ts
2570
- var qt = 2e4, Jt = 6, Yt = 20, Xt = 6, Zt = "You are a context summarization assistant. You are given the earlier portion of an assistant conversation that is about to fall out of the context window. Produce a structured summary that the assistant will rely on INSTEAD of those messages.\n\nUse exactly these sections:\n## Goal — what the user is ultimately trying to accomplish.\n## Progress — what was built, changed, or discovered, with exact names and ids (editors, dashboards, jobs, connections, queries, fields).\n## Key decisions — choices made and why, including approaches tried and rejected.\n## Current state — where the work stands right now.\n## Open items — unresolved questions, known failures, and obvious next steps.\n\nBe concrete: prefer exact identifiers, query text and error messages over paraphrase. Aim for 300-600 words. Omit nothing the assistant would need to continue seamlessly. Do not editorialize and do not add recommendations of your own.";
2571
- function Qt(e) {
2695
+ var Kt = 2e4, qt = 6, Jt = 20, Yt = 6, Xt = "You are a context summarization assistant. You are given the earlier portion of an assistant conversation that is about to fall out of the context window. Produce a structured summary that the assistant will rely on INSTEAD of those messages.\n\nUse exactly these sections:\n## Goal — what the user is ultimately trying to accomplish.\n## Progress — what was built, changed, or discovered, with exact names and ids (editors, dashboards, jobs, connections, queries, fields).\n## Key decisions — choices made and why, including approaches tried and rejected.\n## Current state — where the work stands right now.\n## Open items — unresolved questions, known failures, and obvious next steps.\n\nBe concrete: prefer exact identifiers, query text and error messages over paraphrase. Aim for 300-600 words. Omit nothing the assistant would need to continue seamlessly. Do not editorialize and do not add recommendations of your own.";
2696
+ function Zt(e) {
2572
2697
  return e.role === "user" && !(e.toolResults && e.toolResults.length > 0);
2573
2698
  }
2574
- function $t(e, t) {
2575
- let n = t.keepRecentTokens ?? 2e4, r = t.minKeepRecentCount ?? Jt, i = t.maxKeepRecentCount ?? Yt, a = t.minArchiveCount ?? Xt, o = 0, s = 0;
2576
- for (let t = e.length - 1; t >= 0 && !(o >= i || o >= r && s >= n); t--) s += zt(e[t]), o++;
2699
+ function Qt(e, t) {
2700
+ let n = t.keepRecentTokens ?? 2e4, r = t.minKeepRecentCount ?? qt, i = t.maxKeepRecentCount ?? Jt, a = t.minArchiveCount ?? Yt, o = 0, s = 0;
2701
+ for (let t = e.length - 1; t >= 0 && !(o >= i || o >= r && s >= n); t--) s += Lt(e[t]), o++;
2577
2702
  let c = e.length - o;
2578
- for (; c > 0 && !Qt(e[c]);) c--;
2703
+ for (; c > 0 && !Zt(e[c]);) c--;
2579
2704
  return c < a ? -1 : c;
2580
2705
  }
2581
- function en(e, t, n) {
2582
- return !t || t <= 0 ? !1 : (n ?? Bt(e.getLLMMessages())) > t;
2706
+ function $t(e, t, n) {
2707
+ return !t || t <= 0 ? !1 : (n ?? Rt(e.getLLMMessages())) > t;
2583
2708
  }
2584
- async function tn(e, t, n) {
2585
- let r = Pt(t), i = e.getFastModel(), a = e.model, o = !!i && i !== a;
2709
+ async function en(e, t, n) {
2710
+ let r = Mt(t), i = e.getFastModel(), a = e.model, o = !!i && i !== a;
2586
2711
  try {
2587
2712
  return o && (e.model = i), (await e.generateCompletion({
2588
2713
  prompt: "",
2589
- systemPrompt: n ? `${Zt}\n\nAdditional emphasis requested: ${n}` : Zt
2714
+ systemPrompt: n ? `${Xt}\n\nAdditional emphasis requested: ${n}` : Xt
2590
2715
  }, [{
2591
2716
  role: "user",
2592
2717
  content: `--- TRANSCRIPT ---\n${r}`
@@ -2595,22 +2720,22 @@ async function tn(e, t, n) {
2595
2720
  o && (e.model = a);
2596
2721
  }
2597
2722
  }
2598
- function nn(e) {
2723
+ function tn(e) {
2599
2724
  return {
2600
2725
  role: "user",
2601
2726
  hidden: !0,
2602
2727
  content: e
2603
2728
  };
2604
2729
  }
2605
- async function rn(e, t, n = {}) {
2606
- let r = t.getLLMMessages(), i = $t(r, n);
2730
+ async function nn(e, t, n = {}) {
2731
+ let r = t.getLLMMessages(), i = Qt(r, n);
2607
2732
  if (i < 0) return null;
2608
- let a = r.slice(0, i), o = await tn(e, a, n.focus), s = t.messages.indexOf(r[i]);
2733
+ let a = r.slice(0, i), o = await en(e, a, n.focus), s = t.messages.indexOf(r[i]);
2609
2734
  if (s < 0) return null;
2610
2735
  a.forEach((e) => {
2611
2736
  e.archived = !0;
2612
2737
  });
2613
- let c = n.createSummaryMessage ?? nn, l = `${I}[Conversation compacted. Summary of the earlier conversation:]\n\n${o}${n.appendix ?? ""}${L}`;
2738
+ let c = n.createSummaryMessage ?? tn, l = `${z}[Conversation compacted. Summary of the earlier conversation:]\n\n${o}${n.appendix ?? ""}${B}`;
2614
2739
  t.messages.splice(s, 0, c(l));
2615
2740
  let u = {
2616
2741
  archivedCount: a.length,
@@ -2618,17 +2743,17 @@ async function rn(e, t, n = {}) {
2618
2743
  };
2619
2744
  return n.onCompacted?.(u), u;
2620
2745
  }
2621
- function an(e) {
2746
+ function rn(e) {
2622
2747
  let t = e.artifacts.filter((e) => !e.hidden);
2623
2748
  return t.length === 0 ? "" : `\n\nArtifacts still available via get_artifact/get_artifact_rows:\n${t.map((e) => {
2624
2749
  let t = e.config?.title || e.config?.query?.slice(0, 60) || "", n = e.config?.resultSize == null ? "" : `, ${e.config.resultSize} rows`;
2625
2750
  return `- [${e.id}] ${e.type}${t ? ` "${t}"` : ""}${n}`;
2626
2751
  }).join("\n")}`;
2627
2752
  }
2628
- async function on(e, t, n = {}) {
2629
- return rn(e, t, {
2753
+ async function an(e, t, n = {}) {
2754
+ return nn(e, t, {
2630
2755
  ...n,
2631
- appendix: n.appendix ?? an(t),
2756
+ appendix: n.appendix ?? rn(t),
2632
2757
  createSummaryMessage: n.createSummaryMessage ?? ((e) => ({
2633
2758
  role: "user",
2634
2759
  hidden: !0,
@@ -2640,10 +2765,10 @@ async function on(e, t, n = {}) {
2640
2765
  }
2641
2766
  });
2642
2767
  }
2643
- async function sn(e, t, n = {}) {
2644
- if (!en(t, e.getCompactionThresholdTokens(), t.lastContextTokens)) return null;
2768
+ async function on(e, t, n = {}) {
2769
+ if (!$t(t, e.getCompactionThresholdTokens(), t.lastContextTokens)) return null;
2645
2770
  try {
2646
- let r = await on(e, t, n);
2771
+ let r = await an(e, t, n);
2647
2772
  return r && (t.lastContextTokens = void 0), r;
2648
2773
  } catch (e) {
2649
2774
  return console.error("Automatic conversation compaction failed:", e), null;
@@ -2651,7 +2776,7 @@ async function sn(e, t, n = {}) {
2651
2776
  }
2652
2777
  //#endregion
2653
2778
  //#region llm/editorRefinementTools.ts
2654
- function z(e) {
2779
+ function sn(e) {
2655
2780
  return e.filter((e) => e.trilogyType === "concept").map((e) => ({
2656
2781
  name: e.label,
2657
2782
  type: e.datatype || e.type,
@@ -2661,7 +2786,7 @@ function z(e) {
2661
2786
  }));
2662
2787
  }
2663
2788
  function cn(e) {
2664
- return A(z(e));
2789
+ return j(sn(e));
2665
2790
  }
2666
2791
  var ln = [
2667
2792
  {
@@ -2704,10 +2829,10 @@ var ln = [
2704
2829
  name: "edit_chart_config",
2705
2830
  description: `Update the chart configuration for visualizing query results. Use this when the user wants to change how results are displayed.
2706
2831
 
2707
- ${M}`,
2832
+ ${P}`,
2708
2833
  input_schema: {
2709
2834
  type: "object",
2710
- properties: { chartConfig: j },
2835
+ properties: { chartConfig: N },
2711
2836
  required: ["chartConfig"]
2712
2837
  }
2713
2838
  },
@@ -2785,7 +2910,7 @@ ${M}`,
2785
2910
  required: []
2786
2911
  }
2787
2912
  },
2788
- N
2913
+ F
2789
2914
  ], un = [
2790
2915
  {
2791
2916
  name: "browse_database_tree",
@@ -2899,7 +3024,7 @@ ${M}`,
2899
3024
  required: []
2900
3025
  }
2901
3026
  },
2902
- N
3027
+ F
2903
3028
  ], dn = ln;
2904
3029
  function fn(e) {
2905
3030
  return e === "sql" ? un : ln;
@@ -2913,7 +3038,7 @@ ${i}
2913
3038
  ` : "", c = a ? `
2914
3039
  CURRENT CHART CONFIG:
2915
3040
  ${JSON.stringify(a, null, 2)}
2916
- ` : "", l = z(o), u = l.length > 0 ? `
3041
+ ` : "", l = sn(o), u = l.length > 0 ? `
2917
3042
  AVAILABLE FIELDS FOR QUERIES (${l.length} concepts):
2918
3043
  ${cn(o)}
2919
3044
  ` : "";
@@ -2969,7 +3094,7 @@ AVAILABLE TOOLS:
2969
3094
  - close_session: Immediately close (only after user confirms or says "done")
2970
3095
  ${u}
2971
3096
  TRILOGY LANGUAGE REFERENCE:
2972
- ${k}
3097
+ ${A}
2973
3098
 
2974
3099
  WORKFLOW:
2975
3100
  1. Understand what the user wants to change
@@ -2991,13 +3116,13 @@ IMPORTANT:
2991
3116
  }
2992
3117
  //#endregion
2993
3118
  //#region chats/chat.ts
2994
- function B() {
3119
+ function V() {
2995
3120
  return `art-${Date.now().toString(36)}-${Math.random().toString(36).substr(2, 6)}`;
2996
3121
  }
2997
- function V() {
3122
+ function mn() {
2998
3123
  return `Chat ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`;
2999
3124
  }
3000
- var mn = class e {
3125
+ var hn = class e {
3001
3126
  id;
3002
3127
  name;
3003
3128
  dataConnectionName;
@@ -3021,7 +3146,7 @@ var mn = class e {
3021
3146
  changed;
3022
3147
  deleted;
3023
3148
  constructor(e = {}) {
3024
- this.id = e.id || `chat-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, this.name = e.name || V(), this.dataConnectionName = e.dataConnectionName || "", this.dataConnectionId = e.dataConnectionId || "", this.llmConnectionName = e.llmConnectionName || "", this.messages = e.messages || [], this.artifacts = e.artifacts || [], this.activeArtifactIndex = e.activeArtifactIndex ?? -1, this.imports = e.imports || [], this.createdAt = e.createdAt || /* @__PURE__ */ new Date(), this.updatedAt = e.updatedAt || /* @__PURE__ */ new Date(), this.storage = e.storage || "local", this.source = e.source || "user", this.sourceRefId = e.sourceRefId ?? null, this.kind = e.kind || "user", this.parentChatId = e.parentChatId ?? null, this.parentProjectId = e.parentProjectId ?? null, this.pendingInjections = e.pendingInjections || [], this.pendingContextNote = e.pendingContextNote ?? null, this.lastContextTokens = e.lastContextTokens ?? void 0, this.changed = e.changed ?? !0, this.deleted = e.deleted ?? !1;
3149
+ this.id = e.id || `chat-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, this.name = e.name || mn(), this.dataConnectionName = e.dataConnectionName || "", this.dataConnectionId = e.dataConnectionId || "", this.llmConnectionName = e.llmConnectionName || "", this.messages = e.messages || [], this.artifacts = e.artifacts || [], this.activeArtifactIndex = e.activeArtifactIndex ?? -1, this.imports = e.imports || [], this.createdAt = e.createdAt || /* @__PURE__ */ new Date(), this.updatedAt = e.updatedAt || /* @__PURE__ */ new Date(), this.storage = e.storage || "local", this.source = e.source || "user", this.sourceRefId = e.sourceRefId ?? null, this.kind = e.kind || "user", this.parentChatId = e.parentChatId ?? null, this.parentProjectId = e.parentProjectId ?? null, this.pendingInjections = e.pendingInjections || [], this.pendingContextNote = e.pendingContextNote ?? null, this.lastContextTokens = e.lastContextTokens ?? void 0, this.changed = e.changed ?? !0, this.deleted = e.deleted ?? !1;
3025
3150
  }
3026
3151
  setName(e) {
3027
3152
  this.name !== e && (this.name = e, this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0);
@@ -3036,7 +3161,7 @@ var mn = class e {
3036
3161
  this.messages.push(e), this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0;
3037
3162
  }
3038
3163
  addArtifact(e) {
3039
- return e.id ||= B(), this.artifacts.push(e), this.activeArtifactIndex = this.artifacts.length - 1, this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0, this.activeArtifactIndex;
3164
+ return e.id ||= V(), this.artifacts.push(e), this.activeArtifactIndex = this.artifacts.length - 1, this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0, this.activeArtifactIndex;
3040
3165
  }
3041
3166
  getArtifactById(e) {
3042
3167
  return this.artifacts.find((t) => t.id === e) || null;
@@ -3070,7 +3195,7 @@ var mn = class e {
3070
3195
  return this.activeArtifactIndex >= 0 && this.activeArtifactIndex < this.artifacts.length ? this.artifacts[this.activeArtifactIndex] : null;
3071
3196
  }
3072
3197
  clearMessages() {
3073
- this.messages = [], this.artifacts = [], this.activeArtifactIndex = -1, this.lastContextTokens = void 0, this.name = V(), this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0;
3198
+ this.messages = [], this.artifacts = [], this.activeArtifactIndex = -1, this.lastContextTokens = void 0, this.name = mn(), this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0;
3074
3199
  }
3075
3200
  addImport(e) {
3076
3201
  return this.imports.some((t) => t.id === e.id) ? !1 : (this.imports.push(e), this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0, !0);
@@ -3123,19 +3248,19 @@ var mn = class e {
3123
3248
  changed: !1
3124
3249
  });
3125
3250
  }
3126
- }, hn = [
3251
+ }, gn = [
3127
3252
  "line",
3128
3253
  "area",
3129
3254
  "bar",
3130
3255
  "barh",
3131
3256
  "point",
3132
3257
  "heatmap"
3133
- ], gn = [
3258
+ ], _n = [
3134
3259
  "geo-map",
3135
3260
  "tree",
3136
3261
  "donut",
3137
3262
  "headline"
3138
- ], _n = [
3263
+ ], vn = [
3139
3264
  "bar",
3140
3265
  "barh",
3141
3266
  "line",
@@ -3144,7 +3269,7 @@ var mn = class e {
3144
3269
  "boxplot",
3145
3270
  "donut",
3146
3271
  "heatmap"
3147
- ], vn = [
3272
+ ], yn = [
3148
3273
  {
3149
3274
  id: "group-by",
3150
3275
  label: "Group By",
@@ -3279,7 +3404,7 @@ var mn = class e {
3279
3404
  field: "trellisField",
3280
3405
  columnFilter: "categorical",
3281
3406
  allowEmpty: !0,
3282
- visibleFor: hn,
3407
+ visibleFor: gn,
3283
3408
  filterGroup: "appearance"
3284
3409
  },
3285
3410
  {
@@ -3288,7 +3413,7 @@ var mn = class e {
3288
3413
  field: "trellisRowField",
3289
3414
  columnFilter: "categorical",
3290
3415
  allowEmpty: !0,
3291
- visibleFor: hn,
3416
+ visibleFor: gn,
3292
3417
  filterGroup: "appearance"
3293
3418
  },
3294
3419
  {
@@ -3419,7 +3544,7 @@ var mn = class e {
3419
3544
  ],
3420
3545
  filterGroup: "appearance"
3421
3546
  }
3422
- ], yn = [
3547
+ ], bn = [
3423
3548
  {
3424
3549
  value: "bar",
3425
3550
  label: "Bar Chart",
@@ -3480,7 +3605,7 @@ var mn = class e {
3480
3605
  label: "Beeswarm",
3481
3606
  icon: "mdi mdi-chart-bubble"
3482
3607
  }
3483
- ], bn = "#1f77b4", xn = "#9467bd", Sn = "#FF7F7F", H = (e, t) => e[t], Cn = [
3608
+ ], xn = "#1f77b4", Sn = "#9467bd", Cn = "#FF7F7F", H = (e, t) => e[t], wn = [
3484
3609
  "year",
3485
3610
  "week",
3486
3611
  "day",
@@ -3488,7 +3613,7 @@ var mn = class e {
3488
3613
  "minute",
3489
3614
  "second",
3490
3615
  "quarter"
3491
- ], wn = [
3616
+ ], Tn = [
3492
3617
  "decade",
3493
3618
  "year",
3494
3619
  "month",
@@ -3498,50 +3623,50 @@ var mn = class e {
3498
3623
  "minute",
3499
3624
  "second",
3500
3625
  "day_of_week"
3501
- ], U = (e) => e.traits ? e.traits.some((e) => wn.some((t) => e === t || e.endsWith("_" + t))) : !1, Tn = (e, t) => {
3626
+ ], U = (e) => e.traits ? e.traits.some((e) => Tn.some((t) => e === t || e.endsWith("_" + t))) : !1, En = (e, t) => {
3502
3627
  let n = t.get(e);
3503
3628
  return n ? U(n) : !1;
3504
- }, En = [
3629
+ }, Dn = [
3505
3630
  "rank",
3506
3631
  "index",
3507
3632
  "grade",
3508
3633
  "flag",
3509
3634
  "letter_grade"
3510
- ], Dn = [
3511
- ...Cn,
3512
- ...En,
3635
+ ], On = [
3636
+ ...wn,
3637
+ ...Dn,
3513
3638
  "identifier",
3514
3639
  "decade"
3515
3640
  ];
3516
- function On(e) {
3641
+ function kn(e) {
3517
3642
  return new Date(e);
3518
3643
  }
3519
3644
  var W = (e, t, n) => {
3520
3645
  if (!e || !t.get(e)) return !1;
3521
3646
  let r = t.get(e);
3522
- return r ? kn(r, n) : !1;
3523
- }, kn = (e, t) => !!(e.traits && e.traits.some((e) => e.endsWith(t))), G = (e) => e.purpose === "key" || U(e) ? !1 : [
3647
+ return r ? An(r, n) : !1;
3648
+ }, An = (e, t) => !!(e.traits && e.traits.some((e) => e.endsWith(t))), G = (e) => e.purpose === "key" || U(e) ? !1 : [
3524
3649
  n.NUMBER,
3525
3650
  n.INTEGER,
3526
3651
  n.FLOAT
3527
- ].includes(e.type) && !e.traits?.some((e) => e.endsWith("latitude") || e.endsWith("longitude")), An = (e) => !!(e.traits && En.some((t) => e.traits?.includes(t))), jn = (e) => !!([n.STRING].includes(e.type) && e.traits?.some((e) => e.endsWith("url_image"))), K = (e) => !!([
3652
+ ].includes(e.type) && !e.traits?.some((e) => e.endsWith("latitude") || e.endsWith("longitude")), jn = (e) => !!(e.traits && Dn.some((t) => e.traits?.includes(t))), Mn = (e) => !!([n.STRING].includes(e.type) && e.traits?.some((e) => e.endsWith("url_image"))), K = (e) => !!([
3528
3653
  n.DATE,
3529
3654
  n.DATETIME,
3530
3655
  n.TIME,
3531
3656
  n.TIMESTAMP
3532
- ].includes(e.type) || e.traits && Cn.some((t) => e.traits?.includes(t))), Mn = (e) => !!([n.STRING, n.BOOLEAN].includes(e.type) || e.purpose === "key" || e.traits && Dn.some((t) => e.traits?.includes(t))), Nn = (e) => e.traits?.includes("hex") ?? !1, q = (e, t) => e.traits ? e.traits.some((e) => e.endsWith(t)) : !1, J = (e, t) => {
3657
+ ].includes(e.type) || e.traits && wn.some((t) => e.traits?.includes(t))), Nn = (e) => !!([n.STRING, n.BOOLEAN].includes(e.type) || e.purpose === "key" || e.traits && On.some((t) => e.traits?.includes(t))), Pn = (e) => e.traits?.includes("hex") ?? !1, q = (e, t) => e.traits ? e.traits.some((e) => e.endsWith(t)) : !1, J = (e, t) => {
3533
3658
  let n = [];
3534
3659
  return t.forEach((t, r) => {
3535
- (e === "all" || e === "numeric" && G(t) && !K(t) || e === "categorical" && Mn(t) || e === "temporal" && K(t) || e === "latitude" && q(t, "latitude") || e === "longitude" && q(t, "longitude") || e === "geographic" && t.traits?.some((e) => e.endsWith("state") || e.endsWith("state_short") || e.endsWith("country"))) && n.push(t);
3660
+ (e === "all" || e === "numeric" && G(t) && !K(t) || e === "categorical" && Nn(t) || e === "temporal" && K(t) || e === "latitude" && q(t, "latitude") || e === "longitude" && q(t, "longitude") || e === "geographic" && t.traits?.some((e) => e.endsWith("state") || e.endsWith("state_short") || e.endsWith("country"))) && n.push(t);
3536
3661
  }), n;
3537
- }, Pn = (e, t) => {
3662
+ }, Fn = (e, t) => {
3538
3663
  let n = t.filter((t) => t.name !== e.xField && t.name !== e.yField);
3539
3664
  return n.length > 0 ? n[0] : t[0];
3540
- }, Fn = (e, t, n) => {
3665
+ }, In = (e, t, n) => {
3541
3666
  let r = {
3542
3667
  showTitle: !1,
3543
3668
  hideLegend: !1
3544
- }, i = J("numeric", t), a = J("categorical", t).filter((e) => !Nn(e)), o = J("temporal", t), s = J("latitude", t), c = J("longitude", t), l = J("geographic", t);
3669
+ }, i = J("numeric", t), a = J("categorical", t).filter((e) => !Pn(e)), o = J("temporal", t), s = J("latitude", t), c = J("longitude", t), l = J("geographic", t);
3545
3670
  if (t.size === 0) return {};
3546
3671
  if (!n && i.length === 0 && l.length === 0 && c.length === 0 && s.length === 0) return r.chartType = "headline", r;
3547
3672
  let u = a.filter((e) => U(e));
@@ -3556,7 +3681,7 @@ var W = (e, t, n) => {
3556
3681
  n.size > 7 ? r.chartType = "barh" : r.chartType = "bar";
3557
3682
  } else i.length >= 2 ? e.length === 1 ? r.chartType = "headline" : r.chartType = "point" : a.length >= 2 && i.length > 0 ? r.chartType = "heatmap" : i.length > 0 && a.length == 0 && (r.chartType = "headline");
3558
3683
  if (r.chartType === "barh") {
3559
- r.yField = H(a, 0)?.name, i.length > 0 && (r.xField = Pn(r, i).name);
3684
+ r.yField = H(a, 0)?.name, i.length > 0 && (r.xField = Fn(r, i).name);
3560
3685
  let e = a.filter((e) => e.name !== r.yField && e.name !== r.xField);
3561
3686
  e.length > 0 && (r.colorField = e[0].name), i.length > 1 && !r.colorField && (r.colorField = i[1].name);
3562
3687
  let t = a.filter((e) => e.name !== r.yField && e.name !== r.xField && e.name !== r.colorField);
@@ -3604,7 +3729,7 @@ var W = (e, t, n) => {
3604
3729
  n.length > 0 && (r.trellisRowField = n[0].name), n.length > 1 && (r.trellisField = n[1].name);
3605
3730
  }
3606
3731
  return r;
3607
- }, In = (e, t) => {
3732
+ }, Ln = (e, t) => {
3608
3733
  let n = J("numeric", t), r = J("categorical", t), i = J("temporal", t), a = J("latitude", t), o = J("longitude", t), s = J("geographic", t), c = [];
3609
3734
  if (r.length > 0 && c.push("beeswarm"), n.length === 0) return (a.length > 0 && o.length > 0 || s.length > 0) && c.push("geo-map"), c.push("headline"), c;
3610
3735
  let l = r.filter((e) => U(e));
@@ -3613,19 +3738,19 @@ var W = (e, t, n) => {
3613
3738
  for (let r = 0; r < Math.min(e.length, 50); r++) e[r] && e[r][t] !== void 0 && n.add(e[r][t]);
3614
3739
  c.push("bar"), c.push("barh"), c.push("donut");
3615
3740
  }
3616
- return n.length >= 2 && c.push("point"), r.length >= 2 && n.length > 0 && c.push("heatmap"), n.length > 0 && (c.push("boxplot"), c.push("headline")), (a.length > 0 && o.length > 0 || s.length > 0) && c.push("geo-map"), c.filter((e) => yn.map((e) => e.value).includes(e));
3617
- }, Ln = (e) => q(e, "latitude") ? "latitude" : q(e, "longitude") ? "longitude" : e.traits?.some((e) => e.endsWith("state") || e.endsWith("state_short") || e.endsWith("country")) ? "geographic" : K(e) ? "temporal" : U(e) ? "categorical" : G(e) ? "numeric" : Mn(e) ? "categorical" : "other", Rn = (e) => {
3741
+ return n.length >= 2 && c.push("point"), r.length >= 2 && n.length > 0 && c.push("heatmap"), n.length > 0 && (c.push("boxplot"), c.push("headline")), (a.length > 0 && o.length > 0 || s.length > 0) && c.push("geo-map"), c.filter((e) => bn.map((e) => e.value).includes(e));
3742
+ }, Rn = (e) => q(e, "latitude") ? "latitude" : q(e, "longitude") ? "longitude" : e.traits?.some((e) => e.endsWith("state") || e.endsWith("state_short") || e.endsWith("country")) ? "geographic" : K(e) ? "temporal" : U(e) ? "categorical" : G(e) ? "numeric" : Nn(e) ? "categorical" : "other", zn = (e) => {
3618
3743
  let t = [];
3619
3744
  return e.forEach((e) => {
3620
- t.push(`${e.name} (${Ln(e)})`);
3745
+ t.push(`${e.name} (${Rn(e)})`);
3621
3746
  }), t.length > 0 ? t.join(", ") : "no fields";
3622
- }, zn = (e, n, r) => {
3623
- let i = In(e, n), a = [];
3747
+ }, Bn = (e, n, r) => {
3748
+ let i = Ln(e, n), a = [];
3624
3749
  if (r.layers && r.layers.length > 0) {
3625
3750
  let e = [], a = Array.from(n.keys());
3626
3751
  return r.layers.forEach((r, i) => {
3627
3752
  let o = `Layer ${i + 1}`;
3628
- _n.includes(r.chartType) || e.push(`${o}: chart type "${r.chartType}" cannot be used as a layer. Layerable types: ${_n.join(", ")}.`);
3753
+ vn.includes(r.chartType) || e.push(`${o}: chart type "${r.chartType}" cannot be used as a layer. Layerable types: ${vn.join(", ")}.`);
3629
3754
  for (let i of t) {
3630
3755
  let t = r[i];
3631
3756
  typeof t == "string" && t !== "" && !n.has(t) && e.push(`${o}: field "${t}" (${i}) does not exist in the query results. Available columns: ${a.join(", ")}.`);
@@ -3639,16 +3764,16 @@ var W = (e, t, n) => {
3639
3764
  };
3640
3765
  }
3641
3766
  if (!i.includes(r.chartType)) {
3642
- let t = Rn(n);
3767
+ let t = zn(n);
3643
3768
  return {
3644
3769
  valid: !1,
3645
3770
  chartTypeError: `Chart type "${r.chartType}" is not compatible with the available fields (${t}). Eligible chart types: ${i.join(", ")}.`,
3646
3771
  fieldErrors: [],
3647
3772
  eligibleChartTypes: i,
3648
- suggestedConfig: Fn(e, n)
3773
+ suggestedConfig: In(e, n)
3649
3774
  };
3650
3775
  }
3651
- let o = Fn(e, n, [
3776
+ let o = In(e, n, [
3652
3777
  "line",
3653
3778
  "bar",
3654
3779
  "barh",
@@ -3675,7 +3800,7 @@ var W = (e, t, n) => {
3675
3800
  let t = r[e];
3676
3801
  typeof t == "string" && t !== "" && !n.has(t) && a.push(`Field "${t}" (${e}) does not exist in the query results. Available columns: ${s.join(", ")}.`);
3677
3802
  }
3678
- let c = vn.filter((e) => !e.allowEmpty && e.visibleFor.includes(r.chartType));
3803
+ let c = yn.filter((e) => !e.allowEmpty && e.visibleFor.includes(r.chartType));
3679
3804
  for (let e of c) {
3680
3805
  let t = r[e.field];
3681
3806
  (typeof t != "string" || t === "") && a.push(`Chart type "${r.chartType}" requires ${e.label} (${String(e.field)}) to be specified, but it is missing.`);
@@ -3686,7 +3811,7 @@ var W = (e, t, n) => {
3686
3811
  }
3687
3812
  if ((r.chartType === "line" || r.chartType === "area") && r.xField && n.has(r.xField)) {
3688
3813
  let e = n.get(r.xField);
3689
- !K(e) && !Mn(e) && !G(e) && a.push(`For ${r.chartType} charts, xField should typically be a temporal or categorical column.`);
3814
+ !K(e) && !Nn(e) && !G(e) && a.push(`For ${r.chartType} charts, xField should typically be a temporal or categorical column.`);
3690
3815
  }
3691
3816
  let l = { ...o };
3692
3817
  for (let [e, t] of Object.entries(r)) t != null && (typeof t == "string" && t === "" || (l[e] = t));
@@ -3696,7 +3821,7 @@ var W = (e, t, n) => {
3696
3821
  eligibleChartTypes: i,
3697
3822
  suggestedConfig: l
3698
3823
  };
3699
- }, Bn = (e) => `Chart configuration is invalid for the data:\n${[e.chartTypeError, ...e.fieldErrors].filter(Boolean).map((e) => ` - ${e}`).join("\n")}\nEligible chart types for this data: ${e.eligibleChartTypes.join(", ") || "none"}.\nSuggested auto-detected config: ${JSON.stringify(e.suggestedConfig)}\nTip: Omit chartConfig to let auto-detection choose the best chart type and field mapping.`, Y = (e, t) => {
3824
+ }, Vn = (e) => `Chart configuration is invalid for the data:\n${[e.chartTypeError, ...e.fieldErrors].filter(Boolean).map((e) => ` - ${e}`).join("\n")}\nEligible chart types for this data: ${e.eligibleChartTypes.join(", ") || "none"}.\nSuggested auto-detected config: ${JSON.stringify(e.suggestedConfig)}\nTip: Omit chartConfig to let auto-detection choose the best chart type and field mapping.`, Y = (e, t) => {
3700
3825
  if (!e || !t.get(e)) return {};
3701
3826
  let r = t.get(e);
3702
3827
  if (!r) return {};
@@ -3721,7 +3846,7 @@ var W = (e, t, n) => {
3721
3846
  case n.INTEGER: return {};
3722
3847
  default: return {};
3723
3848
  }
3724
- }, Vn = (e, t, n = null) => {
3849
+ }, Hn = (e, t, n = null) => {
3725
3850
  if (!e || !t.get(e)) return {};
3726
3851
  let r = t.get(e);
3727
3852
  return r ? W(e, t, "day_of_week_name") ? { sort: [
@@ -3732,7 +3857,7 @@ var W = (e, t, n) => {
3732
3857
  "Thursday",
3733
3858
  "Friday",
3734
3859
  "Saturday"
3735
- ] } : An(r) || U(r) || K(r) ? { sort: {
3860
+ ] } : jn(r) || U(r) || K(r) ? { sort: {
3736
3861
  field: e,
3737
3862
  order: "ascending"
3738
3863
  } } : G(r) ? n ? { sort: {
@@ -3757,19 +3882,19 @@ var W = (e, t, n) => {
3757
3882
  n.DATETIME,
3758
3883
  n.TIMESTAMP,
3759
3884
  n.INTEGER
3760
- ].includes(r.type) || [n.DATE].includes(r.type) ? "temporal" : "ordinal" : An(r) || U(r) ? "ordinal" : G(r) ? "quantitative" : "nominal" : "nominal";
3761
- }, Hn = (e, t, n = {}, i = !0, a = {}) => e ? {
3885
+ ].includes(r.type) || [n.DATE].includes(r.type) ? "temporal" : "ordinal" : jn(r) || U(r) ? "ordinal" : G(r) ? "quantitative" : "nominal" : "nominal";
3886
+ }, Un = (e, t, n = {}, i = !0, a = {}) => e ? {
3762
3887
  field: e,
3763
3888
  type: X(e, t),
3764
3889
  title: r(t.get(e)?.description || e),
3765
3890
  ...Y(e, t),
3766
3891
  ...n,
3767
- ...i ? Vn(e, t) : {},
3892
+ ...i ? Hn(e, t) : {},
3768
3893
  ...a.scale !== void 0 || a.zero !== void 0 ? { scale: {
3769
3894
  ...a.scale !== void 0 && { type: a.scale },
3770
3895
  ...a.zero !== void 0 && { zero: a.zero }
3771
3896
  } } : {}
3772
- } : {}, Un = (e = 0) => e > 0 ? `_l${e}` : "", Z = (e, t = "") => `${e}${t}`, Wn = (e = "") => ({
3897
+ } : {}, Wn = (e = 0) => e > 0 ? `_l${e}` : "", Z = (e, t = "") => `${e}${t}`, Gn = (e = "") => ({
3773
3898
  fillOpacity: {
3774
3899
  condition: {
3775
3900
  param: Z("select", e),
@@ -3789,7 +3914,7 @@ var W = (e, t, n) => {
3789
3914
  }],
3790
3915
  value: 0
3791
3916
  }
3792
- }), Gn = (e, t, n, r = "color") => {
3917
+ }), Kn = (e, t, n, r = "color") => {
3793
3918
  let i = !1;
3794
3919
  return n === "quantitative" && r === "color" && (i = !0), e && e.length > 10 ? {
3795
3920
  orient: t ? "bottom" : "right",
@@ -3802,14 +3927,14 @@ var W = (e, t, n) => {
3802
3927
  direction: t ? "horizontal" : "vertical",
3803
3928
  titleFontSize: t ? 10 : 12
3804
3929
  };
3805
- }, Kn = 15, qn = (e, t, n, i = !1, a = "light", o = !1, s = [], c = "") => {
3806
- let l = { tickCount: Kn }, u = [], d = [], f = s || [], p = Array.from(n.entries()).filter(([e, t]) => t.traits?.includes("hex")).map(([e, t]) => e);
3930
+ }, qn = 15, Jn = (e, t, n, i = !1, a = "light", o = !1, s = [], c = "") => {
3931
+ let l = { tickCount: qn }, u = [], d = [], f = s || [], p = Array.from(n.entries()).filter(([e, t]) => t.traits?.includes("hex")).map(([e, t]) => e);
3807
3932
  if (t) {
3808
3933
  let e = X(t, n);
3809
3934
  l = {
3810
3935
  ...l,
3811
- ...Gn(t, i, e)
3812
- }, (e === "nominal" || e === "ordinal") && (d = Array.from(new Set(f.map((e) => String(e[t] ?? "")))).sort((e, t) => e.localeCompare(t))), u = d.slice(0, Kn);
3936
+ ...Kn(t, i, e)
3937
+ }, (e === "nominal" || e === "ordinal") && (d = Array.from(new Set(f.map((e) => String(e[t] ?? "")))).sort((e, t) => e.localeCompare(t))), u = d.slice(0, qn);
3813
3938
  }
3814
3939
  let m = (e) => (!o && Object.keys(l).length > 0 ? e.legend = l : o && (e.legend = null), e);
3815
3940
  if (t && n.get(t)) {
@@ -3838,23 +3963,23 @@ var W = (e, t, n) => {
3838
3963
  ...c === null ? {} : { condition: [{
3839
3964
  param: Z("highlight", c),
3840
3965
  empty: !1,
3841
- value: Sn
3966
+ value: Cn
3842
3967
  }, {
3843
3968
  param: Z("select", c),
3844
3969
  empty: !1,
3845
- value: Sn
3970
+ value: Cn
3846
3971
  }] },
3847
3972
  ...Y(t, n)
3848
3973
  });
3849
3974
  }
3850
3975
  return m({});
3851
- }, Jn = (e, t, n = !1, i = !1) => {
3976
+ }, Yn = (e, t, n = !1, i = !1) => {
3852
3977
  let a = { tickCount: 5 };
3853
3978
  if (e) {
3854
3979
  let r = X(e, t);
3855
3980
  a = {
3856
3981
  ...a,
3857
- ...Gn(e, n, r, "size")
3982
+ ...Kn(e, n, r, "size")
3858
3983
  };
3859
3984
  }
3860
3985
  return e && t.get(e) ? ((e) => (!i && Object.keys(a).length > 0 ? e.legend = a : i && (e.legend = null), e))({
@@ -3869,7 +3994,7 @@ var W = (e, t, n) => {
3869
3994
  };
3870
3995
  //#endregion
3871
3996
  //#region llm/importConcepts.ts
3872
- async function Yn(e, t, n) {
3997
+ async function Xn(e, t, n) {
3873
3998
  let { connectionStore: r, editorStore: i, queryExecutionService: a } = e;
3874
3999
  try {
3875
4000
  if (!i?.editors[t.id]?.contents) return "No fields found in this data source.";
@@ -3912,7 +4037,7 @@ function Q(e) {
3912
4037
  function $(e, t) {
3913
4038
  return e.connections[t] || e.connectionByName(t) || null;
3914
4039
  }
3915
- async function Xn(e, t, n) {
4040
+ async function Zn(e, t, n) {
3916
4041
  if (!t || typeof t != "string") return {
3917
4042
  success: !1,
3918
4043
  error: `Connection name is required. Available connections: ${Q(e) || "None"}`
@@ -3939,7 +4064,7 @@ async function Xn(e, t, n) {
3939
4064
  };
3940
4065
  }
3941
4066
  }
3942
- function Zn(e, t, n, r) {
4067
+ function Qn(e, t, n, r) {
3943
4068
  let i = /* @__PURE__ */ new Map(), a = $(e, n), o = a?.id || n;
3944
4069
  return e.getConnectionSources(o).forEach((e) => i.set(e.alias, e.contents)), t && a && Object.values(t.editors).filter((e) => !e.deleted && e.connectionId === a.id).forEach((e) => {
3945
4070
  i.set(e.name, e.contents);
@@ -3951,7 +4076,7 @@ function Zn(e, t, n, r) {
3951
4076
  contents: t
3952
4077
  }));
3953
4078
  }
3954
- function Qn(e, t, n, r) {
4079
+ function $n(e, t, n, r) {
3955
4080
  let i = typeof t?.toJSON == "function" ? t.toJSON() : t, a = i?.data;
3956
4081
  if (!Array.isArray(a)) return {
3957
4082
  success: !1,
@@ -3975,7 +4100,7 @@ function Qn(e, t, n, r) {
3975
4100
  }, null, 2)
3976
4101
  };
3977
4102
  }
3978
- function $n(e, t, n) {
4103
+ function er(e, t, n) {
3979
4104
  if (!e) return [];
3980
4105
  let r = n ? $(n, t) : null;
3981
4106
  return Object.values(e.editors).filter((e) => e.deleted ? !1 : r ? e.connectionId === r.id : e.connection === t).sort((e, t) => e.name.localeCompare(t.name)).map((e) => ({
@@ -3984,7 +4109,7 @@ function $n(e, t, n) {
3984
4109
  alias: ""
3985
4110
  }));
3986
4111
  }
3987
- async function er(e, t, n, r, i) {
4112
+ async function tr(e, t, n, r, i) {
3988
4113
  let a = t.getConnectionName(), o = i && a ? $(i, a) : null;
3989
4114
  if (!e || e.trim() === "") return t.setActiveImports([]), {
3990
4115
  success: !0,
@@ -3995,27 +4120,27 @@ async function er(e, t, n, r, i) {
3995
4120
  success: !1,
3996
4121
  error: "No data connection selected. Please select a data connection first."
3997
4122
  };
3998
- let s = $n(r, o?.id || a, i), c = s.find((t) => t.name === e || t.name.endsWith(`.${e}`));
4123
+ let s = er(r, o?.id || a, i), c = s.find((t) => t.name === e || t.name.endsWith(`.${e}`));
3999
4124
  if (!c) return {
4000
4125
  success: !1,
4001
4126
  error: `Import "${e}" not found. Available imports: ${s.map((e) => e.name).join(", ") || "none"}`
4002
4127
  };
4003
4128
  let l = t.getActiveImports(), u = l.length === 1 && l[0].id === c.id;
4004
4129
  u || t.setActiveImports([c]);
4005
- let d = await Yn(n, c, o?.id || a);
4130
+ let d = await Xn(n, c, o?.id || a);
4006
4131
  return {
4007
4132
  success: !0,
4008
4133
  message: `${u ? `"${c.name}" is already the active data source.` : `Successfully selected "${c.name}" as the active data source.`}\n\n${d}`,
4009
4134
  triggersSymbolRefresh: !u
4010
4135
  };
4011
4136
  }
4012
- function tr(e, t, n) {
4137
+ function nr(e, t, n) {
4013
4138
  let r = e.getConnectionName(), i = n && r ? $(n, r) : null, a = i?.name || r;
4014
4139
  if (!r) return {
4015
4140
  success: !1,
4016
4141
  error: "No data connection selected. Please select a data connection first."
4017
4142
  };
4018
- let o = $n(t, i?.id || r, n), s = e.getActiveImports(), c = s.length > 0 ? s[0] : null;
4143
+ let o = er(t, i?.id || r, n), s = e.getActiveImports(), c = s.length > 0 ? s[0] : null;
4019
4144
  return o.length === 0 ? {
4020
4145
  success: !0,
4021
4146
  message: `No data sources available on connection "${a}".`
@@ -4024,7 +4149,7 @@ function tr(e, t, n) {
4024
4149
  message: `${c ? `Current active data source: ${c.name}\n\n` : "No data source currently selected.\n\n"}Available data sources for connection "${a}":\n${o.map((e) => `- ${e.name}${c?.id === e.id ? " (active)" : ""}`).join("\n")}\n\nUse select_active_import to select a data source.`
4025
4150
  };
4026
4151
  }
4027
- async function nr(e, t, n, r, i, a) {
4152
+ async function rr(e, t, n, r, i, a) {
4028
4153
  if (!i || typeof i != "string" || i.trim() === "") return {
4029
4154
  success: !1,
4030
4155
  error: "Query is required and must be a non-empty string",
@@ -4046,7 +4171,7 @@ async function nr(e, t, n, r, i, a) {
4046
4171
  error: `Connection "${o.name}" is not connected. Use connect_data_connection first.`,
4047
4172
  query: i
4048
4173
  };
4049
- let s = Zn(t, n, o.id, a), c = (a || []).map((e) => ({
4174
+ let s = Qn(t, n, o.id, a), c = (a || []).map((e) => ({
4050
4175
  name: e.name,
4051
4176
  alias: e.alias || ""
4052
4177
  })), l = {
@@ -4083,13 +4208,13 @@ async function nr(e, t, n, r, i, a) {
4083
4208
  }
4084
4209
  //#endregion
4085
4210
  //#region llm/editorRefinementToolExecutor.ts
4086
- function rr() {
4211
+ function ir() {
4087
4212
  return {
4088
4213
  artifactRegistry: /* @__PURE__ */ new Map(),
4089
4214
  lastQueryResults: { value: null }
4090
4215
  };
4091
4216
  }
4092
- var ir = class {
4217
+ var ar = class {
4093
4218
  queryExecutionService;
4094
4219
  connectionStore;
4095
4220
  editorStore;
@@ -4098,7 +4223,7 @@ var ir = class {
4098
4223
  lastQueryResults;
4099
4224
  constructor(e, t, n, r, i) {
4100
4225
  this.queryExecutionService = e, this.connectionStore = t, this.editorContext = n, this.editorStore = r || null;
4101
- let a = i ?? rr();
4226
+ let a = i ?? ir();
4102
4227
  this.artifactRegistry = a.artifactRegistry, this.lastQueryResults = a.lastQueryResults;
4103
4228
  }
4104
4229
  async executeToolCall(e, t) {
@@ -4251,7 +4376,7 @@ var ir = class {
4251
4376
  generatedSql: i.generatedSql
4252
4377
  };
4253
4378
  let a = {
4254
- id: B(),
4379
+ id: V(),
4255
4380
  type: "results",
4256
4381
  data: i.results,
4257
4382
  config: {
@@ -4317,7 +4442,7 @@ var ir = class {
4317
4442
  };
4318
4443
  let t = this.editorContext.getCurrentResults?.() ?? this.lastQueryResults.value ?? null;
4319
4444
  if (t && t.headers) {
4320
- let n = zn(t.data, t.headers, e);
4445
+ let n = Bn(t.data, t.headers, e);
4321
4446
  if (!n.valid) {
4322
4447
  let e = n.suggestedConfig;
4323
4448
  try {
@@ -4325,7 +4450,7 @@ var ir = class {
4325
4450
  } catch {}
4326
4451
  return {
4327
4452
  success: !1,
4328
- error: "Auto-corrected to default configuration.\n" + Bn(n)
4453
+ error: "Auto-corrected to default configuration.\n" + Vn(n)
4329
4454
  };
4330
4455
  }
4331
4456
  }
@@ -4394,7 +4519,7 @@ var ir = class {
4394
4519
  generatedSql: t.generatedSql
4395
4520
  };
4396
4521
  let n = {
4397
- id: B(),
4522
+ id: V(),
4398
4523
  type: "results",
4399
4524
  data: t.results,
4400
4525
  config: {
@@ -4424,7 +4549,7 @@ var ir = class {
4424
4549
  }
4425
4550
  getArtifactRows(e, t, n) {
4426
4551
  let r = this.artifactRegistry.get(e);
4427
- return r ? Qn(e, r.data, t, n) : {
4552
+ return r ? $n(e, r.data, t, n) : {
4428
4553
  success: !1,
4429
4554
  error: `Artifact "${e}" not found. Only artifacts created by run_query or run_active_editor_query in this session are available.`
4430
4555
  };
@@ -4451,7 +4576,7 @@ var ir = class {
4451
4576
  }
4452
4577
  }
4453
4578
  async connectDataConnection(e) {
4454
- return Xn(this.connectionStore, e);
4579
+ return Zn(this.connectionStore, e);
4455
4580
  }
4456
4581
  serializeDatabaseTree(e, t, n) {
4457
4582
  let r = this.connectionStore.connectionByName(this.editorContext.connectionName), i = r?.databases || [], a = (e ? i.filter((t) => t.name === e) : i).map((e) => {
@@ -4492,8 +4617,8 @@ var ir = class {
4492
4617
  };
4493
4618
  }
4494
4619
  buildEditorExtraContent() {
4495
- return Zn(this.connectionStore, this.editorStore, this.editorContext.connectionName);
4620
+ return Qn(this.connectionStore, this.editorStore, this.editorContext.connectionName);
4496
4621
  }
4497
4622
  };
4498
4623
  //#endregion
4499
- export { Ut as $, xe as $t, vn as A, We as At, pn as B, D as Bt, X as C, tt as Ct, Un as D, Ye as Dt, jn as E, O as Et, bn as F, He as Ft, rn as G, oe as Gt, fn as H, De as Ht, mn as I, Ae as It, L as J, re as Jt, sn as K, ne as Kt, V as L, Ie as Lt, gn as M, Re as Mt, hn as N, E as Nt, zn as O, Je as Ot, xn as P, Ve as Pt, R as Q, Ce as Qt, B as R, ze as Rt, Vn as S, Xe as St, Nn as T, et as Tt, qt as U, ae as Ut, z as V, Ue as Vt, on as W, te as Wt, Wt as X, le as Xt, I as Y, b as Yt, Kt as Z, ie as Zt, In as _, ut as _t, nr as a, c as an, Ct as at, W as b, Qe as bt, tr as c, Tt as ct, On as d, j as dt, ye as en, Bt as et, qn as f, N as ft, Fn as g, mt as gt, Jn as h, ht, Xn as i, f as in, wt as it, _n as j, Ge as jt, yn as k, Ke as kt, er as l, Et as lt, Wn as m, gt as mt, rr as n, y as nn, F as nt, Qn as o, d as on, At as ot, Hn as p, A as pt, en as q, se as qt, Zn as r, m as rn, Nt as rt, $n as s, u as sn, St as st, ir as t, Se as tn, zt as tt, Sn as u, M as ut, J as v, ot as vt, Tn as w, $e as wt, Y as x, Ze as xt, Bn as y, k as yt, dn as z, Be as zt };
4624
+ export { Vt as $, ie as $t, bn as A, qe as At, dn as B, Re as Bt, X as C, Xe as Ct, Wn as D, $e as Dt, Mn as E, Qe as Et, Sn as F, D as Ft, an as G, ae as Gt, sn as H, O as Ht, xn as I, Be as It, $t as J, ne as Jt, nn as K, te as Kt, hn as L, Ve as Lt, vn as M, Ue as Mt, _n as N, We as Nt, Z as O, k as Ot, gn as P, Le as Pt, Gt as Q, le as Qt, mn as R, ke as Rt, Hn as S, Ze as St, Pn as T, et as Tt, fn as U, He as Ut, pn as V, ze as Vt, Kt as W, De as Wt, z as X, re as Xt, B as Y, se as Yt, Ut as Z, b as Zt, Ln as _, mt as _t, rr as a, m as an, St as at, W as b, at as bt, nr as c, d as cn, xt as ct, kn as d, P as dt, Ce as en, Ht as et, Jn as f, N as ft, In as g, ht as gt, Yn as h, gt as ht, Zn as i, y as in, jt as it, yn as j, Ge as jt, Bn as k, Je as kt, tr as l, u as ln, Ct as lt, Gn as m, j as mt, ir as n, ye as nn, Lt as nt, $n as o, f as on, I as ot, Un as p, F as pt, on as q, oe as qt, Qn as r, Se as rn, R as rt, er as s, c as sn, Ot as st, ar as t, xe as tn, Rt as tt, Cn as u, wt as ut, J as v, ut as vt, En as w, Ye as wt, Y as x, A as xt, Vn as y, ot as yt, V as z, Fe as zt };