@bitcobblers/wod-wiki-wql 0.51.2195 → 0.52.2198

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.
package/dist/index.cjs CHANGED
@@ -4,6 +4,7 @@ var wodWikiCore = require('@bitcobblers/wod-wiki-core');
4
4
  var lr = require('@lezer/lr');
5
5
  var language = require('@codemirror/language');
6
6
  var autocomplete = require('@codemirror/autocomplete');
7
+ var state = require('@codemirror/state');
7
8
  var highlight = require('@lezer/highlight');
8
9
 
9
10
  var __defProp = Object.defineProperty;
@@ -59,7 +60,7 @@ var WQL_CALC_TARGETS = [
59
60
  ];
60
61
  var WQL_INTENSITY_TIERS = ["low", "moderate", "high"];
61
62
  var WQL_GRAINS = ["summary", "event"];
62
- var WQL_FIND_TARGETS = ["note", "block", "effort", "session", "segment", "event", "notebook", "page", "tag", "equipment"];
63
+ var WQL_FIND_TARGETS = ["note", "block", "effort", "session", "segment", "event", "notebook", "page", "tag", "equipment", "feed"];
63
64
  var WQL_SOURCE_VALUES = ["journal", "notebooks", "guides", "playground", "dashboards", "efforts", "equipment"];
64
65
  var WQL_SOURCE_HEADS = [
65
66
  "note",
@@ -76,6 +77,8 @@ var WQL_SOURCE_HEADS = [
76
77
  "page",
77
78
  "tag",
78
79
  "equipment",
80
+ "feed",
81
+ "feeds",
79
82
  "dashboard"
80
83
  ];
81
84
  var WQL_SOURCE_HEAD_SCOPES = {
@@ -89,8 +92,8 @@ var WQL_TYPE_HEADS = {
89
92
  };
90
93
  var WQL_NOTE_DEFAULT_SOURCES = [];
91
94
  var WQL_FUNCTION_HEADS = WQL_AGGREGATORS;
92
- var WQL_CHART_HEADS = ["timeseries", "bar", "table", "donut", "toplist", "value"];
93
- var WQL_STANDARD_DATASETS = ["@session", "@today"];
95
+ var WQL_CHART_HEADS = ["timeseries", "bar", "stacked-bar", "table", "list", "donut", "toplist", "value", "goal-rings", "zone-distribution"];
96
+ var WQL_STANDARD_DATASETS = ["@session", "@today", "@note"];
94
97
  var WQL_RESULT_PLANES = wodWikiCore.KNOWN_OUTPUT_TYPES;
95
98
  var WQL_CONTENT_FILTER_KEYS = ["type", "text", "source", "notebook", "page", "contributes", "feed", "subscribed", ...WQL_TAG_KEYS];
96
99
  var WQL_SOURCES = WQL_FIND_TARGETS;
@@ -147,10 +150,11 @@ var TARGET_TYPES = {
147
150
  // return flat rows (no `by {}` grouping), so groupby stays unadvertised.
148
151
  page: /* @__PURE__ */ new Set(["page", "notebook", "text", "note", "time"]),
149
152
  tag: /* @__PURE__ */ new Set(["tag", "type", "text", "contributes", "time"]),
150
- equipment: /* @__PURE__ */ new Set(["equipment", "tags", "text", "notebook", "time"])
153
+ equipment: /* @__PURE__ */ new Set(["equipment", "tags", "text", "notebook", "time"]),
154
+ feed: /* @__PURE__ */ new Set(["feed", "tag", "text", "subscribed", "groupby"])
151
155
  };
152
156
  var EMPTY_TYPES = /* @__PURE__ */ new Set();
153
- var AGGREGATE_KEYS = WQL_TAG_KEYS;
157
+ var AGGREGATE_KEYS = ["metric", ...WQL_TAG_KEYS.filter((key) => key !== "metric")];
154
158
  var TARGET_KEYS = Object.fromEntries(
155
159
  Object.entries(TARGET_TYPES).map(([target, types]) => [
156
160
  target,
@@ -179,14 +183,14 @@ function supportsWqlFilterKey(target, family, rawKey) {
179
183
  }
180
184
  var parser = lr.LRParser.deserialize({
181
185
  version: 14,
182
- states: "+dOVQPOOOOQO'#Cb'#CbObQPO'#C`OgQPO'#C`OlQPO'#ClOqQQO'#C_OOQO'#C_'#C_O!SQQO'#CnQOQPOOOOQO,58z,58zO!bQPO,58zOOQO,59W,59WO!gQPO'#CdOOQO,58y,58yOVQPO,58xO!rQPO'#CoO!wQPO'#CrOOQO,59Y,59YO!|QPO,59YO#UQQO'#CcOOQO1G.f1G.fOOQO'#Cg'#CgO#jQPO'#CeO#oQPO'#CeOOQO,59O,59OO#tQPO,59OO#|QPO'#C_O$XQPO1G.dO$aQPO,59ZO$iQQO,59^OOQO1G.t1G.tO$nQPO'#CuO$sQQO,58}O%XQPO,59PO%dQPO,59PO%iQPO'#CxO%qQPO1G.jOOQO1G.j1G.jOVQPO'#CyO%yQPO7+$OOOQO'#Cq'#CqOOQO1G.u1G.uO&RQPO1G.uO&ZQPO1G.xOOQO,59a,59aOOQO-E6s-E6sOOQO'#Ci'#CiO&`QPO'#CiO&tQPO'#CiO'SQPO'#ChOOQO1G.k1G.kO%XQPO1G.kOOQO,59d,59dOOQO-E6v-E6vOOQO7+$U7+$UO'_QPO'#CcOOQO,59e,59eOOQO-E6w-E6wO'mQPO'#CzO'rQPO7+$aOOQO7+$a7+$aO'zQPO7+$dO(PQPO'#CvO(UQPO,59TOOQO,59T,59TO%XQPO'#CwO(jQPO,59SOOQO7+$V7+$VO(uQPO,58}OOQO,59f,59fOOQO-E6x-E6xOOQO<<G{<<G{OOQO<<HO<<HOOOQO,59b,59bOOQO-E6t-E6tOOQO1G.o1G.oOOQO,59c,59cOOQO-E6u-E6uO)TQPO,58zO)YQPO'#C`",
183
- stateData: ")_~OqOS~OTPOaSOrQO~OTXO~OrYO~OTZO~Ot[OdRXgRXoRXuRX~Od_Og`Ou^OobX~OTcO~OTeOYgOwhO~OtlO~OxmO~Og`Ooba~OsoOdVXgVXoVXtVXuVX~OrqO~OTeO~OvsOwuO~Ot[OoRXuRX~OuvOoQi~OTxOwyO~Oh{O~OT|O~OsoOdVagVaoVatVauVa~OT!PO^!OO_!QO~Or!TO~OTeOYgO~OvsOw!WO~OuvOoQq~Ov![Ow!^O~OT!_O~O^!bOr!`Os!`Ou]Xv]Xw]X~O^!bOu]Xv]Xw]X~Ou!cOv[Xw[X~OsoOoVXtVXuVX~OTxO~Ov![Ow!iO~Oy!jO~OT!kO~O^!mOr!`Os!`Ou]av]aw]a~Ou!cOv[aw[a~OsoOoVatVauVa~OT!XO~Or!pO~O",
184
- goto: "$^oPPps|P!T![!`!dP!j!q!wPP#OPp#TP#W#^PP#d#n#t#z$Q$WRWOQVOQk^R!YvQTOTj^vQROT!q^vTdY!pT]TjQi[R!UsSf[sRrgQ!SqR!e!TS!Rq!TR!n!cVUO^vRbVQzlR!g![QaVRnbQpcS}p!fR!f!XQ!a!PR!l!aQ!d!RR!o!dQtiR!VtQwkR!ZwQ!]zR!h!]",
185
- nodeNames: "\u26A0 Query Pipeline Stage Head Word Aggregator Metric Filters Filter Negate TagKey TagValue Value Star Quoted DatasetReference At Single GroupBy By Dimension Rollup RollupDot Int",
186
- maxTerm: 41,
186
+ states: "+|OVQPOOOOQO'#Cb'#CbObQPO'#C`OgQPO'#C`OlQPO'#CnOqQQO'#C_OOQO'#C_'#C_O!SQQO'#CpQOQPOOOOQO,58z,58zO!bQPO,58zOOQO,59Y,59YO!jQPO'#CfOOQO,58y,58yOVQPO,58xO!xQPO'#CqO!}QPO'#CtOOQO,59[,59[O#SQPO,59[O#[QPO'#CdO#aQQO'#CcOOQO'#Cc'#CcOOQO1G.f1G.fOOQO'#Ci'#CiO#uQPO'#CgO#zQPO'#CgOOQO'#Cg'#CgOOQO,59Q,59QO$PQPO,59QO$XQPO'#C_O$dQPO1G.dO$lQPO,59]O$tQQO,59`OOQO1G.v1G.vOOQO,59O,59OO$yQPO'#CwO%OQQO,58}O%dQPO,59RO%rQPO,59RO%wQPO'#CzO&SQPO1G.lOOQO1G.l1G.lOVQPO'#C{O&[QPO7+$OOOQO'#Cs'#CsOOQO1G.w1G.wO&dQPO1G.wO&lQPO1G.zOOQO,59c,59cOOQO-E6u-E6uOOQO'#Ck'#CkO&qQPO'#CkO'VQPO'#CkO'eQPO'#CjOOQO1G.m1G.mO%dQPO1G.mOOQO,59f,59fOOQO-E6x-E6xOOQO7+$W7+$WO'pQPO'#CcOOQO,59g,59gOOQO-E6y-E6yO(OQPO'#C|O(TQPO7+$cOOQO7+$c7+$cO(]QPO7+$fO(bQPO'#CxO(gQPO,59VOOQO,59V,59VO%dQPO'#CyO({QPO,59UOOQO7+$X7+$XO)WQPO,58}OOQO,59h,59hOOQO-E6z-E6zOOQO<<G}<<G}OOQO<<HQ<<HQOOQO,59d,59dOOQO-E6v-E6vOOQO1G.q1G.qOOQO,59e,59eOOQO-E6w-E6wO)fQPO,58zO)nQPO'#C`",
187
+ stateData: ")s~OsOS~OTPOcSOtQO~OTXO~OtYO~OTZO~Ov[OfRXiRXqRXwRX~Of_Oi`Ow^OqdX~OTdOXcO~OTgOXcO[iOykO~OvoO~OzpO~Oi`Oqda~OTrO~OusOfVXiVXqVXvVXwVX~OtuO~OTgO~OxwOyyO~Ov[OqRXwRX~OwzOqQi~OT|Oy}O~Oj!PO~OT!QO~OusOfVaiVaqVavVawVa~OT!TOXcO`!SOa!UO~Ot!XO~OTgOXcO[iO~OxwOy![O~OwzOqQq~Ox!`Oy!bO~OT!cO~O`!fOt!dOu!dOw_Xx_Xy_X~O`!fOw_Xx_Xy_X~Ow!gOx^Xy^X~OusOqVXvVXwVX~OT|O~Ox!`Oy!mO~O{!nO~OT!oO~O`!qOt!dOu!dOw_ax_ay_a~Ow!gOx^ay^a~OusOqVavVawVa~OT!]OXcO~Ot!tO~O",
188
+ goto: "$mqPPru!OP!V!^!bP!o!sP!y#Q#WPP#_Pr#dP#g#mPP#s#}$T$Z$a$gRWOQVOQn^R!^zQTOTm^zQROT!u^zTfY!tSeY!tSj[wV!Su!X!gT]TmQl[R!YwSh[wRviQ!WuR!i!XS!Vu!XR!r!gVUO^zRbVQ!OoR!k!`QaVRqbQtdS!Rt!jR!j!]Q!e!TR!p!eQ!h!VR!s!hQxlR!ZxQ{nR!_{Q!a!OR!l!a",
189
+ nodeNames: "\u26A0 Query Pipeline Stage Head Word Aggregator Metric ParamRef Dollar Filters Filter Negate TagKey TagValue Value Star Quoted DatasetReference At Single GroupBy By Dimension Rollup RollupDot Int",
190
+ maxTerm: 43,
187
191
  skippedNodes: [0],
188
192
  repeatNodeCount: 6,
189
- tokenData: "'i~RgXY!jYZ!j]^!jpq!jqr!{rs#Qxy#oyz#tz{#y|}$O}!O$T!O!P$i!Q![%f![!]%|!b!c&R!c!}$T#R#S$T#T#U$T#U#V&W#V#o$T#o#p'Y#p#q'_#q#r'd~!oSq~XY!jYZ!j]^!jpq!j~#QOY~~#TTOr#Qrs#ds;'S#Q;'S;=`#i<%lO#Q~#iO_~~#lP;=`<%l#Q~#tOx~~#yOy~~$OO^~~$TOv~P$YTTP}!O$T!Q![$T!c!}$T#R#S$T#T#o$T~$nPs~#f#g$q~$tP#c#d$w~$zP#`#a$}~%QP#`#a%T~%WP#i#j%Z~%^P#d#e%a~%fOg~R%mThQTP}!O$T!Q![%f!c!}$T#R#S$T#T#o$T~&ROr~~&WOa~R&]VTP}!O$T!Q![$T!c!}$T#R#S$T#T#m$T#m#n&r#n#o$TR&yTdQTP}!O$T!Q![$T!c!}$T#R#S$T#T#o$T~'_Ot~~'dOu~~'iOw~",
193
+ tokenData: "'q~RhXY!mYZ!m]^!mpq!mqr#Ors#Ttu#rxy#wyz#|z{$R|}$W}!O$]!O!P$q!Q![%n![!]&U!b!c&Z!c!}$]#R#S$]#T#U$]#U#V&`#V#o$]#o#p'b#p#q'g#q#r'l~!rSs~XY!mYZ!m]^!mpq!m~#TO[~~#WTOr#Trs#gs;'S#T;'S;=`#l<%lO#T~#lOa~~#oP;=`<%l#T~#wOX~~#|Oz~~$RO{~~$WO`~~$]Ox~P$bTTP}!O$]!Q![$]!c!}$]#R#S$]#T#o$]~$vPu~#f#g$y~$|P#c#d%P~%SP#`#a%V~%YP#`#a%]~%`P#i#j%c~%fP#d#e%i~%nOi~R%uTjQTP}!O$]!Q![%n!c!}$]#R#S$]#T#o$]~&ZOt~~&`Oc~R&eVTP}!O$]!Q![$]!c!}$]#R#S$]#T#m$]#m#n&z#n#o$]R'RTfQTP}!O$]!Q![$]!c!}$]#R#S$]#T#o$]~'gOv~~'lOw~~'qOy~",
190
194
  tokenizers: [0, 1],
191
195
  topRules: { "Query": [0, 1] },
192
196
  tokenPrec: 0
@@ -200,6 +204,7 @@ __export(wql_parser_terms_exports, {
200
204
  By: () => By,
201
205
  DatasetReference: () => DatasetReference,
202
206
  Dimension: () => Dimension,
207
+ Dollar: () => Dollar,
203
208
  Filter: () => Filter,
204
209
  Filters: () => Filters,
205
210
  GroupBy: () => GroupBy,
@@ -207,6 +212,7 @@ __export(wql_parser_terms_exports, {
207
212
  Int: () => Int,
208
213
  Metric: () => Metric,
209
214
  Negate: () => Negate,
215
+ ParamRef: () => ParamRef,
210
216
  Pipeline: () => Pipeline,
211
217
  Query: () => Query,
212
218
  Quoted: () => Quoted,
@@ -227,23 +233,25 @@ var Head = 4;
227
233
  var Word = 5;
228
234
  var Aggregator = 6;
229
235
  var Metric = 7;
230
- var Filters = 8;
231
- var Filter = 9;
232
- var Negate = 10;
233
- var TagKey = 11;
234
- var TagValue = 12;
235
- var Value = 13;
236
- var Star = 14;
237
- var Quoted = 15;
238
- var DatasetReference = 16;
239
- var At = 17;
240
- var Single = 18;
241
- var GroupBy = 19;
242
- var By = 20;
243
- var Dimension = 21;
244
- var Rollup = 22;
245
- var RollupDot = 23;
246
- var Int = 24;
236
+ var ParamRef = 8;
237
+ var Dollar = 9;
238
+ var Filters = 10;
239
+ var Filter = 11;
240
+ var Negate = 12;
241
+ var TagKey = 13;
242
+ var TagValue = 14;
243
+ var Value = 15;
244
+ var Star = 16;
245
+ var Quoted = 17;
246
+ var DatasetReference = 18;
247
+ var At = 19;
248
+ var Single = 20;
249
+ var GroupBy = 21;
250
+ var By = 22;
251
+ var Dimension = 23;
252
+ var Rollup = 24;
253
+ var RollupDot = 25;
254
+ var Int = 26;
247
255
 
248
256
  // src/wqlSuffix.ts
249
257
  function splitAtWhere(text) {
@@ -265,6 +273,7 @@ var DISPLAY_UNIT_RE = /\s+in\s+([a-zA-Z0-9_-]+)\s*$/;
265
273
  var ROLLUP_RE = /\.rollup\((\d+)?([a-zA-Z]*)\)\s*$/;
266
274
  var BY_RE = /\s+by\s+\{([^}]*)\}\s*$/;
267
275
  var LAST_RE = /\s+last\s+(\d+)([dw])\s*$/i;
276
+ var LAST_PARAM_RE = /\s+last\s+\$([a-zA-Z_][\w-]*)(?:\s+([dw]))?\s*$/i;
268
277
  var FROM_TO_RE = /\s+from\s+(\d{4}-\d{2}-\d{2})(?:\s+to\s+(\d{4}-\d{2}-\d{2}))?\s*$/i;
269
278
  function parseWqlSuffixes(raw) {
270
279
  const { primary, where } = splitAtWhere(raw);
@@ -305,6 +314,7 @@ function parseWqlSuffixes(raw) {
305
314
  storedAs = "@" + storeMatches[storeMatches.length - 1][1];
306
315
  }
307
316
  const froms = stripRepeated(FROM_TO_RE);
317
+ const lastParams = stripRepeated(LAST_PARAM_RE);
308
318
  const lasts = stripRepeated(LAST_RE);
309
319
  {
310
320
  const windows = [
@@ -315,6 +325,13 @@ function parseWqlSuffixes(raw) {
315
325
  raw: m[0].trim(),
316
326
  at: m.index
317
327
  })),
328
+ ...lastParams.map((m) => ({
329
+ kind: "param",
330
+ param: m[1],
331
+ ...m[2] ? { unit: m[2].toLowerCase() } : {},
332
+ raw: m[0].trim(),
333
+ at: m.index
334
+ })),
318
335
  ...lasts.map((m) => ({
319
336
  kind: "relative",
320
337
  size: parseInt(m[1], 10),
@@ -329,8 +346,8 @@ function parseWqlSuffixes(raw) {
329
346
  );
330
347
  }
331
348
  if (windows.length === 1) {
332
- const w = windows[0];
333
- window = w.kind === "range" ? { kind: "range", start: w.start, end: w.end, raw: w.raw } : { kind: "relative", size: w.size, unit: w.unit, raw: w.raw };
349
+ const { at: _at, ...w } = windows[0];
350
+ window = w;
334
351
  }
335
352
  }
336
353
  const units = stripRepeated(DISPLAY_UNIT_RE);
@@ -395,12 +412,14 @@ function normalizeCommaFilterValues(text) {
395
412
  const merged = [];
396
413
  for (let i = 0; i < parts.length; i++) {
397
414
  let part = parts[i];
398
- while (i + 1 < parts.length) {
399
- const next = parts[i + 1].trim();
400
- if (next && !next.includes(":") && !next.includes('"')) {
401
- part = `${part}|${next}`;
402
- i++;
403
- } else break;
415
+ if (!part.trim().startsWith("$")) {
416
+ while (i + 1 < parts.length) {
417
+ const next = parts[i + 1].trim();
418
+ if (next && !next.includes(":") && !next.includes('"') && !next.startsWith("$")) {
419
+ part = `${part}|${next}`;
420
+ i++;
421
+ } else break;
422
+ }
404
423
  }
405
424
  merged.push(part);
406
425
  }
@@ -658,8 +677,9 @@ function normalizeWql(raw) {
658
677
  }
659
678
  function validateSourceFilter(filters) {
660
679
  for (const f of filters) {
661
- if (f.key !== "source") continue;
680
+ if (f.kind === "spreadParam" || f.key !== "source") continue;
662
681
  for (const v of f.values) {
682
+ if (v.param !== void 0) continue;
663
683
  const val = v.value;
664
684
  if (val === "page" || val === "pages") {
665
685
  return `source:${val} is retired \u2014 page-ness is handled by type:page or source:guides.`;
@@ -686,8 +706,9 @@ function validateSourceFilter(filters) {
686
706
  }
687
707
  function validateSubscribedFilter(filters) {
688
708
  for (const f of filters) {
689
- if (f.key !== "subscribed") continue;
709
+ if (f.kind === "spreadParam" || f.key !== "subscribed") continue;
690
710
  for (const v of f.values) {
711
+ if (v.param !== void 0) continue;
691
712
  if (v.value !== "true" && v.value !== "false") {
692
713
  return `Invalid subscribed: value '${v.value}' \u2014 expected true or false`;
693
714
  }
@@ -697,8 +718,9 @@ function validateSubscribedFilter(filters) {
697
718
  }
698
719
  function validateFeedFilter(filters) {
699
720
  for (const f of filters) {
700
- if (f.key !== "feed") continue;
721
+ if (f.kind === "spreadParam" || f.key !== "feed") continue;
701
722
  for (const v of f.values) {
723
+ if (v.param !== void 0) continue;
702
724
  if (!v.value && !v.wildcard) {
703
725
  return `Invalid feed: value \u2014 expected a non-empty feed identifier or wildcard`;
704
726
  }
@@ -749,6 +771,9 @@ function toQueryWindow(w) {
749
771
  if (w.kind === "relative") {
750
772
  return { window: { kind: "relative", size: w.size, unit: w.unit } };
751
773
  }
774
+ if (w.kind === "param") {
775
+ return { window: { kind: "param", param: w.param, ...w.unit ? { unit: w.unit } : {} } };
776
+ }
752
777
  for (const d of [w.start, w.end]) {
753
778
  if (d !== void 0 && !isCivilDate(d)) {
754
779
  return { error: `Invalid window date "${d}" \u2014 expected a real calendar date YYYY-MM-DD` };
@@ -767,11 +792,33 @@ function extractFilters(query, text) {
767
792
  const filters = query.getChild(Filters);
768
793
  if (!filters) return out;
769
794
  for (const filter of filters.getChildren(Filter)) {
795
+ const spreadParamNode = filter.getChild(ParamRef);
796
+ if (spreadParamNode && !filter.getChild(TagKey)) {
797
+ const wordNode = spreadParamNode.getChild(Word);
798
+ const param = wordNode ? text.slice(wordNode.from, wordNode.to) : text.slice(spreadParamNode.from + 1, spreadParamNode.to);
799
+ if (param) {
800
+ out.push({
801
+ kind: "spreadParam",
802
+ param,
803
+ key: `$${param}`,
804
+ negate: false,
805
+ values: []
806
+ });
807
+ }
808
+ continue;
809
+ }
770
810
  const keyNode = filter.getChild(TagKey);
771
811
  const valueNode = filter.getChild(TagValue);
772
812
  if (!keyNode || !valueNode) continue;
773
813
  const values = [];
774
814
  for (const valueChild of valueNode.getChildren(Value)) {
815
+ const paramNode = valueChild.getChild(ParamRef);
816
+ if (paramNode) {
817
+ const wordNode = paramNode.getChild(Word);
818
+ const param = wordNode ? text.slice(wordNode.from, wordNode.to) : text.slice(paramNode.from + 1, paramNode.to);
819
+ if (param) values.push({ value: `$${param}`, wildcard: false, param });
820
+ continue;
821
+ }
775
822
  const raw = text.slice(valueChild.from, valueChild.to);
776
823
  const quoted = valueChild.getChild(Quoted) !== null;
777
824
  let value = quoted ? raw.slice(1, -1) : raw;
@@ -869,6 +916,7 @@ function parseAggregateQuery(raw, opts) {
869
916
  return base;
870
917
  } else {
871
918
  base.metric = mf.values[0].value;
919
+ if (mf.values[0].param) base.metricParam = mf.values[0].param;
872
920
  }
873
921
  } else {
874
922
  if (!stage || !head || !aggNode || !metricNode) {
@@ -882,8 +930,13 @@ function parseAggregateQuery(raw, opts) {
882
930
  }
883
931
  base.agg = aggText;
884
932
  base.metric = text.slice(metricNode.from, metricNode.to);
933
+ const metricParamNode = metricNode.getChild(ParamRef);
934
+ if (metricParamNode) {
935
+ const w = metricParamNode.getChild(Word);
936
+ base.metricParam = w ? text.slice(w.from, w.to) : base.metric.slice(1);
937
+ }
885
938
  }
886
- const contentKeys = base.filters.map((f) => f.key).filter((k) => WQL_CONTENT_ONLY_KEYS.includes(k));
939
+ const contentKeys = base.filters.filter((f) => f.kind !== "spreadParam" && !f.key.startsWith("$")).map((f) => f.key).filter((k) => WQL_CONTENT_ONLY_KEYS.includes(k));
887
940
  if (contentKeys.length > 0) {
888
941
  base.error = `Content filter key(s) ${contentKeys.map((k) => `"${k}"`).join(", ")} cannot narrow an aggregate \u2014 facts carry effort/dimension data, not note text. Aggregate dims: ${WQL_TAG_KEYS.join(", ")}, plus any custom dimension.`;
889
942
  return base;
@@ -984,7 +1037,7 @@ function parseFindQuery(raw, opts) {
984
1037
  if (scope || typeScope) {
985
1038
  result.target = "note";
986
1039
  } else {
987
- result.target = WQL_NOTE_HEAD_ALIASES.includes(headName) ? "note" : headName;
1040
+ result.target = WQL_NOTE_HEAD_ALIASES.includes(headName) ? "note" : headName === "feeds" ? "feed" : headName;
988
1041
  if (!WQL_FIND_TARGETS.includes(result.target)) {
989
1042
  result.error = `Unknown find target "${result.target}". Try: ${WQL_FIND_TARGETS.join(", ")}`;
990
1043
  return result;
@@ -1163,7 +1216,7 @@ function findTargetAdvisories(query) {
1163
1216
  const advisories = [];
1164
1217
  const target = query.target;
1165
1218
  const supported = allowedFilterTypesForTarget(target, "find");
1166
- const ignored = [...new Set(query.filters.filter((f) => !supportsWqlFilterKey(target, "find", f.key)).map((f) => f.key))];
1219
+ const ignored = [...new Set(query.filters.filter((f) => f.kind !== "spreadParam" && !f.key.startsWith("$") && !supportsWqlFilterKey(target, "find", f.key)).map((f) => f.key))];
1167
1220
  if (ignored.length) {
1168
1221
  const custom = target === "segment" || target === "event" ? ", custom fact dimensions" : "";
1169
1222
  const effortKey = ignored.find((key) => WQL_EFFORT_FILTER_KEYS.includes(key));
@@ -1236,7 +1289,14 @@ function serializeFindStage(f) {
1236
1289
  return parts.filter(Boolean).join(" ");
1237
1290
  }
1238
1291
  function serializeFunctionStage(a) {
1239
- const filters = a.metric ? [{ key: "metric", negate: false, values: [{ value: a.metric, wildcard: false }] }, ...a.filters] : a.filters;
1292
+ const filters = a.metric ? [
1293
+ {
1294
+ key: "metric",
1295
+ negate: false,
1296
+ values: [{ value: a.metric, wildcard: false, ...a.metricParam ? { param: a.metricParam } : {} }]
1297
+ },
1298
+ ...a.filters
1299
+ ] : a.filters;
1240
1300
  let text = `:${a.agg}{${serializeFilters(filters)}}`;
1241
1301
  if (a.groupBy.length) text += ` by {${a.groupBy.join(", ")}}`;
1242
1302
  if (a.rollup) text += `.rollup(${a.rollup.size}${a.rollup.unit})`;
@@ -1261,16 +1321,22 @@ function serializeValue(value) {
1261
1321
  return `"${value}"`;
1262
1322
  }
1263
1323
  function serializeFilters(filters) {
1264
- return filters.map((f) => `${f.negate ? "!" : ""}${f.key}:${f.values.map((v) => serializeValue(v.value) + (v.wildcard ? "*" : "")).join("|")}`).join(",");
1324
+ return filters.map((f) => {
1325
+ if (f.kind === "spreadParam" || f.key.startsWith("$") && f.values.length === 0) {
1326
+ const p = f.param ?? f.key.replace(/^\$/, "");
1327
+ return `$${p}`;
1328
+ }
1329
+ return `${f.negate ? "!" : ""}${f.key}:${f.values.map((v) => v.param ? `$${v.param}` : serializeValue(v.value) + (v.wildcard ? "*" : "")).join("|")}`;
1330
+ }).join(",");
1265
1331
  }
1266
1332
  function serializeAggHead(agg, metric, filters) {
1267
1333
  return `${agg}:${metric}{${serializeFilters(filters)}}`;
1268
1334
  }
1269
1335
  function serializeAggregateHead(a) {
1270
- return serializeAggHead(a.agg, a.metric, a.filters);
1336
+ return serializeAggHead(a.agg, a.metricParam ? `$${a.metricParam}` : a.metric, a.filters);
1271
1337
  }
1272
1338
  function serializeFindHalf(target, filters, last) {
1273
- const head = `find:${target}${filters.length ? `{${serializeFilters(filters)}}` : ""}`;
1339
+ const head = `:${target}${filters.length ? `{${serializeFilters(filters)}}` : ""}`;
1274
1340
  return last ? `${head} last ${last.size}${last.unit}` : head;
1275
1341
  }
1276
1342
  function serializeMetricHalf(j) {
@@ -1279,6 +1345,7 @@ function serializeMetricHalf(j) {
1279
1345
  function serializeWindow(w) {
1280
1346
  if (!w) return "";
1281
1347
  if (w.kind === "relative") return `last ${w.size}${w.unit}`;
1348
+ if (w.kind === "param") return `last $${w.param}${w.unit ? ` ${w.unit}` : ""}`;
1282
1349
  return w.end ? `from ${w.start} to ${w.end}` : `from ${w.start}`;
1283
1350
  }
1284
1351
  function serialize(parsed) {
@@ -1329,220 +1396,1615 @@ function serialize(parsed) {
1329
1396
  return unhandled.raw ?? "";
1330
1397
  }
1331
1398
 
1332
- // src/disciplines.ts
1333
- var EFFORT_DISCIPLINES = [
1334
- "bodyweight",
1335
- "cycling",
1336
- "gymnastics",
1337
- "kettlebell",
1338
- "recovery",
1339
- "rowing",
1340
- "running",
1341
- "strength",
1342
- "swimming",
1343
- "walking"
1344
- ];
1345
- function isEffortDiscipline(value) {
1346
- return typeof value === "string" && EFFORT_DISCIPLINES.includes(value.toLowerCase());
1399
+ // src/paramRef.ts
1400
+ function collectFromFilters(filters, out) {
1401
+ for (const f of filters) {
1402
+ if (f.kind === "spreadParam" || f.key.startsWith("$") && f.values.length === 0) {
1403
+ const name = f.param ?? f.key.replace(/^\$/, "");
1404
+ if (name) out.add(name);
1405
+ continue;
1406
+ }
1407
+ for (const v of f.values) {
1408
+ if (v.param) out.add(v.param);
1409
+ }
1410
+ }
1347
1411
  }
1348
- var DISCIPLINE_FACTORS = {
1349
- strength: 1.2,
1350
- kettlebell: 1.2,
1351
- gymnastics: 1.2,
1352
- recovery: 0.9,
1353
- bodyweight: 1,
1354
- cycling: 1,
1355
- rowing: 1,
1356
- running: 1,
1357
- swimming: 1,
1358
- walking: 1
1359
- };
1360
- function disciplineFactorFor(discipline) {
1361
- if (!discipline) return 1;
1362
- const key = discipline.toLowerCase();
1363
- return isEffortDiscipline(key) ? DISCIPLINE_FACTORS[key] : 1;
1412
+ function collectFromWindow(w, out) {
1413
+ if (w?.kind === "param") out.add(w.param);
1364
1414
  }
1365
- var wqlLanguage = language.LRLanguage.define({
1366
- parser: parser.configure({
1367
- props: [
1368
- highlight.styleTags({
1369
- // Rule styles color stray punctuation; the Word tokens inside carry
1370
- // the visible styling (unstyled children reset the parent class) —
1371
- // so parents with punctuation children (Head spans `:journal`) are
1372
- // NOT styled directly, or highlightTree merges the word into the
1373
- // parent span.
1374
- "Aggregator/Word": highlight.tags.keyword,
1375
- "Metric/Word": highlight.tags.variableName,
1376
- // Direct Word child of Head = the colon head name (`:sum`, `:journal`).
1377
- "Head/Word": highlight.tags.keyword,
1378
- DatasetReference: highlight.tags.keyword,
1379
- "DatasetReference/Word": highlight.tags.keyword,
1380
- At: highlight.tags.punctuation,
1381
- TagKey: highlight.tags.propertyName,
1382
- "TagKey/Word": highlight.tags.propertyName,
1383
- TagValue: highlight.tags.string,
1384
- "TagValue/Value/Word": highlight.tags.string,
1385
- "TagValue/Word": highlight.tags.string,
1386
- Negate: highlight.tags.operator,
1387
- Star: highlight.tags.operator,
1388
- Dimension: highlight.tags.attributeName,
1389
- "Dimension/Word": highlight.tags.attributeName,
1390
- By: highlight.tags.keyword,
1391
- RollupDot: highlight.tags.keyword,
1392
- Int: highlight.tags.number,
1393
- "Rollup/Word": highlight.tags.unit,
1394
- braceOpen: highlight.tags.bracket,
1395
- braceClose: highlight.tags.bracket,
1396
- parenOpen: highlight.tags.bracket,
1397
- parenClose: highlight.tags.bracket,
1398
- colon: highlight.tags.punctuation,
1399
- comma: highlight.tags.punctuation,
1400
- pipe: highlight.tags.punctuation,
1401
- dot: highlight.tags.punctuation
1402
- })
1403
- ]
1404
- }),
1405
- languageData: {
1406
- closeBrackets: { brackets: ["{", "("] }
1415
+ function collectFromSingle(q, out) {
1416
+ if (q.family === "aggregate") {
1417
+ if (q.metricParam) out.add(q.metricParam);
1418
+ collectFromFilters(q.filters, out);
1419
+ collectFromWindow(q.window, out);
1420
+ if (q.join) collectFromFilters(q.join.filters, out);
1421
+ return;
1407
1422
  }
1408
- });
1409
- function options(labels) {
1410
- return labels.map((label) => typeof label === "string" ? { label } : label);
1423
+ collectFromFilters(q.filters, out);
1424
+ collectFromWindow(q.window, out);
1425
+ if (q.join) collectFromFilters(q.join.filters, out);
1411
1426
  }
1412
- function scanContext(doc, pos) {
1413
- let depth = 0, quoted = false, pipeFrom = -1, lastBrace = -1, fragStart = 0;
1414
- for (let i = 0; i < pos; i++) {
1415
- const ch = doc[i];
1416
- if (quoted) {
1417
- if (ch === '"') quoted = false;
1427
+ function collectQueryParams(parsed) {
1428
+ const out = /* @__PURE__ */ new Set();
1429
+ if (parsed.family === "pipeline") {
1430
+ if (parsed.source.kind === "query") collectFromSingle(parsed.source.query, out);
1431
+ for (const t2 of parsed.transforms) collectFromSingle(t2, out);
1432
+ if (parsed.sink) collectFromFilters(parsed.sink.filters, out);
1433
+ return [...out];
1434
+ }
1435
+ collectFromSingle(parsed, out);
1436
+ return [...out];
1437
+ }
1438
+ function parseFilterFragment(fragment) {
1439
+ const trimmed = fragment.trim();
1440
+ if (!trimmed) return { filters: [] };
1441
+ if (trimmed.startsWith(":") || trimmed.startsWith("find:")) {
1442
+ const asQuery = parseQuery(trimmed);
1443
+ if (asQuery.error) return { filters: [], error: asQuery.error };
1444
+ if (asQuery.family === "find" || asQuery.family === "aggregate") {
1445
+ return { filters: asQuery.filters };
1446
+ }
1447
+ if (asQuery.family === "pipeline" && asQuery.source.kind === "query") {
1448
+ return { filters: asQuery.source.query.filters };
1449
+ }
1450
+ return { filters: [] };
1451
+ }
1452
+ const inner = trimmed.startsWith("{") && trimmed.endsWith("}") ? trimmed.slice(1, -1).trim() : trimmed;
1453
+ if (!inner) return { filters: [] };
1454
+ const probe = parseQuery(`:segment{${inner}}`);
1455
+ if (!probe.error && probe.family === "find") {
1456
+ return { filters: probe.filters };
1457
+ }
1458
+ const noteProbe = parseQuery(`:note{${inner}}`);
1459
+ if (!noteProbe.error && noteProbe.family === "find") {
1460
+ return { filters: noteProbe.filters };
1461
+ }
1462
+ return { filters: [], error: noteProbe.error ?? probe.error ?? `Cannot parse filter parameter "${fragment}"` };
1463
+ }
1464
+ function bindWindow(w, params) {
1465
+ if (!w || w.kind !== "param") return { window: w };
1466
+ const raw = (params[w.param] ?? "").trim();
1467
+ if (!raw || raw.toLowerCase() === "all") return { window: void 0 };
1468
+ const withUnit = w.unit && /^\d+$/.test(raw) ? `${raw}${w.unit}` : raw;
1469
+ const rel = /^(?:last\s+)?(\d+)\s*([dw])$/i.exec(withUnit);
1470
+ if (rel) {
1471
+ return {
1472
+ window: {
1473
+ kind: "relative",
1474
+ size: Number(rel[1]),
1475
+ unit: rel[2].toLowerCase()
1476
+ }
1477
+ };
1478
+ }
1479
+ const range = /^(?:from\s+)?(\d{4}-\d{2}-\d{2})(?:\s*(?:to|\.\.)\s*(\d{4}-\d{2}-\d{2}))?$/i.exec(raw);
1480
+ if (range) {
1481
+ return {
1482
+ window: {
1483
+ kind: "range",
1484
+ start: range[1],
1485
+ ...range[2] ? { end: range[2] } : {}
1486
+ }
1487
+ };
1488
+ }
1489
+ return { error: `Invalid window value "${raw}" for $${w.param} \u2014 expected <n>d|<n>w or YYYY-MM-DD[..YYYY-MM-DD]` };
1490
+ }
1491
+ function bindFilters(filters, params, opts) {
1492
+ const bound = [];
1493
+ const contentJoinFilters = [];
1494
+ for (const f of filters) {
1495
+ if (f.kind === "spreadParam" || f.key.startsWith("$") && f.values.length === 0) {
1496
+ const name = f.param ?? f.key.replace(/^\$/, "");
1497
+ const rawVal = (params[name] ?? "").trim();
1498
+ if (!rawVal) continue;
1499
+ const frag = parseFilterFragment(rawVal);
1500
+ if (frag.error) return { filters: [], contentJoinFilters: [], error: frag.error };
1501
+ for (const expanded of frag.filters) {
1502
+ if (opts.isAggregate && WQL_CONTENT_ONLY_KEYS.includes(expanded.key)) {
1503
+ contentJoinFilters.push(expanded);
1504
+ } else {
1505
+ bound.push(expanded);
1506
+ }
1507
+ }
1418
1508
  continue;
1419
1509
  }
1420
- if (ch === '"') {
1421
- quoted = true;
1510
+ if (!f.values.some((v) => v.param !== void 0)) {
1511
+ bound.push(f);
1422
1512
  continue;
1423
1513
  }
1424
- if (ch === "{") {
1425
- depth++;
1426
- lastBrace = i;
1427
- fragStart = i + 1;
1428
- } else if (ch === "}") {
1429
- depth = Math.max(0, depth - 1);
1430
- fragStart = i + 1;
1431
- } else if (ch === "," && depth > 0) fragStart = i + 1;
1432
- else if (ch === "|" && depth === 0) {
1433
- pipeFrom = i + 1;
1434
- fragStart = i + 1;
1514
+ const nextValues = [];
1515
+ for (const v of f.values) {
1516
+ if (!v.param) {
1517
+ nextValues.push(v);
1518
+ continue;
1519
+ }
1520
+ const rawVal = (params[v.param] ?? "").trim();
1521
+ if (!rawVal) continue;
1522
+ const formatted = /[\s,{}"]/.test(rawVal) && !rawVal.startsWith('"') ? `"${rawVal.replace(/"/g, "")}"` : rawVal;
1523
+ const probe = parseQuery(`:segment{${f.key}:${formatted}}`);
1524
+ if (!probe.error && probe.family === "find" && probe.filters[0]) {
1525
+ nextValues.push(...probe.filters[0].values);
1526
+ } else {
1527
+ const noteProbe = parseQuery(`:note{${f.key}:${formatted}}`);
1528
+ if (!noteProbe.error && noteProbe.family === "find" && noteProbe.filters[0]) {
1529
+ nextValues.push(...noteProbe.filters[0].values);
1530
+ } else {
1531
+ return {
1532
+ filters: [],
1533
+ contentJoinFilters: [],
1534
+ error: noteProbe.error ?? probe.error ?? `Invalid value "${rawVal}" for $${v.param}`
1535
+ };
1536
+ }
1537
+ }
1538
+ }
1539
+ if (nextValues.length > 0) {
1540
+ bound.push({ key: f.key, negate: f.negate, values: nextValues });
1435
1541
  }
1436
1542
  }
1437
- return { depth, quoted, pipeFrom, lastBrace, fragStart };
1543
+ return { filters: bound, contentJoinFilters };
1438
1544
  }
1439
- function maskQuotes(text) {
1440
- let out = "";
1441
- let quoted = false;
1442
- for (const ch of text) {
1443
- if (ch === '"') {
1444
- quoted = !quoted;
1445
- out += " ";
1446
- } else out += quoted ? " " : ch;
1545
+ function bindAggregate(q, params, opts = {}) {
1546
+ const metric = q.metricParam ? (params[q.metricParam] ?? "").trim() : q.metric;
1547
+ if (q.metricParam && !metric && q.agg !== "count") {
1548
+ return { ...q, error: `Parameter $${q.metricParam} resolved to an empty metric` };
1447
1549
  }
1448
- return out;
1449
- }
1450
- function unquotedColonIndex(text) {
1451
- let quoted = false;
1452
- for (let i = 0; i < text.length; i++) {
1453
- const ch = text[i];
1454
- if (ch === '"') quoted = !quoted;
1455
- else if (ch === ":" && !quoted) return i;
1550
+ const win = bindWindow(q.window, params);
1551
+ if (win.error) return { ...q, error: win.error };
1552
+ const fRes = bindFilters(q.filters, params, { isAggregate: true });
1553
+ if (fRes.error) return { ...q, error: fRes.error };
1554
+ let join = q.join;
1555
+ if (join) {
1556
+ const jRes = bindFilters(join.filters, params, { isAggregate: false });
1557
+ if (jRes.error) return { ...q, error: jRes.error };
1558
+ join = { ...join, filters: jRes.filters };
1456
1559
  }
1457
- return -1;
1458
- }
1459
- function usedEntries(doc, groupStart, fragStart) {
1460
- const text = maskQuotes(doc.slice(groupStart + 1, fragStart));
1461
- const used = /* @__PURE__ */ new Set();
1462
- for (const entry of text.split(",")) {
1463
- const m = /!?\s*([A-Za-z][\w-]*)/.exec(entry);
1464
- if (m) used.add(m[1]);
1560
+ if (fRes.contentJoinFilters.length > 0 && !opts.inPipelineTransform) {
1561
+ join = join ? { ...join, filters: [...join.filters, ...fRes.contentJoinFilters] } : { target: "note", filters: fRes.contentJoinFilters };
1465
1562
  }
1466
- return used;
1467
- }
1468
- function valueOf(c) {
1469
- return typeof c.apply === "string" ? c.apply : c.label;
1470
- }
1471
- function quoteApply(c) {
1472
- const value = valueOf(c);
1473
- if (!/\s/.test(value)) return c;
1474
1563
  return {
1475
- ...c,
1476
- apply: (view, _c, from, to) => {
1477
- const text = view.state.sliceDoc(Math.max(0, from - 1), from) === '"' ? value : `"${value}"`;
1478
- view.dispatch({ changes: { from, to, insert: text }, selection: { anchor: from + text.length }, userEvent: "input.complete" });
1479
- }
1564
+ ...q,
1565
+ metric,
1566
+ metricParam: void 0,
1567
+ filters: fRes.filters,
1568
+ window: win.window,
1569
+ ...join ? { join } : { join: void 0 }
1480
1570
  };
1481
1571
  }
1482
- function mergeValues(host, statics) {
1483
- const seen = /* @__PURE__ */ new Set();
1484
- const out = [];
1485
- host.forEach((c, i) => {
1486
- const k = valueOf(c).toLowerCase();
1487
- if (!k || seen.has(k)) return;
1488
- seen.add(k);
1489
- out.push(quoteApply({ ...c, boost: Math.max(-99, Math.min(99, 80 - i)) }));
1490
- });
1491
- for (const c of statics) {
1492
- const k = valueOf(c).toLowerCase();
1493
- if (!k || seen.has(k)) continue;
1494
- seen.add(k);
1495
- out.push(quoteApply(c));
1496
- }
1497
- return out;
1498
- }
1499
- function narrow(options2, typed) {
1500
- const q = typed.toLowerCase();
1501
- if (!q) return [...options2];
1502
- const prefix = [];
1503
- const rest = [];
1504
- for (const o of options2) {
1505
- const label = o.label.toLowerCase();
1506
- if (label.startsWith(q)) prefix.push(o);
1507
- else if (label.includes(q)) rest.push(o);
1572
+ function bindFind(q, params) {
1573
+ const win = bindWindow(q.window, params);
1574
+ if (win.error) return { ...q, error: win.error };
1575
+ const fRes = bindFilters(q.filters, params, { isAggregate: false });
1576
+ if (fRes.error) return { ...q, error: fRes.error };
1577
+ let join = q.join;
1578
+ if (join) {
1579
+ const jRes = bindFilters(join.filters, params, { isAggregate: true });
1580
+ if (jRes.error) return { ...q, error: jRes.error };
1581
+ join = { ...join, filters: jRes.filters };
1508
1582
  }
1509
- return [...prefix, ...rest];
1510
- }
1511
- function finish(_context, from, options2, typed) {
1512
- const ranked = narrow(options2, typed);
1513
- if (!ranked.length) return null;
1514
1583
  return {
1515
- from,
1516
- options: ranked.map((o, i) => o.boost === void 0 ? { ...o, boost: Math.max(-99, 50 - i) } : o),
1517
- filter: false
1584
+ ...q,
1585
+ filters: fRes.filters,
1586
+ window: win.window,
1587
+ ...join ? { join } : { join: void 0 }
1518
1588
  };
1519
1589
  }
1520
- function chained(list, suffix) {
1521
- return list.map((c) => ({
1522
- ...c,
1523
- apply: (view, _c, from, to) => {
1524
- const text = typeof c.apply === "string" ? c.apply : c.label;
1525
- const selfTerminated = /\W$/.test(text);
1526
- const present = view.state.sliceDoc(to, to + suffix.length) === suffix;
1527
- const insert = selfTerminated || present ? text : text + suffix;
1528
- view.dispatch({
1529
- changes: { from, to: to + (selfTerminated && present ? suffix.length : 0), insert },
1530
- selection: { anchor: from + insert.length },
1531
- userEvent: "input.complete"
1532
- });
1533
- autocomplete.startCompletion(view);
1590
+ function bindQueryParams(parsed, params) {
1591
+ if (parsed.error) return { parsed, missing: [] };
1592
+ const referenced = collectQueryParams(parsed);
1593
+ if (referenced.length === 0) return { parsed, missing: [] };
1594
+ const missing = referenced.filter((name) => params[name] === void 0);
1595
+ if (missing.length > 0) return { parsed, missing };
1596
+ let bound;
1597
+ if (parsed.family === "aggregate") {
1598
+ bound = bindAggregate(parsed, params);
1599
+ } else if (parsed.family === "find") {
1600
+ bound = bindFind(parsed, params);
1601
+ } else {
1602
+ let source = parsed.source;
1603
+ let contentJoinForSource = [];
1604
+ if (source.kind === "query") {
1605
+ const sq = source.query.family === "aggregate" ? bindAggregate(source.query, params) : bindFind(source.query, params);
1606
+ if (sq.error) return { parsed: { ...parsed, error: sq.error }, missing: [] };
1607
+ source = { kind: "query", query: sq };
1608
+ }
1609
+ const transforms = [];
1610
+ for (const t2 of parsed.transforms) {
1611
+ const rawFilterBind = bindFilters(t2.filters, params, { isAggregate: true });
1612
+ if (rawFilterBind.contentJoinFilters.length > 0) {
1613
+ contentJoinForSource = [...contentJoinForSource, ...rawFilterBind.contentJoinFilters];
1614
+ }
1615
+ const bt = bindAggregate(t2, params, { inPipelineTransform: true });
1616
+ if (bt.error) return { parsed: { ...parsed, error: bt.error }, missing: [] };
1617
+ transforms.push(bt);
1618
+ }
1619
+ if (contentJoinForSource.length > 0 && source.kind === "query" && source.query.family === "find") {
1620
+ if (source.query.target === "note" || source.query.target === "block") {
1621
+ source = {
1622
+ kind: "query",
1623
+ query: { ...source.query, filters: [...source.query.filters, ...contentJoinForSource] }
1624
+ };
1625
+ }
1534
1626
  }
1535
- }));
1627
+ let sink = parsed.sink;
1628
+ if (sink) {
1629
+ const sRes = bindFilters(sink.filters, params, { isAggregate: false });
1630
+ if (sRes.error) return { parsed: { ...parsed, error: sRes.error }, missing: [] };
1631
+ sink = { ...sink, filters: sRes.filters };
1632
+ }
1633
+ bound = {
1634
+ ...parsed,
1635
+ source,
1636
+ transforms,
1637
+ ...sink ? { sink } : {}
1638
+ };
1639
+ }
1640
+ if (bound.error) return { parsed: bound, missing: [] };
1641
+ const reparsed = parseQuery(serialize(bound));
1642
+ return { parsed: reparsed, missing: [] };
1536
1643
  }
1537
- var HEAD_OPTIONS = [
1538
- ...WQL_FUNCTION_HEADS.map((label) => ({ label, detail: "function head", type: "keyword" })),
1539
- ...WQL_SOURCE_HEADS.map((label) => ({ label, detail: "source head", type: "keyword" })),
1540
- ...WQL_CHART_HEADS.map((label) => ({ label, detail: "chart head", type: "class" })),
1541
- ...WQL_STANDARD_DATASETS.map((label) => ({ label, detail: "page dataset", type: "class" }))
1542
- ];
1543
- function headApply(o, colonTyped) {
1544
- if (o.label.startsWith("@")) return { ...o, apply: o.label };
1545
- return { ...o, apply: `${colonTyped ? "" : ":"}${o.label}{` };
1644
+
1645
+ // src/document.ts
1646
+ var FORMULA_CHARS = /^[\w$\s.+\-*/(),]+$/;
1647
+ function referencesOf(expression, known) {
1648
+ const refs = [];
1649
+ for (const match of expression.matchAll(/[A-Za-z_][\w-]*/g)) {
1650
+ const name = match[0];
1651
+ if (known.has(name) && !refs.includes(name)) refs.push(name);
1652
+ }
1653
+ return refs;
1654
+ }
1655
+ function parseDocument(text) {
1656
+ const diagnostics = [];
1657
+ const lines = text.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("//"));
1658
+ const defaults = {};
1659
+ let hasDefaults = false;
1660
+ const assignments = [];
1661
+ let show;
1662
+ let showChart;
1663
+ let degenerate = false;
1664
+ const known = /* @__PURE__ */ new Set();
1665
+ for (const line of lines) {
1666
+ if (line.startsWith("defaults ")) {
1667
+ hasDefaults = true;
1668
+ const rest = line.slice("defaults ".length).trim();
1669
+ const byMatch = /by\s*\{([^}]*)\}/.exec(rest);
1670
+ if (byMatch) {
1671
+ defaults.groupBy = byMatch[1].split(",").map((d) => d.trim()).filter(Boolean);
1672
+ }
1673
+ const lastParamMatch = /last\s+\$([a-zA-Z_][\w-]*)(?:\s*(d|w))?/.exec(rest);
1674
+ const lastMatch = !lastParamMatch ? /last\s+(\d+)\s*(d|w)/.exec(rest) : null;
1675
+ if (lastParamMatch) {
1676
+ defaults.window = {
1677
+ kind: "param",
1678
+ param: lastParamMatch[1],
1679
+ ...lastParamMatch[2] ? { unit: lastParamMatch[2] } : {}
1680
+ };
1681
+ } else if (lastMatch) {
1682
+ defaults.window = { kind: "relative", size: Number(lastMatch[1]), unit: lastMatch[2] };
1683
+ }
1684
+ const fromMatch = /from\s+(\d{4}-\d{2}-\d{2})(?:\s+to\s+(\d{4}-\d{2}-\d{2}))?/.exec(rest);
1685
+ if (fromMatch) {
1686
+ defaults.window = { kind: "range", start: fromMatch[1], end: fromMatch[2] };
1687
+ }
1688
+ continue;
1689
+ }
1690
+ if (line.startsWith("show ")) {
1691
+ const rest = line.slice("show ".length).trim();
1692
+ const pipeIdx = rest.indexOf("|");
1693
+ const namesPart = pipeIdx === -1 ? rest : rest.slice(0, pipeIdx).trim();
1694
+ show = namesPart.split(",").map((s) => s.trim()).filter(Boolean);
1695
+ if (pipeIdx !== -1) {
1696
+ const sinkText = rest.slice(pipeIdx + 1).trim();
1697
+ const sinkParsed = parseQuery(`:segment{} | ${sinkText}`);
1698
+ if (!sinkParsed.error && sinkParsed.family === "pipeline" && sinkParsed.sink && sinkParsed.transforms.length === 0) {
1699
+ showChart = sinkParsed.sink;
1700
+ } else {
1701
+ diagnostics.push(`Invalid chart sink on show: "${sinkText}"`);
1702
+ }
1703
+ }
1704
+ continue;
1705
+ }
1706
+ const assign = /^([A-Za-z_][\w-]*)\s*=\s*(.+)$/.exec(line);
1707
+ if (assign) {
1708
+ const name = assign[1];
1709
+ const body = assign[2].trim();
1710
+ known.add(name);
1711
+ const unitMatch = /->\s*([\w/%.]+)\s*$/.exec(body);
1712
+ const normalizeMatch = /\(normalize\s+([^)]+)\)/.exec(body);
1713
+ const expression = body.replace(/->\s*[\w/%.]+\s*$/, "").replace(/\(normalize\s+[^)]+\)/, "").trim();
1714
+ const looksLikeQuery = /^(sum|avg|min|max|count|last|delta|find|rows)\b|^[:@|]/.test(expression);
1715
+ if (looksLikeQuery) {
1716
+ const parsed = parseQuery(expression);
1717
+ if (!parsed.error) {
1718
+ assignments.push({ name, kind: "query", queryText: expression, parsed, raw: line });
1719
+ } else {
1720
+ diagnostics.push(`Assignment "${name}" is not a valid query`);
1721
+ }
1722
+ continue;
1723
+ }
1724
+ const refPipe = /^([A-Za-z_][\w-]*)\s*\|\s*(.+)$/.exec(expression);
1725
+ if (refPipe) {
1726
+ const sourceRef = refPipe[1];
1727
+ const pipeTail = refPipe[2].trim();
1728
+ const synthetic = parseQuery(`:segment{} | ${pipeTail}`);
1729
+ if (!synthetic.error && synthetic.family === "pipeline") {
1730
+ assignments.push({
1731
+ name,
1732
+ kind: "query",
1733
+ sourceRef,
1734
+ pipeTail,
1735
+ transformsText: pipeTail,
1736
+ queryText: expression,
1737
+ parsed: synthetic,
1738
+ raw: line
1739
+ });
1740
+ } else {
1741
+ diagnostics.push(`Assignment "${name}" is not a valid pipeline over "${sourceRef}"`);
1742
+ }
1743
+ continue;
1744
+ }
1745
+ if (!FORMULA_CHARS.test(expression)) {
1746
+ diagnostics.push(`Assignment "${name}" has characters a formula cannot parse`);
1747
+ continue;
1748
+ }
1749
+ const formula = {
1750
+ expression,
1751
+ references: [],
1752
+ ...normalizeMatch ? { normalize: normalizeMatch[1].trim() } : {},
1753
+ ...unitMatch ? { unit: unitMatch[1] } : {}
1754
+ };
1755
+ assignments.push({ name, kind: "formula", formula, raw: line });
1756
+ continue;
1757
+ }
1758
+ degenerate = true;
1759
+ }
1760
+ if (!hasDefaults && show === void 0 && !degenerate && assignments.length === 1 && assignments[0].kind === "query") ;
1761
+ if (!hasDefaults && show === void 0 && assignments.length === 0) {
1762
+ const parsed = parseQuery(lines.join(" "));
1763
+ if (!parsed.error) {
1764
+ known.add("query");
1765
+ assignments.push({ name: "query", kind: "query", queryText: lines.join(" "), parsed, raw: lines.join(" ") });
1766
+ show = ["query"];
1767
+ degenerate = true;
1768
+ } else {
1769
+ diagnostics.push("Body is neither a valid query nor a valid document");
1770
+ }
1771
+ }
1772
+ for (const assignment of assignments) {
1773
+ if (assignment.sourceRef && !known.has(assignment.sourceRef)) {
1774
+ diagnostics.push(`Assignment "${assignment.name}" references unknown source "${assignment.sourceRef}"`);
1775
+ }
1776
+ if (assignment.kind !== "formula" || !assignment.formula) continue;
1777
+ assignment.formula.references = referencesOf(assignment.formula.expression, known);
1778
+ for (const match of assignment.formula.expression.matchAll(/[A-Za-z_][\w-]*/g)) {
1779
+ const name = match[0];
1780
+ if (!known.has(name)) {
1781
+ diagnostics.push(`Formula "${assignment.name}" references unknown name "${name}"`);
1782
+ }
1783
+ }
1784
+ if (assignment.formula.references.length === 0) {
1785
+ diagnostics.push(`Formula "${assignment.name}" references no named query or formula`);
1786
+ }
1787
+ }
1788
+ const cycle = findCycle(assignments);
1789
+ if (cycle) {
1790
+ diagnostics.push(`Reference cycle detected: ${cycle.join(" -> ")}`);
1791
+ }
1792
+ if (show === void 0) {
1793
+ show = assignments.filter((a) => a.kind === "query").map((a) => a.name);
1794
+ if (assignments.length > 0 && show.length === 0) show = [assignments[0].name];
1795
+ } else {
1796
+ for (const name of show) {
1797
+ if (!known.has(name)) diagnostics.push(`show references unknown name "${name}"`);
1798
+ }
1799
+ }
1800
+ return {
1801
+ doc: {
1802
+ ...Object.keys(defaults).length > 0 ? { defaults } : {},
1803
+ assignments,
1804
+ show,
1805
+ ...showChart ? { showChart } : {},
1806
+ degenerate
1807
+ },
1808
+ diagnostics
1809
+ };
1810
+ }
1811
+ function findCycle(assignments) {
1812
+ const deps = /* @__PURE__ */ new Map();
1813
+ for (const a of assignments) {
1814
+ if (a.kind === "formula" && a.formula) deps.set(a.name, a.formula.references);
1815
+ else if (a.sourceRef) deps.set(a.name, [a.sourceRef]);
1816
+ }
1817
+ const state = /* @__PURE__ */ new Map();
1818
+ const stack = [];
1819
+ const visit = (name) => {
1820
+ const s = state.get(name);
1821
+ if (s === 2) return void 0;
1822
+ if (s === 1) {
1823
+ const start = stack.indexOf(name);
1824
+ return [...stack.slice(start === -1 ? 0 : start), name];
1825
+ }
1826
+ state.set(name, 1);
1827
+ stack.push(name);
1828
+ for (const ref of deps.get(name) ?? []) {
1829
+ const cycle = visit(ref);
1830
+ if (cycle) return cycle;
1831
+ }
1832
+ stack.pop();
1833
+ state.set(name, 2);
1834
+ return void 0;
1835
+ };
1836
+ for (const name of deps.keys()) {
1837
+ const cycle = visit(name);
1838
+ if (cycle) return cycle;
1839
+ }
1840
+ return void 0;
1841
+ }
1842
+ function serializeDocument(doc) {
1843
+ if (doc.degenerate) {
1844
+ const only = doc.assignments[0];
1845
+ return only?.queryText ?? "";
1846
+ }
1847
+ const lines = [];
1848
+ if (doc.defaults) {
1849
+ const parts = [];
1850
+ if (doc.defaults.groupBy?.length) parts.push(`by {${doc.defaults.groupBy.join(", ")}}`);
1851
+ if (doc.defaults.window) {
1852
+ const w = doc.defaults.window;
1853
+ parts.push(
1854
+ w.kind === "relative" ? `last ${w.size}${w.unit}` : w.kind === "param" ? `last $${w.param}${w.unit ? ` ${w.unit}` : ""}` : `from ${w.start}${w.end ? ` to ${w.end}` : ""}`
1855
+ );
1856
+ }
1857
+ if (parts.length > 0) lines.push(`defaults ${parts.join(" ")}`);
1858
+ }
1859
+ for (const a of doc.assignments) {
1860
+ lines.push(a.raw);
1861
+ }
1862
+ if (doc.show.length > 0) {
1863
+ let showLine = `show ${doc.show.join(", ")}`;
1864
+ if (doc.showChart) {
1865
+ const sinkSerialized = serialize({
1866
+ family: "pipeline",
1867
+ raw: "",
1868
+ source: { kind: "dataset", name: "@session" },
1869
+ transforms: [],
1870
+ sink: doc.showChart
1871
+ }).replace(/^@session\s*\|\s*/, "");
1872
+ showLine += ` | ${sinkSerialized}`;
1873
+ }
1874
+ lines.push(showLine);
1875
+ }
1876
+ return lines.join("\n");
1877
+ }
1878
+
1879
+ // src/dashboard/fenceSuffix.ts
1880
+ var DASHBOARD_WIDGET_TYPES = [
1881
+ "table",
1882
+ "list",
1883
+ "value",
1884
+ "timeseries",
1885
+ "bar",
1886
+ "donut",
1887
+ "toplist",
1888
+ "stacked-bar",
1889
+ "goal-rings",
1890
+ "zone-distribution"
1891
+ ];
1892
+ function isDashboardWidgetType(type) {
1893
+ return DASHBOARD_WIDGET_TYPES.includes(type);
1894
+ }
1895
+ var DASHBOARD_GRID_MAX_COLS = 4;
1896
+ function parseQueryWidgetSuffix(suffix) {
1897
+ if (suffix === "") {
1898
+ return { type: "", attributes: {}, attributeOrder: [], error: 'missing widget type after "query:"' };
1899
+ }
1900
+ const attributes = {};
1901
+ const attributeOrder = [];
1902
+ const trimmedSuffix = suffix.trim();
1903
+ const headEnd = trimmedSuffix.search(/\s/);
1904
+ const head = headEnd === -1 ? trimmedSuffix : trimmedSuffix.slice(0, headEnd);
1905
+ const attrTail = headEnd === -1 ? "" : trimmedSuffix.slice(headEnd);
1906
+ const attrPattern = /([a-zA-Z][a-zA-Z0-9_-]*)=("([^"]*)"|\S+)/g;
1907
+ for (const match of attrTail.matchAll(attrPattern)) {
1908
+ const key = match[1].toLowerCase();
1909
+ const value = match[3] !== void 0 ? match[3] : match[2];
1910
+ if (key in attributes) {
1911
+ return { type: head, attributes, attributeOrder, error: `duplicate attribute "${key}"` };
1912
+ }
1913
+ attributes[key] = value;
1914
+ attributeOrder.push(key);
1915
+ }
1916
+ const leftover = attrTail.replace(attrPattern, "").trim();
1917
+ if (leftover !== "") {
1918
+ return { type: head, attributes, attributeOrder, error: `malformed attribute "${leftover.split(/\s+/)[0]}"` };
1919
+ }
1920
+ let rest = head.toLowerCase();
1921
+ let spanCols;
1922
+ let spanFull;
1923
+ if (rest.endsWith("-full")) {
1924
+ spanFull = true;
1925
+ rest = rest.slice(0, -"-full".length);
1926
+ } else {
1927
+ const spanMatch = rest.match(/-(\d+)$/);
1928
+ if (spanMatch) {
1929
+ spanCols = Number(spanMatch[1]);
1930
+ rest = rest.slice(0, -spanMatch[0].length);
1931
+ }
1932
+ }
1933
+ if (rest === "" || !/^[a-z][a-z0-9-]*$/.test(rest)) {
1934
+ return { type: rest, spanCols, spanFull, attributes, attributeOrder, error: `malformed widget type "${suffix}"` };
1935
+ }
1936
+ if (/-(\d+|full)$/.test(rest)) {
1937
+ return { type: rest, spanCols, spanFull, attributes, attributeOrder, error: `malformed widget suffix "${suffix}"` };
1938
+ }
1939
+ if (spanCols != null && (spanCols < 1 || spanCols > DASHBOARD_GRID_MAX_COLS)) {
1940
+ return { type: rest, spanCols, spanFull, attributes, attributeOrder, error: `span ${spanCols} outside 1..${DASHBOARD_GRID_MAX_COLS}` };
1941
+ }
1942
+ return { type: rest, spanCols, spanFull, attributes, attributeOrder };
1943
+ }
1944
+
1945
+ // src/queryBlockCodec.ts
1946
+ var KNOWN_YAML_KEYS = /* @__PURE__ */ new Set([
1947
+ "title",
1948
+ "description",
1949
+ "question",
1950
+ "view",
1951
+ "type",
1952
+ "span",
1953
+ "unit",
1954
+ "window",
1955
+ "groupBy",
1956
+ "query",
1957
+ "series",
1958
+ "columns",
1959
+ "sort",
1960
+ "limit",
1961
+ "goal",
1962
+ "targets",
1963
+ "comparePr"
1964
+ ]);
1965
+ function unquoteYaml(raw) {
1966
+ const t2 = raw.trim();
1967
+ if (t2.length >= 2 && t2.startsWith('"') && t2.endsWith('"')) {
1968
+ return t2.slice(1, -1).replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
1969
+ }
1970
+ if (t2.length >= 2 && t2.startsWith("'") && t2.endsWith("'")) {
1971
+ return t2.slice(1, -1).replace(/''/g, "'");
1972
+ }
1973
+ return t2;
1974
+ }
1975
+ function stripInlineYamlComment(line) {
1976
+ let inSingle = false;
1977
+ let inDouble = false;
1978
+ for (let i = 0; i < line.length; i++) {
1979
+ const ch = line[i];
1980
+ if (inDouble) {
1981
+ if (ch === "\\") i++;
1982
+ else if (ch === '"') inDouble = false;
1983
+ continue;
1984
+ }
1985
+ if (inSingle) {
1986
+ if (ch === "'") inSingle = false;
1987
+ continue;
1988
+ }
1989
+ if (ch === '"') inDouble = true;
1990
+ else if (ch === "'") inSingle = true;
1991
+ else if (ch === "#" && (i === 0 || /\s/.test(line[i - 1]))) {
1992
+ return line.slice(0, i).trimEnd();
1993
+ }
1994
+ }
1995
+ return line;
1996
+ }
1997
+ function quoteYamlString(value, opts = {}) {
1998
+ if (value === "") return '""';
1999
+ if (opts.allowWqlBare && value === value.trim() && !/^[{["'&*!|>%@`#-]/.test(value) && !value.includes(": ") && !value.includes(" #") && !value.includes("\n")) {
2000
+ return value;
2001
+ }
2002
+ const looksNumeric = /^-?\d+(?:\.\d+)?$/.test(value);
2003
+ const looksKeyword = /^(true|false|null|yes|no|on|off)$/i.test(value);
2004
+ if (/^[$a-zA-Z0-9_./-]+$/.test(value) && !value.startsWith("-") && !looksNumeric && !looksKeyword) {
2005
+ return value;
2006
+ }
2007
+ if (!/[:"'\n#{}[\],&*?|<>=!%@`]/.test(value) && value === value.trim() && !value.startsWith("-") && !looksNumeric && !looksKeyword) {
2008
+ return value;
2009
+ }
2010
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t")}"`;
2011
+ }
2012
+ function splitFlowList(inner) {
2013
+ const out = [];
2014
+ let current = "";
2015
+ let inSingle = false;
2016
+ let inDouble = false;
2017
+ for (let i = 0; i < inner.length; i++) {
2018
+ const ch = inner[i];
2019
+ if (inDouble) {
2020
+ if (ch === "\\" && i + 1 < inner.length) {
2021
+ current += ch + inner[i + 1];
2022
+ i++;
2023
+ continue;
2024
+ }
2025
+ if (ch === '"') inDouble = false;
2026
+ current += ch;
2027
+ continue;
2028
+ }
2029
+ if (inSingle) {
2030
+ if (ch === "'") inSingle = false;
2031
+ current += ch;
2032
+ continue;
2033
+ }
2034
+ if (ch === '"') {
2035
+ inDouble = true;
2036
+ current += ch;
2037
+ continue;
2038
+ }
2039
+ if (ch === "'") {
2040
+ inSingle = true;
2041
+ current += ch;
2042
+ continue;
2043
+ }
2044
+ if (ch === ",") {
2045
+ if (current.trim()) out.push(unquoteYaml(current));
2046
+ current = "";
2047
+ continue;
2048
+ }
2049
+ current += ch;
2050
+ }
2051
+ if (current.trim()) out.push(unquoteYaml(current));
2052
+ return out;
2053
+ }
2054
+ function isYamlQueryBlockBody(rawBody) {
2055
+ const lines = rawBody.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#") && !l.startsWith("//"));
2056
+ if (lines.length === 0) return false;
2057
+ const first = lines[0];
2058
+ if (first.startsWith(":") || first.startsWith("@")) return false;
2059
+ const m = /^([a-zA-Z][a-zA-Z0-9_]*)\s*:(?:\s+(.*))?$/.exec(first);
2060
+ if (!m) return false;
2061
+ return KNOWN_YAML_KEYS.has(m[1]);
2062
+ }
2063
+ function parseScalarOrParam(raw) {
2064
+ const clean = unquoteYaml(raw).trim();
2065
+ if (!clean) return void 0;
2066
+ if (clean.startsWith("$") && clean.length > 1) {
2067
+ return { param: clean.slice(1) };
2068
+ }
2069
+ const num = Number(clean);
2070
+ if (!Number.isNaN(num)) return num;
2071
+ return void 0;
2072
+ }
2073
+ function formatScalarOrParam(val) {
2074
+ return typeof val === "number" ? String(val) : `$${val.param}`;
2075
+ }
2076
+ function parseYamlWindow(raw) {
2077
+ const clean = unquoteYaml(raw).trim();
2078
+ if (!clean || clean.toLowerCase() === "all") return {};
2079
+ if (clean.startsWith("$") && clean.length > 1) {
2080
+ const m = /^\$([a-zA-Z_][\w-]*)(?:\s*([dw]))?$/i.exec(clean);
2081
+ if (m) {
2082
+ return {
2083
+ window: {
2084
+ kind: "param",
2085
+ param: m[1],
2086
+ ...m[2] ? { unit: m[2].toLowerCase() } : {}
2087
+ }
2088
+ };
2089
+ }
2090
+ }
2091
+ const relParam = /^last\s+\$([a-zA-Z_][\w-]*)(?:\s*([dw]))?$/i.exec(clean);
2092
+ if (relParam) {
2093
+ return {
2094
+ window: {
2095
+ kind: "param",
2096
+ param: relParam[1],
2097
+ ...relParam[2] ? { unit: relParam[2].toLowerCase() } : {}
2098
+ }
2099
+ };
2100
+ }
2101
+ const rel = /^(?:last\s+)?(\d+)\s*([dw])$/i.exec(clean);
2102
+ if (rel) {
2103
+ return {
2104
+ window: {
2105
+ kind: "relative",
2106
+ size: Number(rel[1]),
2107
+ unit: rel[2].toLowerCase()
2108
+ }
2109
+ };
2110
+ }
2111
+ const range = /^(?:from\s+)?(\d{4}-\d{2}-\d{2})(?:\s*(?:to|\.\.)\s*(\d{4}-\d{2}-\d{2}))?$/i.exec(clean);
2112
+ if (range) {
2113
+ return {
2114
+ window: {
2115
+ kind: "range",
2116
+ start: range[1],
2117
+ ...range[2] ? { end: range[2] } : {}
2118
+ }
2119
+ };
2120
+ }
2121
+ return { error: `Invalid window "${clean}" \u2014 expected 8w, 14d, $range, or YYYY-MM-DD..YYYY-MM-DD` };
2122
+ }
2123
+ function formatYamlWindow(w) {
2124
+ if (w.kind === "relative") return `${w.size}${w.unit}`;
2125
+ if (w.kind === "param") return w.unit ? `$${w.param} ${w.unit}` : `$${w.param}`;
2126
+ return w.end ? `${w.start}..${w.end}` : `from ${w.start}`;
2127
+ }
2128
+ function parseSortItem(raw) {
2129
+ const clean = unquoteYaml(raw).trim();
2130
+ if (!clean) return void 0;
2131
+ const m = /^(\S+)(?:\s+(asc|desc))?$/i.exec(clean);
2132
+ if (!m) return void 0;
2133
+ return { col: m[1], dir: m[2]?.toLowerCase() ?? "asc" };
2134
+ }
2135
+ function parseIndentedYamlMap(rawBody) {
2136
+ const scalars = {};
2137
+ const stringLists = {};
2138
+ const objectLists = {};
2139
+ const rawLines = rawBody.split(/\r?\n/);
2140
+ let i = 0;
2141
+ while (i < rawLines.length) {
2142
+ const cleaned = stripInlineYamlComment(rawLines[i]);
2143
+ const trimmed = cleaned.trim();
2144
+ if (!trimmed || trimmed.startsWith("//")) {
2145
+ i++;
2146
+ continue;
2147
+ }
2148
+ const indent = cleaned.length - cleaned.trimStart().length;
2149
+ if (indent !== 0) {
2150
+ return { data: { scalars, stringLists, objectLists }, error: `Unexpected indentation on line ${i + 1}: "${trimmed}"` };
2151
+ }
2152
+ const m = /^([a-zA-Z][a-zA-Z0-9_]*)\s*:(?:\s*(.*))?$/.exec(trimmed);
2153
+ if (!m) {
2154
+ return { data: { scalars, stringLists, objectLists }, error: `Malformed YAML property on line ${i + 1}: "${trimmed}"` };
2155
+ }
2156
+ const key = m[1];
2157
+ const rest = (m[2] ?? "").trim();
2158
+ if (rest !== "") {
2159
+ if (rest.startsWith("[") && rest.endsWith("]")) {
2160
+ stringLists[key] = splitFlowList(rest.slice(1, -1));
2161
+ } else {
2162
+ scalars[key] = unquoteYaml(rest);
2163
+ }
2164
+ i++;
2165
+ continue;
2166
+ }
2167
+ i++;
2168
+ const strItems = [];
2169
+ const objItems = [];
2170
+ let isObjList = false;
2171
+ while (i < rawLines.length) {
2172
+ const childClean = stripInlineYamlComment(rawLines[i]);
2173
+ const childTrim = childClean.trim();
2174
+ if (!childTrim || childTrim.startsWith("//")) {
2175
+ i++;
2176
+ continue;
2177
+ }
2178
+ const childIndent = childClean.length - childClean.trimStart().length;
2179
+ if (childIndent === 0) break;
2180
+ if (childTrim.startsWith("- ")) {
2181
+ const afterDash = childTrim.slice(2).trim();
2182
+ const kvMatch = /^([a-zA-Z][a-zA-Z0-9_]*)\s*:\s*(.*)$/.exec(afterDash);
2183
+ if (kvMatch) {
2184
+ isObjList = true;
2185
+ const itemObj = {
2186
+ [kvMatch[1]]: unquoteYaml(kvMatch[2] ?? "")
2187
+ };
2188
+ const itemIndent = childIndent;
2189
+ i++;
2190
+ while (i < rawLines.length) {
2191
+ const propClean = stripInlineYamlComment(rawLines[i]);
2192
+ const propTrim = propClean.trim();
2193
+ if (!propTrim || propTrim.startsWith("//")) {
2194
+ i++;
2195
+ continue;
2196
+ }
2197
+ const propIndent = propClean.length - propClean.trimStart().length;
2198
+ if (propIndent <= itemIndent) break;
2199
+ const propMatch = /^([a-zA-Z][a-zA-Z0-9_]*)\s*:\s*(.*)$/.exec(propTrim);
2200
+ if (propMatch) {
2201
+ itemObj[propMatch[1]] = unquoteYaml(propMatch[2] ?? "");
2202
+ }
2203
+ i++;
2204
+ }
2205
+ objItems.push(itemObj);
2206
+ continue;
2207
+ }
2208
+ strItems.push(unquoteYaml(afterDash));
2209
+ i++;
2210
+ continue;
2211
+ }
2212
+ i++;
2213
+ }
2214
+ if (isObjList) objectLists[key] = objItems;
2215
+ else stringLists[key] = strItems;
2216
+ }
2217
+ return { data: { scalars, stringLists, objectLists } };
2218
+ }
2219
+ function parseTargetsString(raw) {
2220
+ return raw.split(/[\s,]+/).map((tok) => parseScalarOrParam(tok)).filter((v) => v !== void 0);
2221
+ }
2222
+ function normalizeSingleWqlForObject(rawWql) {
2223
+ const trimmed = rawWql.trim();
2224
+ const parsed = parseQuery(trimmed);
2225
+ if (parsed.error) return { query: trimmed };
2226
+ if (isFindQuery(parsed) && parsed.pipes) {
2227
+ const columns = parsed.pipes.select?.map((s) => s.unit ? `${s.col} in ${s.unit}` : s.col);
2228
+ const sort = parsed.pipes.order?.map((o) => ({ col: o.col, dir: o.dir }));
2229
+ const limit = parsed.pipes.limit;
2230
+ const stripped = { ...parsed, pipes: void 0 };
2231
+ return {
2232
+ query: serialize(stripped),
2233
+ ...columns?.length ? { columns } : {},
2234
+ ...sort?.length ? { sort } : {},
2235
+ ...limit !== void 0 ? { limit } : {}
2236
+ };
2237
+ }
2238
+ if (!isPipelineQuery(parsed)) {
2239
+ return { query: trimmed };
2240
+ }
2241
+ let sinkView;
2242
+ let sinkGoal;
2243
+ let sinkTargets;
2244
+ if (parsed.sink) {
2245
+ if (isDashboardWidgetType(parsed.sink.head)) {
2246
+ sinkView = parsed.sink.head;
2247
+ }
2248
+ for (const f of parsed.sink.filters) {
2249
+ if (f.key === "goal" && f.values[0]) {
2250
+ const v = f.values[0];
2251
+ sinkGoal = v.param ? { param: v.param } : parseScalarOrParam(v.value);
2252
+ } else if (f.key === "targets" && f.values[0]) {
2253
+ sinkTargets = parseTargetsString(f.values[0].value);
2254
+ }
2255
+ }
2256
+ }
2257
+ if (parsed.source.kind === "query" && parsed.source.query.family === "find" && parsed.source.query.target === "segment" && !parsed.source.query.join && !parsed.source.query.groupBy?.length && parsed.transforms.length === 1) {
2258
+ const src = parsed.source.query;
2259
+ const tr = parsed.transforms[0];
2260
+ if (tr.metric || tr.agg === "count") {
2261
+ const collapsed = {
2262
+ family: "aggregate",
2263
+ raw: "",
2264
+ agg: tr.agg,
2265
+ metric: tr.metric || "sessionLoad",
2266
+ ...tr.metricParam ? { metricParam: tr.metricParam } : {},
2267
+ filters: [...src.filters, ...tr.filters],
2268
+ groupBy: tr.groupBy,
2269
+ ...tr.rollup ? { rollup: tr.rollup } : {},
2270
+ ...tr.displayUnit ?? src.displayUnit ? { displayUnit: tr.displayUnit ?? src.displayUnit } : {},
2271
+ ...tr.window ?? src.window ? { window: tr.window ?? src.window } : {}
2272
+ };
2273
+ return {
2274
+ query: serialize(collapsed),
2275
+ ...sinkView ? { sinkView } : {},
2276
+ ...sinkGoal !== void 0 ? { sinkGoal } : {},
2277
+ ...sinkTargets?.length ? { sinkTargets } : {}
2278
+ };
2279
+ }
2280
+ }
2281
+ if (parsed.source.kind === "query" && parsed.transforms.length === 0 && parsed.sink) {
2282
+ return {
2283
+ query: serialize(parsed.source.query),
2284
+ ...sinkView ? { sinkView } : {},
2285
+ ...sinkGoal !== void 0 ? { sinkGoal } : {},
2286
+ ...sinkTargets?.length ? { sinkTargets } : {}
2287
+ };
2288
+ }
2289
+ if (parsed.sink) {
2290
+ const withoutSink = serialize({ ...parsed, sink: void 0 });
2291
+ return {
2292
+ query: withoutSink,
2293
+ ...sinkView ? { sinkView } : {},
2294
+ ...sinkGoal !== void 0 ? { sinkGoal } : {},
2295
+ ...sinkTargets?.length ? { sinkTargets } : {}
2296
+ };
2297
+ }
2298
+ return { query: trimmed };
2299
+ }
2300
+ function applyLegacyFenceOptions(base, options2) {
2301
+ const next = { ...base };
2302
+ if (options2.fenceTag) {
2303
+ const tag = options2.fenceTag.trim().replace(/^`+/, "");
2304
+ const colonMatch = tag.match(/^(?:query|widget):(.*)$/i);
2305
+ const suffixText = colonMatch ? colonMatch[1] : tag.toLowerCase() === "query" ? "" : tag;
2306
+ if (suffixText !== "") {
2307
+ const spec = parseQueryWidgetSuffix(suffixText);
2308
+ if (spec.error) next.error = spec.error;
2309
+ if (spec.type) {
2310
+ if (isDashboardWidgetType(spec.type)) {
2311
+ next.view = spec.type;
2312
+ } else {
2313
+ next.rawViewType = spec.type;
2314
+ }
2315
+ }
2316
+ if (spec.spanFull) next.span = "full";
2317
+ else if (spec.spanCols) next.span = spec.spanCols;
2318
+ const extraAttrs = {};
2319
+ for (const [k, v] of Object.entries(spec.attributes)) {
2320
+ if (k === "goal" || k === "param1") {
2321
+ const parsedGoal = parseScalarOrParam(v);
2322
+ if (parsedGoal !== void 0) next.goal = next.goal ?? parsedGoal;
2323
+ else extraAttrs[k] = v;
2324
+ } else if (k === "targets") {
2325
+ const parsedTargets = parseTargetsString(v);
2326
+ if (parsedTargets.length > 0) next.targets = next.targets ?? parsedTargets;
2327
+ else extraAttrs[k] = v;
2328
+ } else if (k === "comparepr") {
2329
+ next.comparePr = v === "true";
2330
+ } else {
2331
+ extraAttrs[k] = v;
2332
+ }
2333
+ }
2334
+ if (Object.keys(extraAttrs).length > 0) {
2335
+ next.attributes = { ...next.attributes ?? {}, ...extraAttrs };
2336
+ }
2337
+ }
2338
+ }
2339
+ if (!next.title && options2.adjacentTitle) next.title = options2.adjacentTitle;
2340
+ if (!next.description && options2.adjacentDescription) next.description = options2.adjacentDescription;
2341
+ return next;
2342
+ }
2343
+ function parseQueryBlock(rawBody, options2 = {}) {
2344
+ const trimmed = rawBody.trim();
2345
+ if (isYamlQueryBlockBody(trimmed)) {
2346
+ const { data, error } = parseIndentedYamlMap(trimmed);
2347
+ const rawView = (data.scalars.view ?? data.scalars.type ?? "").toLowerCase();
2348
+ const view = isDashboardWidgetType(rawView) ? rawView : "table";
2349
+ const obj2 = {
2350
+ view,
2351
+ ...rawView && !isDashboardWidgetType(rawView) ? { rawViewType: rawView } : {},
2352
+ ...error ? { error } : {}
2353
+ };
2354
+ if (data.scalars.title) obj2.title = data.scalars.title;
2355
+ const desc = data.scalars.description ?? data.scalars.question;
2356
+ if (desc) obj2.description = desc;
2357
+ if (data.scalars.span) {
2358
+ const s = data.scalars.span.toLowerCase();
2359
+ if (s === "full") obj2.span = "full";
2360
+ else {
2361
+ const n = Number(s);
2362
+ if (n >= 1 && n <= 4) obj2.span = n;
2363
+ else obj2.error = obj2.error ?? `span ${data.scalars.span} outside 1..4`;
2364
+ }
2365
+ }
2366
+ if (data.scalars.unit) obj2.unit = data.scalars.unit;
2367
+ if (data.scalars.window) {
2368
+ const wRes = parseYamlWindow(data.scalars.window);
2369
+ if (wRes.error) obj2.error = obj2.error ?? wRes.error;
2370
+ else if (wRes.window) obj2.window = wRes.window;
2371
+ }
2372
+ const groupByList = data.stringLists.groupBy ?? (data.scalars.groupBy ? splitFlowList(data.scalars.groupBy) : void 0);
2373
+ if (groupByList?.length) obj2.groupBy = groupByList;
2374
+ if (data.scalars.goal !== void 0) {
2375
+ const g = parseScalarOrParam(data.scalars.goal);
2376
+ if (g !== void 0) obj2.goal = g;
2377
+ }
2378
+ const targetStrings = data.stringLists.targets ?? (data.scalars.targets ? parseTargetsString(data.scalars.targets).map(formatScalarOrParam) : void 0);
2379
+ if (targetStrings?.length) {
2380
+ const parsedTargets = targetStrings.map(parseScalarOrParam).filter((v) => v !== void 0);
2381
+ if (parsedTargets.length) obj2.targets = parsedTargets;
2382
+ }
2383
+ if (data.scalars.comparePr !== void 0) {
2384
+ obj2.comparePr = data.scalars.comparePr.toLowerCase() === "true";
2385
+ }
2386
+ const cols = data.stringLists.columns ?? (data.scalars.columns ? splitFlowList(data.scalars.columns) : void 0);
2387
+ if (cols?.length) obj2.columns = cols;
2388
+ if (data.objectLists.sort) {
2389
+ obj2.sort = data.objectLists.sort.filter((item) => Boolean(item.col)).map((item) => ({
2390
+ col: item.col,
2391
+ dir: item.dir?.toLowerCase() === "desc" ? "desc" : "asc"
2392
+ }));
2393
+ } else if (data.stringLists.sort) {
2394
+ obj2.sort = data.stringLists.sort.map(parseSortItem).filter((s) => Boolean(s));
2395
+ } else if (data.scalars.sort) {
2396
+ const single = parseSortItem(data.scalars.sort);
2397
+ if (single) obj2.sort = [single];
2398
+ }
2399
+ if (data.scalars.limit !== void 0) {
2400
+ const lim = Number(data.scalars.limit);
2401
+ if (!Number.isNaN(lim)) obj2.limit = lim;
2402
+ }
2403
+ if (data.scalars.query !== void 0) {
2404
+ const norm2 = normalizeSingleWqlForObject(data.scalars.query);
2405
+ obj2.query = norm2.query;
2406
+ if (!data.scalars.view && !data.scalars.type && norm2.sinkView) obj2.view = norm2.sinkView;
2407
+ if (obj2.goal === void 0 && norm2.sinkGoal !== void 0) obj2.goal = norm2.sinkGoal;
2408
+ if (!obj2.targets && norm2.sinkTargets?.length) obj2.targets = norm2.sinkTargets;
2409
+ }
2410
+ if (data.objectLists.series?.length) {
2411
+ obj2.series = data.objectLists.series.map((item, idx) => ({
2412
+ id: item.id || `s${idx + 1}`,
2413
+ ...item.label ? { label: item.label } : {},
2414
+ ...item.query ? { query: normalizeSingleWqlForObject(item.query).query } : {},
2415
+ ...item.formula ? { formula: item.formula } : {},
2416
+ ...item.normalize ? { normalize: item.normalize } : {},
2417
+ ...item.unit ? { unit: item.unit } : {},
2418
+ ...item.visible === "false" ? { visible: false } : {}
2419
+ }));
2420
+ }
2421
+ return applyLegacyFenceOptions(obj2, options2);
2422
+ }
2423
+ const nonCommentLines = trimmed.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#") && !l.startsWith("//"));
2424
+ if (nonCommentLines.length > 1 || nonCommentLines[0] && /^(?:defaults\s+|show\s+|[A-Za-z_][\w-]*\s*=)/.test(nonCommentLines[0])) {
2425
+ const docParsed = parseDocument(nonCommentLines.join("\n"));
2426
+ if (!docParsed.doc.degenerate && docParsed.doc.assignments.length > 0) {
2427
+ const shownSet = new Set(docParsed.doc.show);
2428
+ const sinkView = docParsed.doc.showChart && isDashboardWidgetType(docParsed.doc.showChart.head) ? docParsed.doc.showChart.head : void 0;
2429
+ const obj2 = {
2430
+ view: sinkView ?? "timeseries",
2431
+ ...docParsed.doc.defaults?.window ? { window: docParsed.doc.defaults.window } : {},
2432
+ ...docParsed.doc.defaults?.groupBy?.length ? { groupBy: docParsed.doc.defaults.groupBy } : {},
2433
+ series: docParsed.doc.assignments.map((a) => ({
2434
+ id: a.name,
2435
+ ...a.kind === "query" ? { query: a.queryText } : {},
2436
+ ...a.kind === "formula" && a.formula ? {
2437
+ formula: a.formula.expression,
2438
+ ...a.formula.normalize ? { normalize: a.formula.normalize } : {},
2439
+ ...a.formula.unit ? { unit: a.formula.unit } : {}
2440
+ } : {},
2441
+ ...shownSet.size > 0 && !shownSet.has(a.name) ? { visible: false } : {}
2442
+ }))
2443
+ };
2444
+ return applyLegacyFenceOptions(obj2, options2);
2445
+ }
2446
+ }
2447
+ const singleLine = nonCommentLines.join(" ");
2448
+ const norm = normalizeSingleWqlForObject(singleLine);
2449
+ const obj = {
2450
+ view: norm.sinkView ?? "table",
2451
+ query: norm.query,
2452
+ ...norm.columns ? { columns: norm.columns } : {},
2453
+ ...norm.sort ? { sort: norm.sort } : {},
2454
+ ...norm.limit !== void 0 ? { limit: norm.limit } : {},
2455
+ ...norm.sinkGoal !== void 0 ? { goal: norm.sinkGoal } : {},
2456
+ ...norm.sinkTargets?.length ? { targets: norm.sinkTargets } : {}
2457
+ };
2458
+ return applyLegacyFenceOptions(obj, options2);
2459
+ }
2460
+ function isDefaultShorthandQueryBlock(obj) {
2461
+ const effectiveView = obj.rawViewType ?? obj.view;
2462
+ if (effectiveView !== "table") return false;
2463
+ if (obj.title || obj.description || obj.span !== void 0 && obj.span !== 1 || obj.unit || obj.window || obj.groupBy?.length || obj.columns?.length || obj.sort?.length || obj.limit !== void 0 || obj.goal !== void 0 || obj.targets?.length || obj.comparePr !== void 0 || obj.attributes && Object.keys(obj.attributes).length > 0) {
2464
+ return false;
2465
+ }
2466
+ if (obj.series && obj.series.length > 0) {
2467
+ if (obj.series.length > 1) return false;
2468
+ const s = obj.series[0];
2469
+ if (s.label || s.formula || s.normalize || s.unit || s.visible === false || !s.query) return false;
2470
+ return true;
2471
+ }
2472
+ return Boolean(obj.query && !obj.query.includes("\n"));
2473
+ }
2474
+ function serializeQueryBlock(obj, opts = {}) {
2475
+ if (!opts.forceYaml && isDefaultShorthandQueryBlock(obj)) {
2476
+ return (obj.query ?? obj.series?.[0]?.query ?? "").trim();
2477
+ }
2478
+ const lines = [];
2479
+ if (obj.title) lines.push(`title: ${quoteYamlString(obj.title)}`);
2480
+ if (obj.description) lines.push(`description: ${quoteYamlString(obj.description)}`);
2481
+ lines.push(`view: ${quoteYamlString(obj.rawViewType ?? obj.view)}`);
2482
+ if (obj.span !== void 0 && obj.span !== 1) {
2483
+ lines.push(`span: ${obj.span}`);
2484
+ }
2485
+ if (obj.unit) lines.push(`unit: ${quoteYamlString(obj.unit)}`);
2486
+ if (obj.window) lines.push(`window: ${quoteYamlString(formatYamlWindow(obj.window))}`);
2487
+ if (obj.groupBy?.length) {
2488
+ lines.push(`groupBy: [${obj.groupBy.map((g) => quoteYamlString(g)).join(", ")}]`);
2489
+ }
2490
+ if (obj.goal !== void 0) {
2491
+ lines.push(`goal: ${formatScalarOrParam(obj.goal)}`);
2492
+ }
2493
+ if (obj.targets?.length) {
2494
+ lines.push(`targets: [${obj.targets.map(formatScalarOrParam).join(", ")}]`);
2495
+ }
2496
+ if (obj.comparePr !== void 0) {
2497
+ lines.push(`comparePr: ${obj.comparePr ? "true" : "false"}`);
2498
+ }
2499
+ if (obj.columns?.length) {
2500
+ lines.push(`columns: [${obj.columns.map((c) => quoteYamlString(c)).join(", ")}]`);
2501
+ }
2502
+ if (obj.sort?.length) {
2503
+ lines.push(
2504
+ `sort: [${obj.sort.map((s) => quoteYamlString(s.dir === "desc" ? `${s.col} desc` : `${s.col} asc`)).join(", ")}]`
2505
+ );
2506
+ }
2507
+ if (obj.limit !== void 0) {
2508
+ lines.push(`limit: ${obj.limit}`);
2509
+ }
2510
+ const hasRichSeries = obj.series && (obj.series.length > 1 || obj.series.some((s) => Boolean(s.formula || s.label || s.normalize || s.unit || s.visible === false)));
2511
+ if (hasRichSeries && obj.series) {
2512
+ lines.push("series:");
2513
+ for (const s of obj.series) {
2514
+ lines.push(` - id: ${quoteYamlString(s.id)}`);
2515
+ if (s.label) lines.push(` label: ${quoteYamlString(s.label)}`);
2516
+ if (s.query) lines.push(` query: ${quoteYamlString(s.query, { allowWqlBare: true })}`);
2517
+ if (s.formula) lines.push(` formula: ${quoteYamlString(s.formula, { allowWqlBare: true })}`);
2518
+ if (s.normalize) lines.push(` normalize: ${quoteYamlString(s.normalize)}`);
2519
+ if (s.unit) lines.push(` unit: ${quoteYamlString(s.unit)}`);
2520
+ if (s.visible === false) lines.push(" visible: false");
2521
+ }
2522
+ } else {
2523
+ const singleQuery = obj.query ?? obj.series?.[0]?.query ?? "";
2524
+ lines.push(`query: ${quoteYamlString(singleQuery, { allowWqlBare: true })}`);
2525
+ }
2526
+ return lines.join("\n");
2527
+ }
2528
+ function serializeQueryBlockFence(obj, opts = {}) {
2529
+ return `\`\`\`query
2530
+ ${serializeQueryBlock(obj, opts)}
2531
+ \`\`\``;
2532
+ }
2533
+ function applyTopLevelClausesToSingleQuery(rawQuery, obj) {
2534
+ const trimmed = rawQuery.trim();
2535
+ if (!trimmed) return "";
2536
+ const hasTopOverrides = Boolean(
2537
+ obj.window || obj.groupBy?.length || obj.unit || obj.columns?.length || obj.sort?.length || obj.limit !== void 0
2538
+ );
2539
+ if (!hasTopOverrides) return trimmed;
2540
+ const parsed = parseQuery(trimmed);
2541
+ if (parsed.error) return trimmed;
2542
+ if (parsed.family === "aggregate") {
2543
+ const next = {
2544
+ ...parsed,
2545
+ ...obj.groupBy?.length && parsed.groupBy.length === 0 ? { groupBy: obj.groupBy } : {},
2546
+ ...obj.window && !parsed.window ? { window: obj.window } : {},
2547
+ ...obj.unit && !parsed.displayUnit ? { displayUnit: obj.unit } : {}
2548
+ };
2549
+ return serialize(next);
2550
+ }
2551
+ if (parsed.family === "find") {
2552
+ const pipes = { ...parsed.pipes ?? {} };
2553
+ if (obj.columns?.length && !pipes.select) {
2554
+ pipes.select = obj.columns.map((c) => {
2555
+ const m = /^([\w.-]+)(?:\s+in\s+([\w/%]+))?$/i.exec(c.trim());
2556
+ return m ? { col: m[1], ...m[2] ? { unit: m[2] } : {} } : { col: c.trim() };
2557
+ });
2558
+ }
2559
+ if (obj.sort?.length && !pipes.order) {
2560
+ pipes.order = obj.sort.map((s) => ({ col: s.col, dir: s.dir }));
2561
+ }
2562
+ if (obj.limit !== void 0 && pipes.limit === void 0) {
2563
+ pipes.limit = obj.limit;
2564
+ }
2565
+ const next = {
2566
+ ...parsed,
2567
+ ...obj.groupBy?.length && !parsed.groupBy?.length ? { groupBy: obj.groupBy } : {},
2568
+ ...obj.window && !parsed.window ? { window: obj.window } : {},
2569
+ ...obj.unit && !parsed.displayUnit ? { displayUnit: obj.unit } : {},
2570
+ ...Object.keys(pipes).length > 0 ? { pipes } : {}
2571
+ };
2572
+ return serialize(next);
2573
+ }
2574
+ if (parsed.family === "pipeline") {
2575
+ const transforms = [...parsed.transforms];
2576
+ if (transforms.length > 0 && obj.groupBy?.length) {
2577
+ const lastIdx = transforms.length - 1;
2578
+ if (transforms[lastIdx].groupBy.length === 0) {
2579
+ transforms[lastIdx] = { ...transforms[lastIdx], groupBy: obj.groupBy };
2580
+ }
2581
+ }
2582
+ let source = parsed.source;
2583
+ if (obj.window && source.kind === "query" && !source.query.window) {
2584
+ source = { kind: "query", query: { ...source.query, window: obj.window } };
2585
+ }
2586
+ return serialize({ ...parsed, source, transforms });
2587
+ }
2588
+ return trimmed;
2589
+ }
2590
+ function queryBlockToDocumentText(obj) {
2591
+ const hasMultiOrFormula = obj.series && (obj.series.length > 1 || obj.series.some((s) => Boolean(s.formula || s.visible === false)));
2592
+ if (!hasMultiOrFormula) {
2593
+ const single = obj.query ?? obj.series?.[0]?.query ?? "";
2594
+ return applyTopLevelClausesToSingleQuery(single, obj);
2595
+ }
2596
+ const lines = [];
2597
+ if (obj.groupBy?.length || obj.window) {
2598
+ const parts = [];
2599
+ if (obj.groupBy?.length) parts.push(`by {${obj.groupBy.join(", ")}}`);
2600
+ if (obj.window) {
2601
+ const w = obj.window;
2602
+ parts.push(
2603
+ w.kind === "relative" ? `last ${w.size}${w.unit}` : w.kind === "param" ? `last $${w.param}${w.unit ? ` ${w.unit}` : ""}` : `from ${w.start}${w.end ? ` to ${w.end}` : ""}`
2604
+ );
2605
+ }
2606
+ lines.push(`defaults ${parts.join(" ")}`);
2607
+ }
2608
+ const showIds = [];
2609
+ for (const s of obj.series ?? []) {
2610
+ if (s.visible !== false) showIds.push(s.id);
2611
+ if (s.formula) {
2612
+ const normPart = s.normalize ? ` (normalize ${s.normalize})` : "";
2613
+ const unitPart = s.unit ? ` -> ${s.unit}` : "";
2614
+ lines.push(`${s.id} = ${s.formula}${normPart}${unitPart}`);
2615
+ } else if (s.query) {
2616
+ lines.push(`${s.id} = ${s.query}`);
2617
+ }
2618
+ }
2619
+ if (showIds.length > 0) {
2620
+ lines.push(`show ${showIds.join(", ")}`);
2621
+ }
2622
+ return lines.join("\n");
2623
+ }
2624
+ function queryBlockAttributes(obj) {
2625
+ const attributes = { ...obj.attributes ?? {} };
2626
+ const attributeOrder = Object.keys(attributes);
2627
+ const setAttr = (k, v) => {
2628
+ if (!(k in attributes)) attributeOrder.push(k);
2629
+ attributes[k] = v;
2630
+ };
2631
+ if (obj.goal !== void 0) {
2632
+ setAttr("goal", formatScalarOrParam(obj.goal));
2633
+ }
2634
+ if (obj.targets?.length) {
2635
+ setAttr("targets", obj.targets.map(formatScalarOrParam).join(" "));
2636
+ }
2637
+ if (obj.comparePr !== void 0) {
2638
+ setAttr("comparePr", obj.comparePr ? "true" : "false");
2639
+ }
2640
+ if (obj.series) {
2641
+ for (const s of obj.series) {
2642
+ if (s.label) setAttr(`label.${s.id}`, s.label);
2643
+ }
2644
+ }
2645
+ if (!obj.series && obj.title) ;
2646
+ return { attributes, attributeOrder };
2647
+ }
2648
+ function collectQueryBlockParams(obj) {
2649
+ const out = /* @__PURE__ */ new Set();
2650
+ const addFromText = (text) => {
2651
+ if (!text) return;
2652
+ const parsed = parseQuery(text);
2653
+ if (!parsed.error) {
2654
+ for (const m of text.matchAll(/\$([A-Za-z_][\w-]*)/g)) out.add(m[1]);
2655
+ return;
2656
+ }
2657
+ for (const m of text.matchAll(/\$([A-Za-z_][\w-]*)/g)) out.add(m[1]);
2658
+ };
2659
+ if (obj.window?.kind === "param") out.add(obj.window.param);
2660
+ if (obj.goal && typeof obj.goal === "object") out.add(obj.goal.param);
2661
+ for (const t2 of obj.targets ?? []) {
2662
+ if (typeof t2 === "object") out.add(t2.param);
2663
+ }
2664
+ addFromText(obj.query);
2665
+ for (const s of obj.series ?? []) {
2666
+ addFromText(s.query);
2667
+ addFromText(s.formula);
2668
+ }
2669
+ for (const v of Object.values(obj.attributes ?? {})) {
2670
+ addFromText(v);
2671
+ }
2672
+ return [...out];
2673
+ }
2674
+
2675
+ // src/disciplines.ts
2676
+ var EFFORT_DISCIPLINES = [
2677
+ "bodyweight",
2678
+ "cycling",
2679
+ "gymnastics",
2680
+ "kettlebell",
2681
+ "recovery",
2682
+ "rowing",
2683
+ "running",
2684
+ "strength",
2685
+ "swimming",
2686
+ "walking"
2687
+ ];
2688
+ function isEffortDiscipline(value) {
2689
+ return typeof value === "string" && EFFORT_DISCIPLINES.includes(value.toLowerCase());
2690
+ }
2691
+ var DISCIPLINE_FACTORS = {
2692
+ strength: 1.2,
2693
+ kettlebell: 1.2,
2694
+ gymnastics: 1.2,
2695
+ recovery: 0.9,
2696
+ bodyweight: 1,
2697
+ cycling: 1,
2698
+ rowing: 1,
2699
+ running: 1,
2700
+ swimming: 1,
2701
+ walking: 1
2702
+ };
2703
+ function disciplineFactorFor(discipline) {
2704
+ if (!discipline) return 1;
2705
+ const key = discipline.toLowerCase();
2706
+ return isEffortDiscipline(key) ? DISCIPLINE_FACTORS[key] : 1;
2707
+ }
2708
+ var wqlLanguage = language.LRLanguage.define({
2709
+ parser: parser.configure({
2710
+ props: [
2711
+ highlight.styleTags({
2712
+ // Rule styles color stray punctuation; the Word tokens inside carry
2713
+ // the visible styling (unstyled children reset the parent class) —
2714
+ // so parents with punctuation children (Head spans `:journal`) are
2715
+ // NOT styled directly, or highlightTree merges the word into the
2716
+ // parent span.
2717
+ "Aggregator/Word": highlight.tags.keyword,
2718
+ "Metric/Word": highlight.tags.variableName,
2719
+ // Direct Word child of Head = the colon head name (`:sum`, `:journal`).
2720
+ "Head/Word": highlight.tags.keyword,
2721
+ DatasetReference: highlight.tags.keyword,
2722
+ "DatasetReference/Word": highlight.tags.keyword,
2723
+ At: highlight.tags.punctuation,
2724
+ ParamRef: highlight.tags.special(highlight.tags.variableName),
2725
+ "ParamRef/Word": highlight.tags.special(highlight.tags.variableName),
2726
+ Dollar: highlight.tags.punctuation,
2727
+ TagKey: highlight.tags.propertyName,
2728
+ "TagKey/Word": highlight.tags.propertyName,
2729
+ TagValue: highlight.tags.string,
2730
+ "TagValue/Value/Word": highlight.tags.string,
2731
+ "TagValue/Word": highlight.tags.string,
2732
+ Negate: highlight.tags.operator,
2733
+ Star: highlight.tags.operator,
2734
+ Dimension: highlight.tags.attributeName,
2735
+ "Dimension/Word": highlight.tags.attributeName,
2736
+ By: highlight.tags.keyword,
2737
+ RollupDot: highlight.tags.keyword,
2738
+ Int: highlight.tags.number,
2739
+ "Rollup/Word": highlight.tags.unit,
2740
+ braceOpen: highlight.tags.bracket,
2741
+ braceClose: highlight.tags.bracket,
2742
+ parenOpen: highlight.tags.bracket,
2743
+ parenClose: highlight.tags.bracket,
2744
+ colon: highlight.tags.punctuation,
2745
+ comma: highlight.tags.punctuation,
2746
+ pipe: highlight.tags.punctuation,
2747
+ dot: highlight.tags.punctuation
2748
+ })
2749
+ ]
2750
+ }),
2751
+ languageData: {
2752
+ closeBrackets: { brackets: ["{", "("] }
2753
+ }
2754
+ });
2755
+ function options(labels) {
2756
+ return labels.map((label) => typeof label === "string" ? { label } : label);
2757
+ }
2758
+ function scanContext(doc, pos) {
2759
+ let depth = 0, quoted = false, pipeFrom = -1, lastBrace = -1, fragStart = 0;
2760
+ for (let i = 0; i < pos; i++) {
2761
+ const ch = doc[i];
2762
+ if (quoted) {
2763
+ if (ch === '"') quoted = false;
2764
+ continue;
2765
+ }
2766
+ if (ch === '"') {
2767
+ quoted = true;
2768
+ continue;
2769
+ }
2770
+ if (ch === "{") {
2771
+ depth++;
2772
+ lastBrace = i;
2773
+ fragStart = i + 1;
2774
+ } else if (ch === "}") {
2775
+ depth = Math.max(0, depth - 1);
2776
+ fragStart = i + 1;
2777
+ } else if (ch === "," && depth > 0) fragStart = i + 1;
2778
+ else if (ch === "|" && depth === 0) {
2779
+ pipeFrom = i + 1;
2780
+ fragStart = i + 1;
2781
+ }
2782
+ }
2783
+ return { depth, quoted, pipeFrom, lastBrace, fragStart };
2784
+ }
2785
+ function maskQuotes(text) {
2786
+ let out = "";
2787
+ let quoted = false;
2788
+ for (const ch of text) {
2789
+ if (ch === '"') {
2790
+ quoted = !quoted;
2791
+ out += " ";
2792
+ } else out += quoted ? " " : ch;
2793
+ }
2794
+ return out;
2795
+ }
2796
+ function unquotedColonIndex(text) {
2797
+ let quoted = false;
2798
+ for (let i = 0; i < text.length; i++) {
2799
+ const ch = text[i];
2800
+ if (ch === '"') quoted = !quoted;
2801
+ else if (ch === ":" && !quoted) return i;
2802
+ }
2803
+ return -1;
2804
+ }
2805
+ function usedEntries(doc, groupStart, fragStart) {
2806
+ const text = maskQuotes(doc.slice(groupStart + 1, fragStart));
2807
+ const used = /* @__PURE__ */ new Set();
2808
+ for (const entry of text.split(",")) {
2809
+ const m = /!?\s*([A-Za-z][\w-]*)/.exec(entry);
2810
+ if (m) used.add(m[1]);
2811
+ }
2812
+ return used;
2813
+ }
2814
+ function valueOf(c) {
2815
+ return typeof c.apply === "string" ? c.apply : c.label;
2816
+ }
2817
+ function quoteApply(c) {
2818
+ const value = valueOf(c);
2819
+ if (!/\s/.test(value)) return c;
2820
+ return {
2821
+ ...c,
2822
+ apply: (view, _c, from, to) => {
2823
+ const text = view.state.sliceDoc(Math.max(0, from - 1), from) === '"' ? value : `"${value}"`;
2824
+ view.dispatch({ changes: { from, to, insert: text }, selection: { anchor: from + text.length }, userEvent: "input.complete" });
2825
+ }
2826
+ };
2827
+ }
2828
+ function mergeValues(host, statics) {
2829
+ const seen = /* @__PURE__ */ new Set();
2830
+ const out = [];
2831
+ host.forEach((c, i) => {
2832
+ const k = valueOf(c).toLowerCase();
2833
+ if (!k || seen.has(k)) return;
2834
+ seen.add(k);
2835
+ out.push(quoteApply({ ...c, boost: Math.max(-99, Math.min(99, 80 - i)) }));
2836
+ });
2837
+ for (const c of statics) {
2838
+ const k = valueOf(c).toLowerCase();
2839
+ if (!k || seen.has(k)) continue;
2840
+ seen.add(k);
2841
+ out.push(quoteApply(c));
2842
+ }
2843
+ return out;
2844
+ }
2845
+ function narrow(options2, typed) {
2846
+ const q = typed.toLowerCase();
2847
+ if (!q) return [...options2];
2848
+ const prefix = [];
2849
+ const rest = [];
2850
+ for (const o of options2) {
2851
+ const label = o.label.toLowerCase();
2852
+ if (label.startsWith(q)) prefix.push(o);
2853
+ else if (label.includes(q)) rest.push(o);
2854
+ }
2855
+ return [...prefix, ...rest];
2856
+ }
2857
+ function finish(_context, from, options2, typed) {
2858
+ const ranked = narrow(options2, typed);
2859
+ if (!ranked.length) return null;
2860
+ return {
2861
+ from,
2862
+ options: ranked.map((o, i) => o.boost === void 0 ? { ...o, boost: Math.max(-99, 50 - i) } : o),
2863
+ filter: false
2864
+ };
2865
+ }
2866
+ function chained(list, suffix) {
2867
+ return list.map((c) => ({
2868
+ ...c,
2869
+ apply: (view, _c, from, to) => {
2870
+ const text = typeof c.apply === "string" ? c.apply : c.label;
2871
+ const selfTerminated = /\W$/.test(text);
2872
+ const present = view.state.sliceDoc(to, to + suffix.length) === suffix;
2873
+ const insert = selfTerminated || present ? text : text + suffix;
2874
+ view.dispatch({
2875
+ changes: { from, to: to + (selfTerminated && present ? suffix.length : 0), insert },
2876
+ selection: { anchor: from + insert.length },
2877
+ userEvent: "input.complete"
2878
+ });
2879
+ autocomplete.startCompletion(view);
2880
+ }
2881
+ }));
2882
+ }
2883
+ var HEAD_OPTIONS = [
2884
+ ...WQL_FUNCTION_HEADS.map((label) => ({ label, detail: "function head", type: "keyword" })),
2885
+ ...WQL_SOURCE_HEADS.map((label) => ({ label, detail: "source head", type: "keyword" })),
2886
+ ...WQL_CHART_HEADS.map((label) => ({ label, detail: "chart head", type: "class" })),
2887
+ ...WQL_STANDARD_DATASETS.map((label) => ({ label, detail: "page dataset", type: "class" }))
2888
+ ];
2889
+ var PIPELINE_STAGE_HEAD_OPTIONS = [
2890
+ ...WQL_FUNCTION_HEADS.map((label) => ({ label, detail: "function head", type: "keyword" })),
2891
+ ...WQL_CHART_HEADS.map((label) => ({ label, detail: "chart head", type: "class" }))
2892
+ ];
2893
+ var CHART_HEAD_OPTIONS = WQL_CHART_HEADS.map((label) => ({
2894
+ label,
2895
+ detail: "chart head",
2896
+ type: "class"
2897
+ }));
2898
+ var TOKEN_COMPLETION_OPTIONS = [
2899
+ { label: "$thisnote", detail: "current note ID", type: "variable" },
2900
+ { label: "$note", detail: "current note ID", type: "variable" },
2901
+ { label: "$scope", detail: "entity scope filter", type: "variable" }
2902
+ ];
2903
+ function splitLinePipeRanges(line, lineOffset = 0) {
2904
+ const stages = [];
2905
+ let depth = 0;
2906
+ let quoted = false;
2907
+ let start = 0;
2908
+ for (let i = 0; i < line.length; i++) {
2909
+ const ch = line[i];
2910
+ if (quoted) {
2911
+ if (ch === '"') quoted = false;
2912
+ continue;
2913
+ }
2914
+ if (ch === '"') {
2915
+ quoted = true;
2916
+ continue;
2917
+ }
2918
+ if (ch === "{") depth++;
2919
+ else if (ch === "}") depth = Math.max(0, depth - 1);
2920
+ else if (ch === "|" && depth === 0) {
2921
+ stages.push({ text: line.slice(start, i), from: lineOffset + start, to: lineOffset + i });
2922
+ start = i + 1;
2923
+ }
2924
+ }
2925
+ stages.push({ text: line.slice(start), from: lineOffset + start, to: lineOffset + line.length });
2926
+ return stages;
2927
+ }
2928
+ function resolveWqlCursorStage(doc, pos) {
2929
+ const clamped = Math.max(0, Math.min(pos, doc.length));
2930
+ const lineStart = doc.lastIndexOf("\n", Math.max(0, clamped - 1)) + 1;
2931
+ const nextNl = doc.indexOf("\n", clamped);
2932
+ const lineEnd = nextNl === -1 ? doc.length : nextNl;
2933
+ const rawLine = doc.slice(lineStart, lineEnd);
2934
+ if (/^\s*defaults\b/.test(rawLine)) {
2935
+ return { role: "defaults", stageIndex: 0, from: lineStart, to: lineEnd };
2936
+ }
2937
+ const showM = /^(\s*show\s+)/.exec(rawLine);
2938
+ if (showM) {
2939
+ const bodyOffset = lineStart + showM[1].length;
2940
+ const stages2 = splitLinePipeRanges(rawLine.slice(showM[1].length), bodyOffset);
2941
+ const activeIdx2 = Math.max(0, stages2.findIndex((s) => clamped >= s.from && clamped <= s.to));
2942
+ const active2 = stages2[activeIdx2] ?? stages2[stages2.length - 1];
2943
+ if (activeIdx2 > 0) {
2944
+ const headM2 = /^\s*:?([A-Za-z][\w-]*)/.exec(active2.text);
2945
+ return { role: "sink", stageIndex: activeIdx2, head: headM2?.[1], from: active2.from, to: active2.to };
2946
+ }
2947
+ return { role: "binding", stageIndex: 0, from: active2.from, to: active2.to };
2948
+ }
2949
+ const assignM = /^(\s*[A-Za-z_][\w-]*\s*=\s*)/.exec(rawLine);
2950
+ const exprStart = assignM ? lineStart + assignM[1].length : lineStart;
2951
+ if (assignM && clamped < exprStart) {
2952
+ return { role: "binding", stageIndex: 0, from: lineStart, to: exprStart };
2953
+ }
2954
+ const exprText = doc.slice(exprStart, lineEnd);
2955
+ const stages = splitLinePipeRanges(exprText, exprStart);
2956
+ let activeIdx = stages.findIndex((s) => clamped >= s.from && clamped <= s.to);
2957
+ if (activeIdx === -1) activeIdx = stages.length - 1;
2958
+ const active = stages[activeIdx];
2959
+ const headM = /^\s*([:@]?)([A-Za-z][\w-]*)/.exec(active.text);
2960
+ const headName = headM?.[2]?.toLowerCase();
2961
+ if (headName && WQL_CHART_HEADS.includes(headName)) {
2962
+ return { role: "sink", stageIndex: activeIdx, head: headName, from: active.from, to: active.to };
2963
+ }
2964
+ if (activeIdx === 0) {
2965
+ if (headName && WQL_FUNCTION_HEADS.includes(headName) && stages.length === 1) {
2966
+ return { role: "reduce", stageIndex: 0, reduceIndex: 0, head: headName, from: active.from, to: active.to };
2967
+ }
2968
+ return { role: "source", stageIndex: 0, head: headM ? `${headM[1] ?? ""}${headName}` : void 0, from: active.from, to: active.to };
2969
+ }
2970
+ if (/^\s*(?:select|order\s+by|limit|offset)\b/i.test(active.text)) {
2971
+ return { role: "source", stageIndex: activeIdx, head: "pipes", from: active.from, to: active.to };
2972
+ }
2973
+ return {
2974
+ role: "reduce",
2975
+ stageIndex: activeIdx,
2976
+ reduceIndex: activeIdx - 1,
2977
+ head: headName,
2978
+ from: active.from,
2979
+ to: active.to
2980
+ };
2981
+ }
2982
+ function collectPriorBindings(doc, lineStart) {
2983
+ if (lineStart <= 0) return [];
2984
+ const priorText = doc.slice(0, lineStart);
2985
+ const seen = /* @__PURE__ */ new Set();
2986
+ const out = [];
2987
+ for (const raw of priorText.split("\n")) {
2988
+ const line = raw.trim();
2989
+ if (!line || line.startsWith("//")) continue;
2990
+ const m = /^([A-Za-z_][\w-]*)\s*=\s*(.+)$/.exec(line);
2991
+ if (!m) continue;
2992
+ const name = m[1];
2993
+ if (seen.has(name)) continue;
2994
+ seen.add(name);
2995
+ const rhs = m[2].trim();
2996
+ const isList = /^:(?:note|notes|journal|notebook|notebooks|playground|block|effort|session|segment|event)\b/.test(rhs) && !rhs.includes("| :");
2997
+ out.push({
2998
+ label: name,
2999
+ detail: isList ? "bound list" : "bound value",
3000
+ type: "variable"
3001
+ });
3002
+ }
3003
+ return out;
3004
+ }
3005
+ function headApply(o, colonTyped) {
3006
+ if (o.label.startsWith("@")) return { ...o, apply: o.label };
3007
+ return { ...o, apply: `${colonTyped ? "" : ":"}${o.label}{` };
1546
3008
  }
1547
3009
  var LEGACY_HEAD_OPTIONS = WQL_AGGREGATORS.map((label) => ({ label, type: "keyword" }));
1548
3010
  var NOTE_COLUMNS = ["title", "date", "createdAt", "type", "sourceId", "notebookId"];
@@ -1568,12 +3030,14 @@ var UNIT_OPTIONS = WQL_DISPLAY_UNITS.map((label) => ({ label, detail: "display u
1568
3030
  function resolveHead(_agg, _typedTarget) {
1569
3031
  return { family: "aggregate" };
1570
3032
  }
3033
+ var CHART_PARAM_KEYS = ["label", "unit", "goal", "limit", "stack", "type", "smooth"];
1571
3034
  function resolveColonHead(name) {
1572
3035
  const n = name.toLowerCase();
1573
3036
  if (WQL_FUNCTION_HEADS.includes(n)) return { family: "aggregate" };
1574
- if (WQL_CHART_HEADS.includes(n)) return { family: "find" };
3037
+ if (WQL_CHART_HEADS.includes(n)) return { family: "find", target: "__chart__" };
1575
3038
  const headTarget = (head) => {
1576
3039
  if (WQL_NOTE_HEAD_ALIASES.includes(head)) return "note";
3040
+ if (head === "feeds") return "feed";
1577
3041
  const scoped = WQL_SOURCE_HEAD_SCOPES[head] || WQL_TYPE_HEADS[head];
1578
3042
  return scoped ? "note" : head;
1579
3043
  };
@@ -1591,6 +3055,7 @@ var FIND_KEY_UNION = [
1591
3055
  ...new Set(WQL_FIND_TARGETS.flatMap((t2) => [...wqlFilterKeys(t2, "find")]))
1592
3056
  ];
1593
3057
  function filterKeysFor(head) {
3058
+ if (head.target === "__chart__") return CHART_PARAM_KEYS;
1594
3059
  return head.family === "find" ? head.target ? wqlFilterKeys(head.target, "find") : FIND_KEY_UNION : wqlFilterKeys("", "aggregate");
1595
3060
  }
1596
3061
  function pipeColumns(target) {
@@ -1797,39 +3262,148 @@ function wqlCompletionSource(options_ = {}) {
1797
3262
  { label: "order by", apply: "order by ", detail: head.target === "session" ? "advisory: order ignored for sessions" : "sort rows", type: "keyword" },
1798
3263
  { label: "select", apply: "select ", detail: SELECT_IGNORED.has(head.target ?? "") ? `advisory: select ignored for find:${head.target}` : "choose columns", type: "keyword" }
1799
3264
  ];
1800
- return finish(context, from, [...chained(kw, " "), limitSnippet], typed);
3265
+ const colonTyped = /^[ \t]*:/.test(text);
3266
+ return finish(context, from, [
3267
+ ...PIPELINE_STAGE_HEAD_OPTIONS.map((o) => headApply(o, colonTyped)),
3268
+ ...chained(kw, " "),
3269
+ limitSnippet
3270
+ ], typed);
1801
3271
  };
1802
3272
  function inner(context, replaceSelection = false) {
1803
- const doc = context.state.doc.toString();
1804
- const pos = context.pos;
1805
- const word = context.matchBefore(/[\w.*-]*/) ?? { from: pos, to: pos, text: "" };
1806
- const opens = word.from === word.to && pos > 0 && /[{,:|(!"@]/.test(doc[pos - 1]);
3273
+ const fullDoc = context.state.doc.toString();
3274
+ const fullPos = context.pos;
3275
+ const lineStart = fullDoc.lastIndexOf("\n", Math.max(0, fullPos - 1)) + 1;
3276
+ const nextNl = fullDoc.indexOf("\n", fullPos);
3277
+ const lineEnd = nextNl === -1 ? fullDoc.length : nextNl;
3278
+ const lineText = fullDoc.slice(lineStart, lineEnd);
3279
+ const priorBindings = collectPriorBindings(fullDoc, lineStart);
3280
+ const dollarMatch = context.matchBefore(/\$[A-Za-z_0-9-]*/);
3281
+ if (dollarMatch) {
3282
+ const typedToken = replaceSelection ? "$" : dollarMatch.text;
3283
+ return finish(context, dollarMatch.from, TOKEN_COMPLETION_OPTIONS, typedToken);
3284
+ }
3285
+ const defaultsM = /^(\s*defaults\s+)/.exec(lineText);
3286
+ if (defaultsM && fullPos >= lineStart + defaultsM[1].length) {
3287
+ const subOffset = lineStart + defaultsM[1].length;
3288
+ const subDoc = ":segment{} " + fullDoc.slice(subOffset, lineEnd);
3289
+ const shift = ":segment{} ".length - subOffset;
3290
+ const subPos = fullPos + shift;
3291
+ const word2 = context.matchBefore(/[\w.*-]*/) ?? { from: fullPos, text: "" };
3292
+ const typed2 = replaceSelection ? "" : word2.text;
3293
+ const scan = scanContext(subDoc, subPos);
3294
+ const subCtx2 = new autocomplete.CompletionContext(state.EditorState.create({ doc: subDoc }), subPos, context.explicit);
3295
+ const res = suffixSlot(subCtx2, subDoc, scan, word2.from + shift, typed2, { family: "aggregate" }, ":segment{}".length);
3296
+ return res && { ...res, from: res.from - shift, ...res.to !== void 0 ? { to: res.to - shift } : {} };
3297
+ }
3298
+ const showM = /^(\s*show\s+)/.exec(lineText);
3299
+ if (showM && fullPos >= lineStart + showM[1].length) {
3300
+ const showBodyOffset = lineStart + showM[1].length;
3301
+ const showStages = splitLinePipeRanges(lineText.slice(showM[1].length), showBodyOffset);
3302
+ const showActiveIdx = Math.max(0, showStages.findIndex((s) => fullPos >= s.from && fullPos <= s.to));
3303
+ if (showActiveIdx === 0) {
3304
+ const word3 = context.matchBefore(/[\w.*-]*/) ?? { from: fullPos, text: "" };
3305
+ const typed3 = replaceSelection ? "" : word3.text;
3306
+ return finish(context, word3.from, priorBindings, typed3);
3307
+ }
3308
+ const sinkStage = showStages[showActiveIdx];
3309
+ const stageDoc = fullDoc.slice(sinkStage.from, lineEnd);
3310
+ const stagePos = fullPos - sinkStage.from;
3311
+ const word2 = context.matchBefore(/[\w.*-]*/) ?? { from: fullPos, text: "" };
3312
+ const typed2 = replaceSelection ? "" : word2.text;
3313
+ const scan = scanContext(stageDoc, stagePos);
3314
+ const colonHeadM = /^[ \t]*:[ \t]*([A-Za-z][\w-]*)/.exec(stageDoc);
3315
+ if (scan.depth > 0 && colonHeadM) {
3316
+ const head = resolveColonHead(colonHeadM[1]);
3317
+ const subCtx2 = new autocomplete.CompletionContext(state.EditorState.create({ doc: stageDoc }), stagePos, context.explicit);
3318
+ const res = braceSlot(subCtx2, stageDoc, scan, word2.from - sinkStage.from, typed2, head);
3319
+ const rebase = (r) => r && { ...r, from: r.from + sinkStage.from, ...r.to !== void 0 ? { to: r.to + sinkStage.from } : {} };
3320
+ return res instanceof Promise ? res.then(rebase) : rebase(res);
3321
+ }
3322
+ const colonTyped2 = word2.from > sinkStage.from && fullDoc[word2.from - 1] === ":";
3323
+ const headFrom2 = colonTyped2 ? word2.from : word2.from;
3324
+ return finish(context, headFrom2, CHART_HEAD_OPTIONS.map((o) => headApply(o, colonTyped2)), typed2);
3325
+ }
3326
+ const assignM = /^(\s*[A-Za-z_][\w-]*\s*=\s*)/.exec(lineText);
3327
+ const exprOffset = assignM && fullPos >= lineStart + assignM[1].length ? lineStart + assignM[1].length : lineStart;
3328
+ const exprDoc = fullDoc.slice(exprOffset, lineEnd);
3329
+ const exprPos = fullPos - exprOffset;
3330
+ const word = context.matchBefore(/[\w.*-]*/) ?? { from: fullPos, to: fullPos, text: "" };
3331
+ const opens = word.from === word.to && fullPos > exprOffset && (/[{,:|(!"@]/.test(fullDoc[fullPos - 1]) || /\|[ \t]*$/.test(exprDoc.slice(0, exprPos)));
1807
3332
  if (word.from === word.to && !context.explicit && !opens) return null;
1808
- const scan = scanContext(doc, pos);
1809
- if (scan.quoted && scan.depth === 0) return null;
3333
+ const fullScan = scanContext(exprDoc, exprPos);
3334
+ if (fullScan.quoted && fullScan.depth === 0) return null;
1810
3335
  const typed = replaceSelection ? "" : word.text;
1811
- const colonHeadM = /^[ \t]*:[ \t]*([A-Za-z][\w-]*)/.exec(doc);
1812
- const headM = colonHeadM ?? /^[ \t]*([A-Za-z]+)[ \t]*:[ \t]*([A-Za-z0-9_.-]*)/.exec(doc);
1813
- const head = colonHeadM ? resolveColonHead(colonHeadM[1]) : headM ? resolveHead(headM[1], headM[2]) : { family: "aggregate" };
1814
- if (scan.pipeFrom >= 0 && scan.depth === 0) return pipeSlot(context, doc, scan, word.from, typed, head);
1815
- if (scan.depth > 0) return braceSlot(context, doc, scan, word.from, typed, head);
1816
- const datasetTyped = word.from > 0 && doc[word.from - 1] === "@";
1817
- const headPool = datasetTyped ? HEAD_OPTIONS.filter((o) => o.label.startsWith("@")) : HEAD_OPTIONS;
3336
+ const stages = splitLinePipeRanges(exprDoc, 0);
3337
+ let activeStageIdx = stages.findIndex((s) => exprPos >= s.from && exprPos <= s.to);
3338
+ if (activeStageIdx === -1) activeStageIdx = stages.length - 1;
3339
+ const activeStage = stages[activeStageIdx];
3340
+ const firstStageText = stages[0].text;
3341
+ const firstColonM = /^[ \t]*:[ \t]*([A-Za-z][\w-]*)/.exec(firstStageText);
3342
+ const firstHeadM = firstColonM ?? /^[ \t]*([A-Za-z]+)[ \t]*:[ \t]*([A-Za-z0-9_.-]*)/.exec(firstStageText);
3343
+ const firstHead = firstColonM ? resolveColonHead(firstColonM[1]) : firstHeadM ? resolveHead(firstHeadM[1], firstHeadM[2]) : { family: "aggregate" };
3344
+ if (activeStageIdx > 0) {
3345
+ const stageText = activeStage.text;
3346
+ const stagePos = exprPos - activeStage.from;
3347
+ const stageAbsOffset = exprOffset + activeStage.from;
3348
+ const stageColonM = /^[ \t]*:[ \t]*([A-Za-z][\w-]*)/.exec(stageText);
3349
+ if (stageColonM || /^[ \t]*:[ \t]*$/.test(stageText.slice(0, stagePos))) {
3350
+ const stageScan = scanContext(stageText, stagePos);
3351
+ const stageHead = stageColonM ? resolveColonHead(stageColonM[1]) : { family: "aggregate" };
3352
+ const subCtx3 = new autocomplete.CompletionContext(state.EditorState.create({ doc: stageText }), stagePos, context.explicit);
3353
+ const rebase = (r) => r && { ...r, from: r.from + stageAbsOffset, ...r.to !== void 0 ? { to: r.to + stageAbsOffset } : {} };
3354
+ if (stageScan.depth > 0) {
3355
+ const res3 = braceSlot(subCtx3, stageText, stageScan, word.from - stageAbsOffset, typed, stageHead);
3356
+ return res3 instanceof Promise ? res3.then(rebase) : rebase(res3);
3357
+ }
3358
+ if (!stageColonM || stagePos <= stageColonM[0].length) {
3359
+ return finish(context, word.from, PIPELINE_STAGE_HEAD_OPTIONS.map((o) => headApply(o, true)), typed);
3360
+ }
3361
+ const res2 = suffixSlot(subCtx3, stageText, stageScan, word.from - stageAbsOffset, typed, stageHead, stageColonM[0].length);
3362
+ return rebase(res2);
3363
+ }
3364
+ const subCtx2 = new autocomplete.CompletionContext(state.EditorState.create({ doc: exprDoc }), exprPos, context.explicit);
3365
+ const res = pipeSlot(subCtx2, exprDoc, fullScan, word.from - exprOffset, typed, firstHead);
3366
+ return res && { ...res, from: res.from + exprOffset, ...res.to !== void 0 ? { to: res.to + exprOffset } : {} };
3367
+ }
3368
+ const subCtx = new autocomplete.CompletionContext(state.EditorState.create({ doc: exprDoc }), exprPos, context.explicit);
3369
+ const rebaseExpr = (r) => r && { ...r, from: r.from + exprOffset, ...r.to !== void 0 ? { to: r.to + exprOffset } : {} };
3370
+ if (fullScan.depth > 0) {
3371
+ const res = braceSlot(subCtx, exprDoc, fullScan, word.from - exprOffset, typed, firstHead);
3372
+ return res instanceof Promise ? res.then(rebaseExpr) : rebaseExpr(res);
3373
+ }
3374
+ if (priorBindings.length > 0 && /[+\-*/(]\s*[\w-]*$/.test(exprDoc.slice(0, exprPos))) {
3375
+ return finish(context, word.from, priorBindings, typed);
3376
+ }
3377
+ const datasetTyped = word.from > exprOffset && fullDoc[word.from - 1] === "@";
3378
+ const colonTyped = word.from > exprOffset && fullDoc[word.from - 1] === ":";
3379
+ const docKeywords = exprOffset === lineStart && lineStart > 0 ? [
3380
+ { label: "defaults", apply: "defaults ", detail: "block defaults", type: "keyword" },
3381
+ { label: "show", apply: "show ", detail: "display outputs", type: "keyword" }
3382
+ ] : [];
3383
+ const headPool = datasetTyped ? HEAD_OPTIONS.filter((o) => o.label.startsWith("@")) : colonTyped ? HEAD_OPTIONS : [...priorBindings, ...docKeywords, ...HEAD_OPTIONS];
1818
3384
  const headTyped = datasetTyped ? "@" + typed : typed;
1819
3385
  const headFrom = datasetTyped ? word.from - 1 : word.from;
1820
- if (!headM) return finish(context, headFrom, headPool.map((o) => headApply(o, false)), headTyped);
1821
- if (colonHeadM) {
1822
- if (pos <= colonHeadM[0].length) {
1823
- return finish(context, headFrom, headPool.map((o) => headApply(o, true)), headTyped);
3386
+ if (!firstHeadM) {
3387
+ return finish(
3388
+ context,
3389
+ headFrom,
3390
+ headPool.map((o) => o.type === "variable" || o.label === "defaults" || o.label === "show" ? o : headApply(o, colonTyped)),
3391
+ headTyped
3392
+ );
3393
+ }
3394
+ if (firstColonM) {
3395
+ if (exprPos <= firstColonM[0].length) {
3396
+ return finish(context, headFrom, HEAD_OPTIONS.map((o) => headApply(o, true)), headTyped);
1824
3397
  }
1825
- return suffixSlot(context, doc, scan, word.from, typed, head, colonHeadM[0].length);
3398
+ return rebaseExpr(suffixSlot(subCtx, exprDoc, fullScan, word.from - exprOffset, typed, firstHead, firstColonM[0].length));
1826
3399
  }
1827
- const colonIdx = headM[0].indexOf(":");
1828
- if (pos <= colonIdx) return finish(context, word.from, chained(LEGACY_HEAD_OPTIONS, ":"), typed);
1829
- if (pos <= headM[0].length) {
1830
- return metricHeadSlot(context, word.from, typed);
3400
+ const colonIdx = firstHeadM[0].indexOf(":");
3401
+ if (exprPos <= colonIdx) return finish(context, word.from, [...priorBindings, ...chained(LEGACY_HEAD_OPTIONS, ":")], typed);
3402
+ if (exprPos <= firstHeadM[0].length) {
3403
+ const res = metricHeadSlot(subCtx, word.from - exprOffset, typed);
3404
+ return res instanceof Promise ? res.then(rebaseExpr) : rebaseExpr(res);
1831
3405
  }
1832
- return suffixSlot(context, doc, scan, word.from, typed, head, headM[0].length);
3406
+ return rebaseExpr(suffixSlot(subCtx, exprDoc, fullScan, word.from - exprOffset, typed, firstHead, firstHeadM[0].length));
1833
3407
  }
1834
3408
  function source(context) {
1835
3409
  const selection = context.state.selection.main;
@@ -1996,7 +3570,7 @@ function zonedNoon(iso, timeZone) {
1996
3570
  return zonedStartOfDay(iso, timeZone) + 12 * 36e5;
1997
3571
  }
1998
3572
  function resolveWindowRange(w, ctx) {
1999
- if (!w) return void 0;
3573
+ if (!w || w.kind === "param") return void 0;
2000
3574
  if (w.kind === "relative") {
2001
3575
  const today = civilDateOf(ctx.instant, ctx.timeZone);
2002
3576
  const start2 = w.unit === "d" ? civilDateAdd(today, -(w.size - 1)) : civilDateAdd(civilMonday(today), -7 * (w.size - 1));
@@ -2024,176 +3598,6 @@ function inRange(instant, range) {
2024
3598
  return instant <= range.end;
2025
3599
  }
2026
3600
 
2027
- // src/document.ts
2028
- var FORMULA_CHARS = /^[\w\s.+\-*/(),]+$/;
2029
- function referencesOf(expression, known) {
2030
- const refs = [];
2031
- for (const match of expression.matchAll(/[A-Za-z_][\w-]*/g)) {
2032
- const name = match[0];
2033
- if (known.has(name) && !refs.includes(name)) refs.push(name);
2034
- }
2035
- return refs;
2036
- }
2037
- function parseDocument(text) {
2038
- const diagnostics = [];
2039
- const lines = text.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("//"));
2040
- const defaults = {};
2041
- let hasDefaults = false;
2042
- const assignments = [];
2043
- let show;
2044
- let degenerate = false;
2045
- const known = /* @__PURE__ */ new Set();
2046
- for (const line of lines) {
2047
- if (line.startsWith("defaults ")) {
2048
- hasDefaults = true;
2049
- const rest = line.slice("defaults ".length).trim();
2050
- const byMatch = /by\s*\{([^}]*)\}/.exec(rest);
2051
- if (byMatch) {
2052
- defaults.groupBy = byMatch[1].split(",").map((d) => d.trim()).filter(Boolean);
2053
- }
2054
- const lastMatch = /last\s+(\d+)\s*(d|w)/.exec(rest);
2055
- if (lastMatch) {
2056
- defaults.window = { kind: "relative", size: Number(lastMatch[1]), unit: lastMatch[2] };
2057
- }
2058
- const fromMatch = /from\s+(\d{4}-\d{2}-\d{2})(?:\s+to\s+(\d{4}-\d{2}-\d{2}))?/.exec(rest);
2059
- if (fromMatch) {
2060
- defaults.window = { kind: "range", start: fromMatch[1], end: fromMatch[2] };
2061
- }
2062
- continue;
2063
- }
2064
- if (line.startsWith("show ")) {
2065
- show = line.slice("show ".length).split(",").map((s) => s.trim()).filter(Boolean);
2066
- continue;
2067
- }
2068
- const assign = /^([A-Za-z_][\w-]*)\s*=\s*(.+)$/.exec(line);
2069
- if (assign) {
2070
- const name = assign[1];
2071
- const body = assign[2].trim();
2072
- known.add(name);
2073
- const unitMatch = /->\s*([\w/%.]+)\s*$/.exec(body);
2074
- const normalizeMatch = /\(normalize\s+([^)]+)\)/.exec(body);
2075
- const expression = body.replace(/->\s*[\w/%.]+\s*$/, "").replace(/\(normalize\s+[^)]+\)/, "").trim();
2076
- const looksLikeQuery = /^(sum|avg|min|max|count|last|delta|find|rows)\b|^[:@|]/.test(expression);
2077
- if (looksLikeQuery) {
2078
- const parsed = parseQuery(expression);
2079
- if (!parsed.error) {
2080
- assignments.push({ name, kind: "query", queryText: expression, parsed, raw: line });
2081
- } else {
2082
- diagnostics.push(`Assignment "${name}" is not a valid query`);
2083
- }
2084
- continue;
2085
- }
2086
- if (!FORMULA_CHARS.test(expression)) {
2087
- diagnostics.push(`Assignment "${name}" has characters a formula cannot parse`);
2088
- continue;
2089
- }
2090
- const formula = {
2091
- expression,
2092
- references: [],
2093
- ...normalizeMatch ? { normalize: normalizeMatch[1].trim() } : {},
2094
- ...unitMatch ? { unit: unitMatch[1] } : {}
2095
- };
2096
- assignments.push({ name, kind: "formula", formula, raw: line });
2097
- continue;
2098
- }
2099
- degenerate = true;
2100
- }
2101
- if (!hasDefaults && show === void 0 && !degenerate && assignments.length === 1 && assignments[0].kind === "query") ;
2102
- if (!hasDefaults && show === void 0 && assignments.length === 0) {
2103
- const parsed = parseQuery(lines.join(" "));
2104
- if (!parsed.error) {
2105
- known.add("query");
2106
- assignments.push({ name: "query", kind: "query", queryText: lines.join(" "), parsed, raw: lines.join(" ") });
2107
- show = ["query"];
2108
- degenerate = true;
2109
- } else {
2110
- diagnostics.push("Body is neither a valid query nor a valid document");
2111
- }
2112
- }
2113
- for (const assignment of assignments) {
2114
- if (assignment.kind !== "formula" || !assignment.formula) continue;
2115
- assignment.formula.references = referencesOf(assignment.formula.expression, known);
2116
- for (const match of assignment.formula.expression.matchAll(/[A-Za-z_][\w-]*/g)) {
2117
- const name = match[0];
2118
- if (!known.has(name)) {
2119
- diagnostics.push(`Formula "${assignment.name}" references unknown name "${name}"`);
2120
- }
2121
- }
2122
- if (assignment.formula.references.length === 0) {
2123
- diagnostics.push(`Formula "${assignment.name}" references no named query or formula`);
2124
- }
2125
- }
2126
- const cycle = findCycle(assignments);
2127
- if (cycle) {
2128
- diagnostics.push(`Reference cycle detected: ${cycle.join(" -> ")}`);
2129
- }
2130
- if (show === void 0) {
2131
- show = assignments.filter((a) => a.kind === "query").map((a) => a.name);
2132
- if (assignments.length > 0 && show.length === 0) show = [assignments[0].name];
2133
- } else {
2134
- for (const name of show) {
2135
- if (!known.has(name)) diagnostics.push(`show references unknown name "${name}"`);
2136
- }
2137
- }
2138
- return {
2139
- doc: {
2140
- ...Object.keys(defaults).length > 0 ? { defaults } : {},
2141
- assignments,
2142
- show,
2143
- degenerate
2144
- },
2145
- diagnostics
2146
- };
2147
- }
2148
- function findCycle(assignments) {
2149
- const formulas = new Map(assignments.filter((a) => a.kind === "formula").map((a) => [a.name, a.formula.references]));
2150
- const state = /* @__PURE__ */ new Map();
2151
- const stack = [];
2152
- const visit = (name) => {
2153
- const s = state.get(name);
2154
- if (s === 2) return void 0;
2155
- if (s === 1) {
2156
- const start = stack.indexOf(name);
2157
- return [...stack.slice(start === -1 ? 0 : start), name];
2158
- }
2159
- state.set(name, 1);
2160
- stack.push(name);
2161
- for (const ref of formulas.get(name) ?? []) {
2162
- const cycle = visit(ref);
2163
- if (cycle) return cycle;
2164
- }
2165
- stack.pop();
2166
- state.set(name, 2);
2167
- return void 0;
2168
- };
2169
- for (const name of formulas.keys()) {
2170
- const cycle = visit(name);
2171
- if (cycle) return cycle;
2172
- }
2173
- return void 0;
2174
- }
2175
- function serializeDocument(doc) {
2176
- if (doc.degenerate) {
2177
- const only = doc.assignments[0];
2178
- return only?.queryText ?? "";
2179
- }
2180
- const lines = [];
2181
- if (doc.defaults) {
2182
- const parts = [];
2183
- if (doc.defaults.groupBy?.length) parts.push(`by {${doc.defaults.groupBy.join(", ")}}`);
2184
- if (doc.defaults.window) {
2185
- const w = doc.defaults.window;
2186
- parts.push(w.kind === "relative" ? `last ${w.size}${w.unit}` : `from ${w.start}${w.end ? ` to ${w.end}` : ""}`);
2187
- }
2188
- if (parts.length > 0) lines.push(`defaults ${parts.join(" ")}`);
2189
- }
2190
- for (const a of doc.assignments) {
2191
- lines.push(a.raw);
2192
- }
2193
- if (doc.show.length > 0) lines.push(`show ${doc.show.join(", ")}`);
2194
- return lines.join("\n");
2195
- }
2196
-
2197
3601
  // src/documentRunner.ts
2198
3602
  function createBuiltinFormulaEvaluator() {
2199
3603
  const evalExpr = (expr, inputs, position) => {
@@ -2281,6 +3685,8 @@ function appendSuffixes(queryText, window, groupBy) {
2281
3685
  }
2282
3686
  if (window.kind === "relative" && !/\blast\b/.test(text)) {
2283
3687
  text += ` last ${window.size}${window.unit}`;
3688
+ } else if (window.kind === "param" && !/\blast\b/.test(text)) {
3689
+ text += ` last $${window.param}${window.unit ? ` ${window.unit}` : ""}`;
2284
3690
  } else if (window.kind === "range" && !/\bfrom\b/.test(text)) {
2285
3691
  text += ` from ${window.start}${window.end ? ` to ${window.end}` : ""}`;
2286
3692
  }
@@ -2302,6 +3708,7 @@ var QueryDocumentRunner = class {
2302
3708
  const doc = parsed.doc;
2303
3709
  const outputs = [];
2304
3710
  const values = /* @__PURE__ */ new Map();
3711
+ const findPayloads = /* @__PURE__ */ new Map();
2305
3712
  const evaluate = async (assignment) => {
2306
3713
  if (assignment.kind === "query") {
2307
3714
  const merged = this.mergeDefaults(assignment, doc);
@@ -2311,6 +3718,7 @@ var QueryDocumentRunner = class {
2311
3718
  const findRun = await this.options?.runFind?.(text2) ?? {
2312
3719
  error: "find queries are not supported by this host"
2313
3720
  };
3721
+ findPayloads.set(assignment.name, { payload: findRun, queryText: text2 });
2314
3722
  outputs.push({
2315
3723
  name: assignment.name,
2316
3724
  kind: "find",
@@ -2327,18 +3735,43 @@ var QueryDocumentRunner = class {
2327
3735
  return;
2328
3736
  }
2329
3737
  if (parsedFamily === "pipeline") {
2330
- const text2 = merged.window ? appendSuffixes(merged.queryText, merged.window, merged.groupBy) : merged.queryText;
2331
- const pipelineRun = await this.options?.runPipeline?.(text2) ?? {
2332
- error: "pipeline queries are not supported by this host"
2333
- };
3738
+ let pipelineRun;
3739
+ if (assignment.sourceRef && (assignment.pipeTail ?? assignment.transformsText)) {
3740
+ const rawTail = assignment.pipeTail ?? assignment.transformsText;
3741
+ const tailWithDefaults = merged.window ? appendSuffixes(rawTail, merged.window, merged.groupBy) : merged.groupBy?.length && !/by\s*\{/.test(rawTail) ? `${rawTail} by {${merged.groupBy.join(", ")}}` : rawTail;
3742
+ const sourceEntry = findPayloads.get(assignment.sourceRef);
3743
+ if (sourceEntry && this.options?.runPipelineFromFind) {
3744
+ pipelineRun = await this.options.runPipelineFromFind(sourceEntry.payload, tailWithDefaults);
3745
+ } else if (sourceEntry && this.options?.runAggregateEvents) {
3746
+ pipelineRun = await this.options.runAggregateEvents(tailWithDefaults, sourceEntry.payload.events ?? [], sourceEntry.payload);
3747
+ } else if (sourceEntry) {
3748
+ pipelineRun = await this.options?.runPipeline?.(`${sourceEntry.queryText} | ${tailWithDefaults}`) ?? {
3749
+ error: "pipeline queries are not supported by this host"
3750
+ };
3751
+ } else {
3752
+ pipelineRun = { error: `Source "${assignment.sourceRef}" is not a valid list source` };
3753
+ }
3754
+ } else {
3755
+ const text2 = merged.window ? appendSuffixes(merged.queryText, merged.window, merged.groupBy) : merged.queryText;
3756
+ pipelineRun = await this.options?.runPipeline?.(text2) ?? {
3757
+ error: "pipeline queries are not supported by this host"
3758
+ };
3759
+ }
2334
3760
  const byPosition3 = /* @__PURE__ */ new Map();
2335
3761
  for (const s of pipelineRun.series ?? []) {
2336
3762
  for (const point of s.points) {
2337
3763
  if (!point.missing) byPosition3.set(point.ts, { value: point.value, missingDerived: false });
2338
3764
  }
2339
3765
  }
3766
+ const lastTransform = assignment.parsed?.family === "pipeline" ? assignment.parsed.transforms[assignment.parsed.transforms.length - 1] : void 0;
3767
+ const hasBuckets2 = Boolean(merged.groupBy && merged.groupBy.length > 0 || lastTransform?.groupBy?.length || lastTransform?.rollup);
2340
3768
  if (byPosition3.size > 0) {
2341
- values.set(assignment.name, { byPosition: byPosition3, unit: pipelineRun.unit, groupKeys: (pipelineRun.series ?? []).map((s) => s.key) });
3769
+ values.set(assignment.name, {
3770
+ byPosition: byPosition3,
3771
+ unit: pipelineRun.unit,
3772
+ groupKeys: (pipelineRun.series ?? []).map((s) => s.key),
3773
+ isScalar: !hasBuckets2 && byPosition3.size === 1
3774
+ });
2342
3775
  }
2343
3776
  outputs.push({
2344
3777
  name: assignment.name,
@@ -2370,7 +3803,13 @@ var QueryDocumentRunner = class {
2370
3803
  }
2371
3804
  }
2372
3805
  }
2373
- values.set(assignment.name, { byPosition: byPosition2, unit: run.unit, groupKeys: series2.map((s) => s.key) });
3806
+ const hasBuckets = Boolean(merged.groupBy && merged.groupBy.length > 0 || assignment.parsed?.family === "aggregate" && assignment.parsed.rollup);
3807
+ values.set(assignment.name, {
3808
+ byPosition: byPosition2,
3809
+ unit: run.unit,
3810
+ groupKeys: series2.map((s) => s.key),
3811
+ isScalar: !hasBuckets && byPosition2.size === 1
3812
+ });
2374
3813
  outputs.push({
2375
3814
  name: assignment.name,
2376
3815
  kind: "aggregate",
@@ -2385,14 +3824,30 @@ var QueryDocumentRunner = class {
2385
3824
  const formula = assignment.formula;
2386
3825
  const inputs = {};
2387
3826
  const positions = /* @__PURE__ */ new Set();
3827
+ const seriesRefs = formula.references.filter((ref) => {
3828
+ const v = values.get(ref);
3829
+ return v !== void 0 && !v.isScalar;
3830
+ });
2388
3831
  for (const ref of formula.references) {
2389
3832
  const input = values.get(ref);
2390
3833
  if (!input) {
2391
3834
  diagnostics.push(`Formula "${assignment.name}" references unavailable input "${ref}"`);
2392
3835
  return;
2393
3836
  }
2394
- for (const position of input.byPosition.keys()) positions.add(position);
2395
- inputs[ref] = (position) => input.byPosition.get(position);
3837
+ const scalarFallback = input.isScalar ? [...input.byPosition.values()][0] : void 0;
3838
+ if (!input.isScalar) {
3839
+ for (const position of input.byPosition.keys()) positions.add(position);
3840
+ }
3841
+ inputs[ref] = (position) => input.byPosition.get(position) ?? scalarFallback;
3842
+ }
3843
+ if (positions.size === 0 && formula.references.length > 0) {
3844
+ for (const ref of formula.references) {
3845
+ const anchorTs = [...values.get(ref)?.byPosition.keys() ?? []][0];
3846
+ if (anchorTs !== void 0) {
3847
+ positions.add(anchorTs);
3848
+ break;
3849
+ }
3850
+ }
2396
3851
  }
2397
3852
  const byPosition = /* @__PURE__ */ new Map();
2398
3853
  let error;
@@ -2418,7 +3873,7 @@ var QueryDocumentRunner = class {
2418
3873
  pairs: pairCount,
2419
3874
  error: "corr needs at least two paired positions"
2420
3875
  });
2421
- values.set(assignment.name, { byPosition: /* @__PURE__ */ new Map(), groupKeys: [] });
3876
+ values.set(assignment.name, { byPosition: /* @__PURE__ */ new Map(), groupKeys: [], isScalar: false });
2422
3877
  return;
2423
3878
  }
2424
3879
  const mean = (xs) => xs.reduce((s, v) => s + v, 0) / xs.length;
@@ -2437,13 +3892,13 @@ var QueryDocumentRunner = class {
2437
3892
  pairs: pairCount,
2438
3893
  error: "corr is undefined for zero-variance inputs"
2439
3894
  });
2440
- values.set(assignment.name, { byPosition: /* @__PURE__ */ new Map(), groupKeys: [] });
3895
+ values.set(assignment.name, { byPosition: /* @__PURE__ */ new Map(), groupKeys: [], isScalar: false });
2441
3896
  return;
2442
3897
  }
2443
3898
  const value = cov / Math.sqrt(va * vb);
2444
3899
  byPosition.set(0, { value, missingDerived: false });
2445
3900
  outputs.push({ name: assignment.name, kind: "formula", pairs: pairCount, series: [{ key: "corr", label: "corr", points: [{ ts: 0, value }] }] });
2446
- values.set(assignment.name, { byPosition, groupKeys: ["corr"] });
3901
+ values.set(assignment.name, { byPosition, groupKeys: ["corr"], isScalar: true });
2447
3902
  return;
2448
3903
  }
2449
3904
  for (const position of [...positions].sort((a, b) => a - b)) {
@@ -2463,7 +3918,7 @@ var QueryDocumentRunner = class {
2463
3918
  ...v.missingDerived ? { missingDerived: true } : {}
2464
3919
  }))
2465
3920
  }];
2466
- values.set(assignment.name, { byPosition, unit: formula.unit, groupKeys: [assignment.name] });
3921
+ values.set(assignment.name, { byPosition, unit: formula.unit, groupKeys: [assignment.name], isScalar: seriesRefs.length === 0 && byPosition.size === 1 });
2467
3922
  outputs.push({
2468
3923
  name: assignment.name,
2469
3924
  kind: "formula",
@@ -2479,7 +3934,7 @@ var QueryDocumentRunner = class {
2479
3934
  progressed = false;
2480
3935
  for (let i = pending.length - 1; i >= 0; i--) {
2481
3936
  const assignment = pending[i];
2482
- const refs = assignment.formula?.references ?? [];
3937
+ const refs = assignment.formula?.references ?? (assignment.sourceRef ? [assignment.sourceRef] : []);
2483
3938
  if (refs.every((r) => evaluated.has(r))) {
2484
3939
  await evaluate(assignment);
2485
3940
  evaluated.add(assignment.name);
@@ -2488,27 +3943,197 @@ var QueryDocumentRunner = class {
2488
3943
  }
2489
3944
  }
2490
3945
  }
2491
- const shown = new Set(doc.show);
2492
- return {
2493
- outputs: outputs.filter((o) => shown.has(o.name)),
2494
- diagnostics
2495
- };
3946
+ const byName = new Map(outputs.map((o) => [o.name, o]));
3947
+ const orderedOutputs = doc.show.map((name) => byName.get(name)).filter((o) => o !== void 0).map((o) => doc.showChart && !o.chart ? { ...o, chart: doc.showChart } : o);
3948
+ return {
3949
+ outputs: orderedOutputs,
3950
+ diagnostics,
3951
+ ...doc.showChart ? { showChart: doc.showChart } : {}
3952
+ };
3953
+ }
3954
+ /** Defaults precedence (ticket 17): explicit per-query range/grouping >
3955
+ * block defaults > host. Explicit per-query by{} fully replaces block
3956
+ * grouping — never merges. */
3957
+ mergeDefaults(assignment, doc) {
3958
+ const parsed = assignment.parsed;
3959
+ const pipelineTransformGroupBy = parsed?.family === "pipeline" ? parsed.transforms.find((t2) => t2.groupBy.length > 0)?.groupBy : void 0;
3960
+ const aggregateGroupBy = parsed?.family === "aggregate" ? parsed.groupBy : pipelineTransformGroupBy;
3961
+ const groupBy = aggregateGroupBy?.length ? aggregateGroupBy : doc.defaults?.groupBy;
3962
+ const window = (parsed && parsed.family !== "pipeline" ? parsed.window : void 0) ?? doc.defaults?.window;
3963
+ return {
3964
+ queryText: assignment.queryText ?? "",
3965
+ ...groupBy ? { groupBy } : {},
3966
+ ...window ? { window } : {}
3967
+ };
3968
+ }
3969
+ };
3970
+
3971
+ // src/dashboard/model.ts
3972
+ function resolveWidgetType(type) {
3973
+ return type == null || type === "" ? "table" : type;
3974
+ }
3975
+ function unknownWidgetTypeMessage(type) {
3976
+ return `unknown widget type "${type}"`;
3977
+ }
3978
+ function unknownTokensMessage(missing) {
3979
+ return `unknown token${missing.length > 1 ? "s" : ""}: ${missing.map((m) => `$${m}`).join(", ")}`;
3980
+ }
3981
+ var PLANNED_WIDGET_TYPES = [];
3982
+ var KNOWN_CALC_METRICS = new Set(WQL_CALC_TARGETS);
3983
+ function isProposedMetric(metricKey) {
3984
+ if (!metricKey) return false;
3985
+ if (!metricKey.startsWith("calc.")) return false;
3986
+ return !KNOWN_CALC_METRICS.has(metricKey);
3987
+ }
3988
+ var TOKEN_PREFIX = "dashboard.";
3989
+ function isDashboardMeta(meta) {
3990
+ return meta["dashboard"] === "true" || meta["dashboard"] === true;
3991
+ }
3992
+ function extractDashboardTokens(meta) {
3993
+ const tokens = [];
3994
+ for (const [key, value] of Object.entries(meta)) {
3995
+ if (!key.startsWith(TOKEN_PREFIX) || key === TOKEN_PREFIX) continue;
3996
+ const name = key.slice(TOKEN_PREFIX.length);
3997
+ if (name === "") continue;
3998
+ if (Array.isArray(value)) {
3999
+ if (value.length > 0) tokens.push({ name, values: value.map(String), isList: true });
4000
+ } else {
4001
+ tokens.push({ name, values: [String(value)], isList: false });
4002
+ }
2496
4003
  }
2497
- /** Defaults precedence (ticket 17): explicit per-query range/grouping >
2498
- * block defaults > host. Explicit per-query by{} fully replaces block
2499
- * grouping — never merges. */
2500
- mergeDefaults(assignment, doc) {
2501
- const parsed = assignment.parsed;
2502
- const aggregateGroupBy = parsed?.family === "aggregate" ? parsed.groupBy : void 0;
2503
- const groupBy = aggregateGroupBy?.length ? aggregateGroupBy : doc.defaults?.groupBy;
2504
- const window = (parsed && parsed.family !== "pipeline" ? parsed.window : void 0) ?? doc.defaults?.window;
2505
- return {
2506
- queryText: assignment.queryText ?? "",
2507
- ...groupBy ? { groupBy } : {},
2508
- ...window ? { window } : {}
2509
- };
4004
+ return tokens;
4005
+ }
4006
+ function defaultTokenValues(tokens) {
4007
+ const values = {};
4008
+ for (const token of tokens) values[token.name] = token.values[0];
4009
+ return values;
4010
+ }
4011
+ function setDashboardTokenValue(meta, name, value) {
4012
+ const key = `${TOKEN_PREFIX}${name}`;
4013
+ const current = meta[key];
4014
+ const next = { ...meta };
4015
+ if (Array.isArray(current)) {
4016
+ if (!current.includes(value)) return meta;
4017
+ next[key] = [value, ...current.filter((v) => v !== value)];
4018
+ } else if (current != null) {
4019
+ next[key] = typeof current === "number" && !isNaN(Number(value)) ? Number(value) : value;
4020
+ } else {
4021
+ next[key] = value;
2510
4022
  }
2511
- };
4023
+ return next;
4024
+ }
4025
+ var TOKEN_REF_RE = /\$([A-Za-z_][\w-]*)/g;
4026
+ function substituteTokens(query, values) {
4027
+ const missing = /* @__PURE__ */ new Set();
4028
+ let hadEmpty = false;
4029
+ let substituted = query.replace(TOKEN_REF_RE, (raw, name) => {
4030
+ const value = values[name];
4031
+ if (value == null) {
4032
+ missing.add(name);
4033
+ return raw;
4034
+ }
4035
+ if (value.trim() === "") hadEmpty = true;
4036
+ return value;
4037
+ });
4038
+ if (hadEmpty) {
4039
+ substituted = substituted.replace(/\{\s*,+/g, "{").replace(/,+\s*\}/g, "}").replace(/,\s*,+/g, ", ").replace(/\blast\s+(?:[dw]\b)?(?=\s*(?:\||$|\r?\n))/gi, "").replace(/[ \t]{2,}/g, " ").trim();
4040
+ }
4041
+ return { query: substituted, missing: [...missing] };
4042
+ }
4043
+ function referencedTokens(query) {
4044
+ const names = /* @__PURE__ */ new Set();
4045
+ for (const match of query.matchAll(TOKEN_REF_RE)) names.add(match[1]);
4046
+ return [...names];
4047
+ }
4048
+ function headingText(content) {
4049
+ const line = content.split("\n", 1)[0].replace(/^#{1,6}\s*/, "").trim();
4050
+ return line === "" ? void 0 : line;
4051
+ }
4052
+ function paragraphText(content) {
4053
+ const text = content.replace(/\s*\n\s*/g, " ").trim();
4054
+ return text === "" ? void 0 : text;
4055
+ }
4056
+ function buildDashboardDocument(sections, meta) {
4057
+ const widgets = [];
4058
+ sections.forEach((section, i) => {
4059
+ if (section.type !== "query") return;
4060
+ let adjacentTitle;
4061
+ let adjacentQuestion;
4062
+ const above = sections[i - 1];
4063
+ if (above?.type === "markdown" && above.subtype === "paragraph") {
4064
+ adjacentQuestion = paragraphText(above.content);
4065
+ const heading = sections[i - 2];
4066
+ if (heading?.type === "markdown" && heading.subtype === "heading") {
4067
+ adjacentTitle = headingText(heading.content);
4068
+ }
4069
+ } else if (above?.type === "markdown" && above.subtype === "heading") {
4070
+ adjacentTitle = headingText(above.content);
4071
+ }
4072
+ if (isYamlQueryBlockBody(section.content)) {
4073
+ const obj2 = parseQueryBlock(section.content, {
4074
+ fenceTag: section.fenceTag,
4075
+ adjacentTitle,
4076
+ adjacentDescription: adjacentQuestion
4077
+ });
4078
+ const compiledBody = queryBlockToDocumentText(obj2);
4079
+ const derivedAttrs = queryBlockAttributes(obj2);
4080
+ const mergedAttributes = {
4081
+ ...derivedAttrs.attributes,
4082
+ ...section.attributes ?? {}
4083
+ };
4084
+ const mergedOrder = [
4085
+ ...derivedAttrs.attributeOrder,
4086
+ ...(section.attributeOrder ?? []).filter((k) => !(k in derivedAttrs.attributes))
4087
+ ];
4088
+ widgets.push({
4089
+ key: `w${widgets.length}`,
4090
+ type: section.widgetType ?? obj2.rawViewType ?? obj2.view,
4091
+ spanCols: section.spanCols ?? (typeof obj2.span === "number" ? obj2.span : void 0),
4092
+ spanFull: section.spanFull ?? (obj2.span === "full" ? true : void 0),
4093
+ widgetError: section.widgetError ?? obj2.error,
4094
+ body: compiledBody,
4095
+ rawBody: section.content.trim(),
4096
+ queryObject: obj2,
4097
+ attributes: mergedAttributes,
4098
+ attributeOrder: mergedOrder,
4099
+ title: obj2.title ?? adjacentTitle,
4100
+ question: obj2.description ?? adjacentQuestion
4101
+ });
4102
+ return;
4103
+ }
4104
+ const body = section.content.split("\n").filter((l) => {
4105
+ const s = l.trim();
4106
+ return s !== "" && !s.startsWith("#");
4107
+ }).map((l) => l.trim()).join("\n");
4108
+ const synthesizedTag = section.fenceTag ?? (section.widgetType ? `query:${section.widgetType}${section.spanFull ? "-full" : section.spanCols ? `-${section.spanCols}` : ""}` : "query");
4109
+ const obj = parseQueryBlock(section.content, {
4110
+ fenceTag: synthesizedTag,
4111
+ adjacentTitle,
4112
+ adjacentDescription: adjacentQuestion
4113
+ });
4114
+ widgets.push({
4115
+ key: `w${widgets.length}`,
4116
+ type: section.widgetType ?? "",
4117
+ spanCols: section.spanCols,
4118
+ spanFull: section.spanFull,
4119
+ widgetError: section.widgetError,
4120
+ body,
4121
+ rawBody: body,
4122
+ queryObject: obj,
4123
+ attributes: section.attributes ?? {},
4124
+ attributeOrder: section.attributeOrder ?? [],
4125
+ title: adjacentTitle,
4126
+ question: adjacentQuestion
4127
+ });
4128
+ });
4129
+ const titleMeta = meta["title"];
4130
+ return {
4131
+ isDashboard: isDashboardMeta(meta),
4132
+ title: typeof titleMeta === "string" && titleMeta !== "" ? titleMeta : void 0,
4133
+ tokens: extractDashboardTokens(meta),
4134
+ widgets
4135
+ };
4136
+ }
2512
4137
 
2513
4138
  // src/queryDocumentRunner.ts
2514
4139
  var PageSourceRegistry = class extends Map {
@@ -2540,8 +4165,55 @@ var QueryDocumentRunner2 = class {
2540
4165
  // that supply none still get a single capture for the run.
2541
4166
  context: runOptions.context ?? this.initialOptions.context ?? captureContext()
2542
4167
  };
2543
- const substituted = substituteTokens(text, captured.tokens ?? {});
2544
- const parsed = parseDocument(substituted);
4168
+ const tokens = captured.tokens ?? {};
4169
+ const initialParsed = parseDocument(text);
4170
+ let effectiveText;
4171
+ if (initialParsed.diagnostics.length === 0 && initialParsed.doc.assignments.length > 0) {
4172
+ const missingAll = /* @__PURE__ */ new Set();
4173
+ const boundAssignments = initialParsed.doc.assignments.map((a) => {
4174
+ if (a.kind === "query" && a.parsed && !a.sourceRef) {
4175
+ const bound = bindQueryParams(a.parsed, tokens);
4176
+ for (const m of bound.missing) missingAll.add(m);
4177
+ if (bound.missing.length === 0 && !bound.parsed.error) {
4178
+ const serialized = serialize(bound.parsed);
4179
+ return {
4180
+ ...a,
4181
+ parsed: bound.parsed,
4182
+ queryText: serialized,
4183
+ raw: initialParsed.doc.degenerate ? serialized : `${a.name} = ${serialized}`
4184
+ };
4185
+ }
4186
+ }
4187
+ const subRaw = substituteTokens(a.raw, tokens);
4188
+ for (const m of subRaw.missing) missingAll.add(m);
4189
+ return { ...a, raw: subRaw.query };
4190
+ });
4191
+ let boundDefaults = initialParsed.doc.defaults;
4192
+ if (boundDefaults?.window?.kind === "param") {
4193
+ const probe = bindQueryParams(
4194
+ { family: "find", raw: ":segment{}", target: "segment", filters: [], window: boundDefaults.window },
4195
+ tokens
4196
+ );
4197
+ for (const m of probe.missing) missingAll.add(m);
4198
+ if (probe.missing.length === 0 && probe.parsed.family === "find") {
4199
+ boundDefaults = { ...boundDefaults, window: probe.parsed.window };
4200
+ }
4201
+ }
4202
+ if (missingAll.size > 0) {
4203
+ return {
4204
+ outputs: [],
4205
+ diagnostics: [unknownTokensMessage([...missingAll])]
4206
+ };
4207
+ }
4208
+ effectiveText = serializeDocument({
4209
+ ...initialParsed.doc,
4210
+ ...boundDefaults ? { defaults: boundDefaults } : {},
4211
+ assignments: boundAssignments
4212
+ });
4213
+ } else {
4214
+ effectiveText = substituteTokens2(text, tokens);
4215
+ }
4216
+ const parsed = parseDocument(effectiveText);
2545
4217
  if (parsed.doc.assignments.some((a) => consumesRollupFacts(a.parsed))) {
2546
4218
  await this.host.rollupEnsure?.();
2547
4219
  }
@@ -2561,16 +4233,16 @@ var QueryDocumentRunner2 = class {
2561
4233
  runPipeline: async (queryText) => {
2562
4234
  const res = await this.host.runPipeline?.(queryText, captured.context);
2563
4235
  return res ?? { error: "pipeline queries are not supported by this host" };
2564
- }
4236
+ },
4237
+ ...this.host.runPipelineFromFind ? {
4238
+ runPipelineFromFind: async (sourceFind, pipeTail) => this.host.runPipelineFromFind(sourceFind, pipeTail, captured.context)
4239
+ } : {}
2565
4240
  });
2566
- return formulaRunner.run(substituted);
4241
+ return formulaRunner.run(effectiveText);
2567
4242
  }
2568
4243
  };
2569
- function substituteTokens(text, tokens) {
2570
- return text.replace(/\$([A-Za-z_][\w-]*)/g, (match, name) => {
2571
- const value = tokens[name];
2572
- return value !== void 0 ? value : match;
2573
- });
4244
+ function substituteTokens2(text, tokens) {
4245
+ return substituteTokens(text, tokens).query;
2574
4246
  }
2575
4247
  function consumesRollupFacts(parsed) {
2576
4248
  if (!parsed) return false;
@@ -2678,8 +4350,9 @@ function firstEffortSlug(metrics) {
2678
4350
  }
2679
4351
  return void 0;
2680
4352
  }
4353
+ var PLUMBING_OUTPUT_TYPES = /* @__PURE__ */ new Set(["load", "compiler", "system", "event"]);
2681
4354
  function toEventRows(logs, identity) {
2682
- return logs.map((output, seq) => ({
4355
+ return logs.filter((output) => !PLUMBING_OUTPUT_TYPES.has(output.outputType ?? "segment")).map((output, seq) => ({
2683
4356
  id: `${identity.resultId}:${seq}`,
2684
4357
  resultId: identity.resultId,
2685
4358
  noteId: identity.noteId,
@@ -2752,13 +4425,18 @@ function toSummaryEventRows(logs, identity) {
2752
4425
  blockVersion: identity.blockVersion
2753
4426
  }));
2754
4427
  }
2755
- function projectEventToFacts(record) {
4428
+ function matchesFactMetricKey(metricKey, queryMetric) {
4429
+ return !queryMetric || metricKey === queryMetric || queryMetric === "rep" && metricKey === "reps" || queryMetric === "reps" && metricKey === "rep";
4430
+ }
4431
+ function projectEventToFacts(record, filter) {
4432
+ const resolvedFilter = typeof filter === "object" && filter !== null ? filter : void 0;
2756
4433
  const metrics = wodWikiCore.createMetricOwnershipLedger(record.metrics).visible();
2757
4434
  if (record.grain === "summary") {
2758
4435
  const m = metrics[0];
2759
4436
  if (!m || typeof m.value !== "number") return [];
2760
4437
  const fieldRef = readFieldRef(m.metadata);
2761
4438
  const metricKey = metadataString(m.metadata, "canonicalKey") ?? fieldRef?.path ?? (m.type ?? "");
4439
+ if (!matchesFactMetricKey(metricKey, resolvedFilter?.metricKey)) return [];
2762
4440
  const temporal = record.metricTemporal?.[0];
2763
4441
  const dims2 = readGroupTags(m.metadata);
2764
4442
  return [{
@@ -2811,13 +4489,15 @@ function projectEventToFacts(record) {
2811
4489
  if (dimKey) dims[dimKey] = String(m.value);
2812
4490
  }
2813
4491
  const rowDims = Object.keys(dims).length > 0 ? dims : void 0;
4492
+ let metricOrdinal = 0;
2814
4493
  metrics.forEach((m) => {
2815
4494
  if (m.type === wodWikiCore.MetricType.Label || m.type === "label" || typeof m.value !== "number") return;
2816
4495
  const fieldRef = readFieldRef(m.metadata);
2817
4496
  const legacyFallback = m.type === wodWikiCore.MetricType.Rep || m.type === "rep" ? "reps" : m.type ?? "metric";
2818
4497
  if (!fieldRef && legacyFallback === "custom") return;
2819
4498
  const metricKey = metadataString(m.metadata, "canonicalKey") ?? fieldRef?.path ?? (labelName ? resolveCanonicalMetricKey(labelName) : legacyFallback);
2820
- const ordinal = facts.length;
4499
+ const ordinal = metricOrdinal++;
4500
+ if (!matchesFactMetricKey(metricKey, resolvedFilter?.metricKey)) return [];
2821
4501
  const temporal = record.metricTemporal?.[ordinal];
2822
4502
  const factTimestamp = temporal?.temporalKind === "instant" && temporal.instant !== void 0 ? temporal.instant : record.timestamp;
2823
4503
  facts.push({
@@ -2969,18 +4649,7 @@ function feedOfItem(item) {
2969
4649
  const id = item.feedSourceId;
2970
4650
  return typeof id === "string" && id.trim() ? id : void 0;
2971
4651
  }
2972
- function notebookOfItem(item) {
2973
- if (item.notebookId && item.notebookId.trim()) {
2974
- return wodWikiCore.toNotebookId(item.notebookId);
2975
- }
2976
- if (item.tags) {
2977
- for (const tag of item.tags) {
2978
- if (tag.startsWith("notebook:") || tag.startsWith("notebook/")) {
2979
- const slug2 = wodWikiCore.notebookSlugFromId(tag);
2980
- if (slug2) return wodWikiCore.toNotebookId(slug2);
2981
- }
2982
- }
2983
- }
4652
+ function resolveIntrinsicNotebook(item) {
2984
4653
  const corpus = /^markdown\/(notebooks|canvas|efforts|dashboards|equipment)\/(.+)$/.exec(item.sourcePath ?? "");
2985
4654
  if (corpus) {
2986
4655
  const [, family, rest] = corpus;
@@ -2999,10 +4668,67 @@ function notebookOfItem(item) {
2999
4668
  if (isJournalRow(item)) return "notebook/journal";
3000
4669
  return void 0;
3001
4670
  }
4671
+ function notebooksOfItem(item) {
4672
+ const out = [];
4673
+ const push = (raw) => {
4674
+ if (!raw || !raw.trim()) return;
4675
+ const id = wodWikiCore.toNotebookId(raw);
4676
+ if (!out.includes(id)) out.push(id);
4677
+ };
4678
+ if (item.notebookIds?.length) {
4679
+ for (const raw of item.notebookIds) push(raw);
4680
+ }
4681
+ if (item.notebookId?.trim()) {
4682
+ push(item.notebookId);
4683
+ }
4684
+ if (item.tags) {
4685
+ for (const tag of item.tags) {
4686
+ if (tag.startsWith("notebook:") || tag.startsWith("notebook/")) {
4687
+ const slug = wodWikiCore.notebookSlugFromId(tag);
4688
+ if (slug) push(slug);
4689
+ }
4690
+ }
4691
+ }
4692
+ if (out.length === 0) {
4693
+ const intrinsic = resolveIntrinsicNotebook(item);
4694
+ if (intrinsic) {
4695
+ push(intrinsic);
4696
+ const isCorePrimitive = (intrinsic === "notebook/efforts" || intrinsic === "notebook/equipment") && item.seedOrigin === "seed" && (!item.feedSourceId || item.feedSourceId === "core");
4697
+ if (isCorePrimitive) {
4698
+ push("notebook/defaults");
4699
+ }
4700
+ }
4701
+ }
4702
+ return out;
4703
+ }
4704
+ function notebookOfItem(item) {
4705
+ if (item.notebookId && item.notebookId.trim()) {
4706
+ return wodWikiCore.toNotebookId(item.notebookId);
4707
+ }
4708
+ if (item.notebookIds?.length) {
4709
+ const nonDefaultUser = item.notebookIds.map((id) => wodWikiCore.toNotebookId(id)).find((id) => id !== "notebook/journal" && id !== "notebook/playground");
4710
+ if (nonDefaultUser) return nonDefaultUser;
4711
+ return wodWikiCore.toNotebookId(item.notebookIds[0]);
4712
+ }
4713
+ if (item.tags) {
4714
+ for (const tag of item.tags) {
4715
+ if (tag.startsWith("notebook:") || tag.startsWith("notebook/")) {
4716
+ const slug = wodWikiCore.notebookSlugFromId(tag);
4717
+ if (slug) return wodWikiCore.toNotebookId(slug);
4718
+ }
4719
+ }
4720
+ }
4721
+ return resolveIntrinsicNotebook(item);
4722
+ }
3002
4723
  function notebookOriginOfItem(item) {
3003
4724
  const notebook = notebookOfItem(item);
3004
4725
  if (!notebook) return void 0;
4726
+ if (notebook === "notebook/defaults") return "default";
3005
4727
  if (item.notebookId?.trim()) return "user";
4728
+ if (item.notebookIds?.some((id) => {
4729
+ const norm = wodWikiCore.toNotebookId(id);
4730
+ return norm !== "notebook/journal" && norm !== "notebook/playground" && norm !== "notebook/defaults";
4731
+ })) return "user";
3006
4732
  if (item.tags?.some((t2) => t2.startsWith("notebook:") || t2.startsWith("notebook/"))) return "user";
3007
4733
  if (notebook === "notebook/journal" || notebook === "notebook/playground") return "default";
3008
4734
  return "feed";
@@ -3087,9 +4813,9 @@ function applyNotebookFilter(items, filters) {
3087
4813
  if (filter.key !== "notebook") continue;
3088
4814
  const wanted = new Set(filter.values.map((v) => wodWikiCore.toNotebookId(v.value)));
3089
4815
  items = items.filter((item) => {
3090
- const nb = notebookOfItem(item);
3091
- if (!nb) return filter.negate;
3092
- const hit = wanted.has(nb);
4816
+ const memberships = notebooksOfItem(item);
4817
+ if (memberships.length === 0) return filter.negate;
4818
+ const hit = memberships.some((nb) => wanted.has(nb));
3093
4819
  return filter.negate ? !hit : hit;
3094
4820
  });
3095
4821
  }
@@ -3225,14 +4951,38 @@ function civilDatesCoveredByRange(start, end, timeZone) {
3225
4951
  }
3226
4952
  return dates;
3227
4953
  }
3228
- function factTagValue(row, key, noteTags) {
4954
+ var EMPTY_FACT_DIMS = {
4955
+ noteTags: /* @__PURE__ */ new Map(),
4956
+ typedNoteTags: /* @__PURE__ */ new Map(),
4957
+ effortsBySlug: /* @__PURE__ */ new Map()
4958
+ };
4959
+ function resolveDimensionContext(dimsOrNoteTags) {
4960
+ if ("noteTags" in dimsOrNoteTags) return dimsOrNoteTags;
4961
+ return { ...EMPTY_FACT_DIMS, noteTags: dimsOrNoteTags };
4962
+ }
4963
+ var TYPED_TAG_KEY_SET = new Set(WQL_TYPED_TAG_KEYS);
4964
+ function factTagValue(row, key, dimsOrNoteTags = EMPTY_FACT_DIMS) {
4965
+ const ctx = resolveDimensionContext(dimsOrNoteTags);
4966
+ const effort = row.effortSlug ? ctx.effortsBySlug.get(row.effortSlug) : void 0;
3229
4967
  switch (key) {
3230
4968
  case "effort":
3231
4969
  return row.effortSlug;
3232
- case "discipline":
3233
- return row.discipline;
3234
- case "intensity":
3235
- return row.intensityTier;
4970
+ case "discipline": {
4971
+ const explicit = row.discipline ?? row.dimensions?.["discipline"];
4972
+ if (explicit) return explicit;
4973
+ const fromEffort = effort?.baseAttributes?.discipline;
4974
+ if (fromEffort) return fromEffort;
4975
+ const fromNote = ctx.typedNoteTags.get(row.noteId)?.get("discipline");
4976
+ return fromNote && fromNote.length > 0 ? fromNote : void 0;
4977
+ }
4978
+ case "intensity": {
4979
+ const explicit = row.intensityTier ?? row.dimensions?.["intensity"];
4980
+ if (explicit) return explicit;
4981
+ const fromEffort = effort?.baseAttributes?.intensityTier;
4982
+ if (fromEffort) return fromEffort;
4983
+ const fromNote = ctx.typedNoteTags.get(row.noteId)?.get("intensity");
4984
+ return fromNote && fromNote.length > 0 ? fromNote : void 0;
4985
+ }
3236
4986
  case "grade":
3237
4987
  return row.grade;
3238
4988
  case "note":
@@ -3250,15 +5000,27 @@ function factTagValue(row, key, noteTags) {
3250
5000
  case "result":
3251
5001
  return row.resultId;
3252
5002
  case "tags":
3253
- return noteTags.get(row.noteId) ?? [];
5003
+ return ctx.noteTags.get(row.noteId) ?? [];
5004
+ case "equipment": {
5005
+ const explicit = row.dimensions?.["equipment"];
5006
+ if (explicit) return explicit;
5007
+ if (effort?.equipment && effort.equipment.length > 0) return effort.equipment;
5008
+ const fromNote = ctx.typedNoteTags.get(row.noteId)?.get("equipment");
5009
+ return fromNote && fromNote.length > 0 ? fromNote : void 0;
5010
+ }
3254
5011
  default: {
3255
- const dims = row.dimensions;
3256
- if (!dims) return void 0;
3257
- return dims[key] ?? dims[wodWikiCore.normalizeFieldComponent(key)];
5012
+ const rowDims = row.dimensions;
5013
+ const explicit = rowDims ? rowDims[key] ?? rowDims[wodWikiCore.normalizeFieldComponent(key)] : void 0;
5014
+ if (explicit !== void 0) return explicit;
5015
+ if (TYPED_TAG_KEY_SET.has(key)) {
5016
+ const fromNote = ctx.typedNoteTags.get(row.noteId)?.get(key);
5017
+ if (fromNote && fromNote.length > 0) return fromNote;
5018
+ }
5019
+ return void 0;
3258
5020
  }
3259
5021
  }
3260
5022
  }
3261
- function matchesFilters(row, filters, noteTags) {
5023
+ function matchesFilters(row, filters, dimsOrNoteTags) {
3262
5024
  const groups = /* @__PURE__ */ new Map();
3263
5025
  for (const f of filters) {
3264
5026
  const groupKey = `${f.negate ? "!" : ""}${f.key}`;
@@ -3269,7 +5031,7 @@ function matchesFilters(row, filters, noteTags) {
3269
5031
  return [...groups.values()].every((group) => {
3270
5032
  const key = group[0].key;
3271
5033
  const negate = group[0].negate;
3272
- const raw = factTagValue(row, key, noteTags);
5034
+ const raw = factTagValue(row, key, dimsOrNoteTags);
3273
5035
  const rowValues = Array.isArray(raw) ? raw : raw !== void 0 ? [raw] : [];
3274
5036
  const hit = rowValues.some(
3275
5037
  (value) => group.some(
@@ -3281,11 +5043,11 @@ function matchesFilters(row, filters, noteTags) {
3281
5043
  return negate ? !hit : hit;
3282
5044
  });
3283
5045
  }
3284
- function dimValue(row, dim, noteTags, ctx) {
5046
+ function dimValue(row, dim, dimsOrNoteTags, ctx) {
3285
5047
  if (dim === "day") return row.metricDate ?? civilDateOf(row.timestamp, ctx.timeZone);
3286
5048
  if (dim === "week") return civilMonday(row.metricDate ?? civilDateOf(row.timestamp, ctx.timeZone));
3287
5049
  if (dim === "session") return row.resultId;
3288
- const raw = factTagValue(row, dim, noteTags);
5050
+ const raw = factTagValue(row, dim, dimsOrNoteTags);
3289
5051
  if (raw === void 0) return UNASSIGNED;
3290
5052
  if (typeof raw === "string") return raw;
3291
5053
  return raw.length ? raw.join(",") : UNASSIGNED;
@@ -3443,11 +5205,15 @@ var QueryService = class {
3443
5205
  relationStore;
3444
5206
  datasetStore;
3445
5207
  isNotebookSubscribed;
3446
- // Optional storage seam properties for block_efforts and typed tag resolution.
5208
+ feedCatalogStore;
5209
+ // Optional storage seam properties for block_efforts, block_equipment, and typed tag resolution.
3447
5210
  // Injected by the app adapter (`services/queryService.ts`); tests can supply fakes.
3448
5211
  blockEffortsStore = {
3449
5212
  getAllFromIndex: async () => []
3450
5213
  };
5214
+ blockEquipmentStore = {
5215
+ getAllFromIndex: async () => []
5216
+ };
3451
5217
  tagsStore = {
3452
5218
  getAllFromIndex: async () => []
3453
5219
  };
@@ -3455,7 +5221,7 @@ var QueryService = class {
3455
5221
  getAllFromIndex: async () => []
3456
5222
  };
3457
5223
  constructor(storesOrEventStore, noteStore, blockStore, effortStore) {
3458
- if (storesOrEventStore && typeof storesOrEventStore === "object" && ("eventStore" in storesOrEventStore || "noteStore" in storesOrEventStore || "blockStore" in storesOrEventStore || "effortStore" in storesOrEventStore || "pageStore" in storesOrEventStore || "relationStore" in storesOrEventStore || "datasetStore" in storesOrEventStore)) {
5224
+ if (storesOrEventStore && typeof storesOrEventStore === "object" && ("eventStore" in storesOrEventStore || "noteStore" in storesOrEventStore || "blockStore" in storesOrEventStore || "effortStore" in storesOrEventStore || "blockEffortsStore" in storesOrEventStore || "blockEquipmentStore" in storesOrEventStore || "tagsStore" in storesOrEventStore || "noteTagsStore" in storesOrEventStore || "pageStore" in storesOrEventStore || "relationStore" in storesOrEventStore || "datasetStore" in storesOrEventStore || "feedCatalogStore" in storesOrEventStore || "isNotebookSubscribed" in storesOrEventStore)) {
3459
5225
  const stores = storesOrEventStore;
3460
5226
  this.store = stores.eventStore ?? defaultEventStore;
3461
5227
  this.noteStore = stores.noteStore ?? defaultNoteStore;
@@ -3465,9 +5231,11 @@ var QueryService = class {
3465
5231
  this.relationStore = stores.relationStore;
3466
5232
  this.datasetStore = stores.datasetStore;
3467
5233
  if (stores.blockEffortsStore) this.blockEffortsStore = stores.blockEffortsStore;
5234
+ if (stores.blockEquipmentStore) this.blockEquipmentStore = stores.blockEquipmentStore;
3468
5235
  if (stores.tagsStore) this.tagsStore = stores.tagsStore;
3469
5236
  if (stores.noteTagsStore) this.noteTagsStore = stores.noteTagsStore;
3470
5237
  if (stores.isNotebookSubscribed) this.isNotebookSubscribed = stores.isNotebookSubscribed;
5238
+ if (stores.feedCatalogStore) this.feedCatalogStore = stores.feedCatalogStore;
3471
5239
  } else {
3472
5240
  this.store = storesOrEventStore ?? defaultEventStore;
3473
5241
  this.noteStore = noteStore ?? defaultNoteStore;
@@ -3478,7 +5246,7 @@ var QueryService = class {
3478
5246
  /** Windowed flat-fact fetch — the projected view over the event store. */
3479
5247
  async getFactsByTimeRange(start, end) {
3480
5248
  const rows = await this.store.getEventsByTimeRange(start, end);
3481
- return rows.flatMap(projectEventToFacts);
5249
+ return rows.flatMap((row) => projectEventToFacts(row));
3482
5250
  }
3483
5251
  async runQuery(raw, options2 = {}) {
3484
5252
  const parsed = parseQuery(raw);
@@ -3553,13 +5321,15 @@ var QueryService = class {
3553
5321
  groups.sort((a, b) => sessionCompletedAt(b[1]) - sessionCompletedAt(a[1]));
3554
5322
  const planeFilters = parsed.filters.filter((f) => f.key === "plane");
3555
5323
  const factFilters = parsed.filters.filter((f) => !["result", "block", "note", "plane"].includes(f.key));
3556
- const touchesTags = factFilters.some((f) => isTagLabelKey(f.key));
3557
- let sessionNoteTags = /* @__PURE__ */ new Map();
3558
- if (touchesTags && groups.length > 0) {
3559
- const dummyPoints = groups.map(([, rows]) => ({ noteId: rows[0]?.noteId ?? "" }));
3560
- sessionNoteTags = await this.loadNoteTags(dummyPoints, true);
3561
- }
3562
5324
  const normalizedFactFilters = factFilters.map((f) => f.key === "tag" ? { ...f, key: "tags" } : f);
5325
+ const sessionDims = groups.length > 0 && normalizedFactFilters.length > 0 ? await this.loadFactDimensions(
5326
+ groups.map(([, rows]) => ({
5327
+ noteId: rows[0]?.noteId ?? "",
5328
+ effortSlug: rows.find((r) => r.effortSlug)?.effortSlug
5329
+ })),
5330
+ normalizedFactFilters,
5331
+ []
5332
+ ) : EMPTY_FACT_DIMS;
3563
5333
  let runs = groups.map(([resultId, rows]) => {
3564
5334
  let events = rows;
3565
5335
  if (planeFilters.length > 0) {
@@ -3573,7 +5343,7 @@ var QueryService = class {
3573
5343
  if (normalizedFactFilters.length > 0) {
3574
5344
  const matchingSession = events.some((row) => {
3575
5345
  const facts = projectEventToFacts(row);
3576
- return facts.some((fact) => matchesFilters(fact, normalizedFactFilters, sessionNoteTags));
5346
+ return facts.some((fact) => matchesFilters(fact, normalizedFactFilters, sessionDims));
3577
5347
  });
3578
5348
  if (!matchingSession) events = [];
3579
5349
  }
@@ -3667,14 +5437,17 @@ var QueryService = class {
3667
5437
  };
3668
5438
  const rawTagFilters = parsed.filters.filter((f) => !["result", "block", "note", "plane"].includes(f.key));
3669
5439
  const tagFilters = rawTagFilters.map((f) => f.key === "tag" ? { ...f, key: "tags" } : f);
3670
- const touchesTags = tagFilters.some((f) => f.key === "tags");
3671
- const noteTags = touchesTags && eligibleRecords.length > 0 ? await this.loadNoteTags(eligibleRecords.map((r) => ({ noteId: r.noteId })), true) : /* @__PURE__ */ new Map();
5440
+ const tableDims = eligibleRecords.length > 0 && tagFilters.length > 0 ? await this.loadFactDimensions(
5441
+ eligibleRecords.map((r) => ({ noteId: r.noteId, effortSlug: r.effortSlug })),
5442
+ tagFilters,
5443
+ []
5444
+ ) : EMPTY_FACT_DIMS;
3672
5445
  const timeScoped = !!(parsed.window || options2.range);
3673
5446
  const eligible = eligibleRecords.filter((row) => !timeScoped || effectiveTimeWindow(anchorTs(row), parsed.window, options2.range, ctx)).filter((row) => {
3674
5447
  if (tagFilters.length === 0) return true;
3675
5448
  const facts = projectEventToFacts(row);
3676
5449
  if (facts.length === 0) return false;
3677
- return facts.some((fact) => matchesFilters(fact, tagFilters, noteTags));
5450
+ return facts.some((fact) => matchesFilters(fact, tagFilters, tableDims));
3678
5451
  });
3679
5452
  const totalCount = eligible.length;
3680
5453
  const unitByMetric = /* @__PURE__ */ new Map();
@@ -3707,7 +5480,13 @@ var QueryService = class {
3707
5480
  return { name: col, type: "number", ...unitFor(col) ? { unit: unitFor(col) } : {} };
3708
5481
  });
3709
5482
  const typeOf = new Map(columns.map((c) => [c.name, c.type]));
3710
- let records = eligible.map((row) => {
5483
+ const offset = pipes?.offset ?? 0;
5484
+ const limit = pipes?.limit;
5485
+ const hasOrder = Boolean(pipes?.order && pipes.order.length > 0);
5486
+ const hasGroupBy = Boolean(parsed.groupBy && parsed.groupBy.length > 0);
5487
+ const canSliceBeforeProject = !hasOrder && !hasGroupBy && (limit !== void 0 || offset > 0);
5488
+ const rowsToProject = canSliceBeforeProject ? limit !== void 0 ? eligible.slice(offset, offset + limit) : eligible.slice(offset) : eligible;
5489
+ let records = rowsToProject.map((row) => {
3711
5490
  const record = {};
3712
5491
  for (const col of selectCols) {
3713
5492
  const type = typeOf.get(col);
@@ -3761,9 +5540,7 @@ var QueryService = class {
3761
5540
  }
3762
5541
  groups = [...groupMap.entries()].map(([key, rows]) => ({ key, label: key, rows }));
3763
5542
  }
3764
- const offset = pipes?.offset ?? 0;
3765
- const limit = pipes?.limit;
3766
- const page = limit !== void 0 ? records.slice(offset, offset + limit) : records.slice(offset);
5543
+ const page = canSliceBeforeProject ? records : limit !== void 0 ? records.slice(offset, offset + limit) : records.slice(offset);
3767
5544
  return {
3768
5545
  parsed,
3769
5546
  notes: [],
@@ -3790,6 +5567,17 @@ var QueryService = class {
3790
5567
  if (parsed.error) {
3791
5568
  return { parsed, notes: [], blocks: [], stages: { selected: 0, matched: 0 } };
3792
5569
  }
5570
+ const bound = bindQueryParams(parsed, options2.params ?? {});
5571
+ if (bound.missing.length > 0) {
5572
+ const errored = { ...parsed, error: unknownTokensMessage(bound.missing) };
5573
+ return { parsed: errored, notes: [], blocks: [], stages: { selected: 0, matched: 0 } };
5574
+ }
5575
+ if (bound.parsed.family === "find") {
5576
+ parsed = bound.parsed;
5577
+ if (parsed.error) {
5578
+ return { parsed, notes: [], blocks: [], stages: { selected: 0, matched: 0 } };
5579
+ }
5580
+ }
3793
5581
  const computed = findTargetAdvisories(parsed);
3794
5582
  if (computed.length) {
3795
5583
  const merged = [.../* @__PURE__ */ new Set([...parsed.advisories ?? [], ...computed])];
@@ -3819,6 +5607,9 @@ var QueryService = class {
3819
5607
  if (parsed.target === "equipment") {
3820
5608
  return this.runFindEquipment(parsed, options2);
3821
5609
  }
5610
+ if (parsed.target === "feed") {
5611
+ return this.runFindFeed(parsed, options2);
5612
+ }
3822
5613
  const read = compileContentRead(parsed.filters, "note", parsed.sourceScope);
3823
5614
  const paging = domainPaging(parsed, options2, read.residual);
3824
5615
  const fetchedNotes = (read.filters.length > 0 || read.selection.length > 0 || paging.paged) && this.noteStore.queryDomain ? await this.noteStore.queryDomain({
@@ -3888,6 +5679,31 @@ var QueryService = class {
3888
5679
  }
3889
5680
  }
3890
5681
  notes = applyNotebookFilter(notes, parsed.filters);
5682
+ if (notes.length === 0 && this.feedCatalogStore?.getRemoteNotebookNotes) {
5683
+ const posNbFilter = parsed.filters.find((f) => f.key === "notebook" && !f.negate && f.values.length === 1 && !f.values[0].wildcard);
5684
+ const subFilter = parsed.filters.find((f) => f.key === "subscribed" && !f.negate);
5685
+ const explicitlySubscribedOnly = subFilter !== void 0 && subFilter.values.some((v) => v.value === "true") && !subFilter.values.some((v) => v.value === "false");
5686
+ if (posNbFilter && !explicitlySubscribedOnly) {
5687
+ const wantedSlug = wodWikiCore.notebookSlugFromId(posNbFilter.values[0].value);
5688
+ const posFeedFilter = parsed.filters.find((f) => f.key === "feed" && !f.negate && f.values.length === 1 && !f.values[0].wildcard);
5689
+ let remoteNotes = await this.feedCatalogStore.getRemoteNotebookNotes(
5690
+ wantedSlug,
5691
+ posFeedFilter?.values[0]?.value
5692
+ );
5693
+ for (const filter of parsed.filters) {
5694
+ if (filter.key === "text") {
5695
+ const search = filter.values.map((v) => v.value).join(" ").toLowerCase();
5696
+ remoteNotes = remoteNotes.filter((n) => {
5697
+ const hit = n.title.toLowerCase().includes(search);
5698
+ return filter.negate ? !hit : hit;
5699
+ });
5700
+ }
5701
+ }
5702
+ if (remoteNotes.length > 0) {
5703
+ notes = remoteNotes;
5704
+ }
5705
+ }
5706
+ }
3891
5707
  const isSubscribedFn = options2.isNotebookSubscribed ?? this.isNotebookSubscribed ?? (() => true);
3892
5708
  for (const filter of parsed.filters) {
3893
5709
  if (filter.key === "feed") {
@@ -3949,7 +5765,7 @@ var QueryService = class {
3949
5765
  const offset = pipes.offset ?? 0;
3950
5766
  notes = pipes.limit !== void 0 ? notes.slice(offset, offset + pipes.limit) : notes.slice(offset);
3951
5767
  }
3952
- const relations = notes.length && (this.pageStore || this.relationStore) ? await this.containersAndPlacements(notes) : void 0;
5768
+ const relations = !options2.skipContainers && notes.length && (this.pageStore || this.relationStore) ? await this.containersAndPlacements(notes) : void 0;
3953
5769
  return {
3954
5770
  parsed,
3955
5771
  notes,
@@ -4221,7 +6037,15 @@ var QueryService = class {
4221
6037
  stages: { selected: 0, buckets: 0, aggregated: 0, groups: 0 },
4222
6038
  matched: []
4223
6039
  };
4224
- if (parsed.error) return empty;
6040
+ const bound = bindQueryParams(parsed, options2.params ?? {});
6041
+ if (bound.missing.length > 0) {
6042
+ const err = unknownTokensMessage(bound.missing);
6043
+ return { ...empty, parsed: { ...parsed, error: err }, error: err };
6044
+ }
6045
+ if (bound.parsed.family === "aggregate") {
6046
+ parsed = bound.parsed;
6047
+ }
6048
+ if (parsed.error) return { ...empty, parsed, error: parsed.error };
4225
6049
  if (parsed.join) return this.runJoined(parsed, options2);
4226
6050
  const ctx = runContext(options2);
4227
6051
  const range = resolveAggregateRange(parsed, options2, ctx);
@@ -4254,7 +6078,15 @@ var QueryService = class {
4254
6078
  stages: { selected: 0, buckets: 0, aggregated: 0, groups: 0 },
4255
6079
  matched: []
4256
6080
  };
4257
- if (query.error) return empty;
6081
+ const bound = bindQueryParams(query, options2.params ?? {});
6082
+ if (bound.missing.length > 0) {
6083
+ const err = unknownTokensMessage(bound.missing);
6084
+ return { ...empty, parsed: { ...query, error: err }, error: err };
6085
+ }
6086
+ if (bound.parsed.family === "aggregate") {
6087
+ query = bound.parsed;
6088
+ }
6089
+ if (query.error) return { ...empty, parsed: query, error: query.error };
4258
6090
  if (query.join) {
4259
6091
  return {
4260
6092
  ...empty,
@@ -4279,33 +6111,41 @@ var QueryService = class {
4279
6111
  const error = parsed.error ?? `Not a pipeline: "${parsed.raw}". Pipelines look like :source{filters} | :sum{metric:x} | :table.`;
4280
6112
  return { parsed: { family: "pipeline", raw: parsed.raw, source: { kind: "dataset", name: "" }, transforms: [], error }, error };
4281
6113
  }
4282
- const fail = (error) => ({ parsed, error });
4283
- if (parsed.error) return fail(parsed.error);
6114
+ let pipeline = parsed;
6115
+ const fail = (error) => ({ parsed: pipeline, error });
6116
+ if (pipeline.error) return fail(pipeline.error);
6117
+ const bound = bindQueryParams(pipeline, options2.params ?? {});
6118
+ if (bound.missing.length > 0) return fail(unknownTokensMessage(bound.missing));
6119
+ if (bound.parsed.family === "pipeline") {
6120
+ pipeline = bound.parsed;
6121
+ if (pipeline.error) return fail(pipeline.error);
6122
+ }
4284
6123
  let content;
4285
6124
  let carried;
4286
6125
  let sourceEvents;
4287
6126
  let datasetNotes = [];
4288
- if (parsed.source.kind === "dataset") {
4289
- const name = parsed.source.name;
6127
+ if (pipeline.source.kind === "dataset") {
6128
+ const name = pipeline.source.name;
4290
6129
  const dataset = options2.pageSources?.get(name) ?? this.datasetStore?.getDataset(name);
4291
6130
  if (!dataset) return fail(`Unknown dataset "${name}" \u2014 the host must register it before the query runs.`);
4292
6131
  sourceEvents = dataset.events;
4293
6132
  datasetNotes = dataset.notes;
4294
- } else if (isFindQuery(parsed.source.query)) {
4295
- const q = parsed.source.query;
6133
+ } else if (isFindQuery(pipeline.source.query)) {
6134
+ const q = pipeline.source.query;
4296
6135
  content = await this.runFind(q, {
4297
6136
  context: options2.context,
6137
+ skipContainers: pipeline.transforms.length > 0,
4298
6138
  ...options2.rangeStart !== void 0 || options2.rangeEnd !== void 0 ? { range: { start: options2.rangeStart ?? 0, end: options2.rangeEnd ?? Number.MAX_SAFE_INTEGER, endExclusive: false } } : {}
4299
6139
  });
4300
6140
  if (q.error) return fail(q.error);
4301
6141
  } else {
4302
- carried = await this.run(parsed.source.query, options2);
4303
- if (parsed.source.query.error) return fail(parsed.source.query.error);
6142
+ carried = await this.run(pipeline.source.query, options2);
6143
+ if (pipeline.source.query.error) return fail(pipeline.source.query.error);
4304
6144
  }
4305
- if (parsed.transforms.length === 0) {
6145
+ if (pipeline.transforms.length === 0) {
4306
6146
  if (content) {
4307
6147
  return {
4308
- parsed,
6148
+ parsed: pipeline,
4309
6149
  ...content.notes.length ? { notes: content.notes } : {},
4310
6150
  ...content.blocks.length ? { blocks: content.blocks } : {},
4311
6151
  ...content.efforts?.length ? { efforts: content.efforts } : {},
@@ -4313,37 +6153,81 @@ var QueryService = class {
4313
6153
  ...content.table ? { table: content.table } : {},
4314
6154
  ...content.containers ? { containers: content.containers } : {},
4315
6155
  ...content.placements ? { placements: content.placements } : {},
4316
- ...parsed.sink ? { chart: parsed.sink } : {}
6156
+ ...pipeline.sink ? { chart: pipeline.sink } : {}
4317
6157
  };
4318
6158
  }
4319
- if (parsed.source.kind === "dataset") {
4320
- return { parsed, notes: datasetNotes, ...parsed.sink ? { chart: parsed.sink } : {} };
6159
+ if (pipeline.source.kind === "dataset") {
6160
+ return { parsed: pipeline, notes: datasetNotes, ...pipeline.sink ? { chart: pipeline.sink } : {} };
4321
6161
  }
4322
6162
  return {
4323
- parsed,
6163
+ parsed: pipeline,
4324
6164
  ...carried ? { series: carried.series, ...carried.unit ? { unit: carried.unit } : {}, ...carried.error ? { error: carried.error } : {} } : {},
4325
- ...parsed.sink ? { chart: parsed.sink } : {}
6165
+ ...pipeline.sink ? { chart: pipeline.sink } : {}
4326
6166
  };
4327
6167
  }
4328
6168
  if (!carried) {
4329
6169
  if (!sourceEvents && content) sourceEvents = await this.sourceEventsFromFind(content);
4330
- carried = await this.runAggregateEvents(parsed.transforms[0], sourceEvents ?? [], options2);
4331
- for (const transform of parsed.transforms.slice(1)) {
6170
+ carried = await this.runAggregateEvents(pipeline.transforms[0], sourceEvents ?? [], options2);
6171
+ for (const transform of pipeline.transforms.slice(1)) {
4332
6172
  if (carried.error) break;
4333
6173
  carried = reduceCarriedResult(carried, transform, options2, runContext(options2));
4334
6174
  }
4335
6175
  } else {
4336
- for (const transform of parsed.transforms) {
6176
+ for (const transform of pipeline.transforms) {
4337
6177
  if (carried.error) break;
4338
6178
  carried = reduceCarriedResult(carried, transform, options2, runContext(options2));
4339
6179
  }
4340
6180
  }
4341
6181
  return {
4342
- parsed,
6182
+ parsed: pipeline,
6183
+ series: carried.series,
6184
+ ...carried.unit ? { unit: carried.unit } : {},
6185
+ ...carried.error ? { error: carried.error } : {},
6186
+ ...pipeline.sink ? { chart: pipeline.sink } : {}
6187
+ };
6188
+ }
6189
+ /**
6190
+ * Evaluate pipeline tail stages (`:sum{…} | :bar{…}`) over an already-fetched
6191
+ * `FindQueryResult` (or `FamilyRunPayload`). Resolves and caches `content.events`
6192
+ * on the first call so multiple downstream named branches (`kb = bench | :sum{…}`,
6193
+ * `bb = bench | :sum{…}`) share the exact `EventRecord[]` slice in memory.
6194
+ */
6195
+ async runPipelineFromFind(content, pipeTail, options2 = {}) {
6196
+ const synthetic = parseQuery(`:segment{} | ${pipeTail}`);
6197
+ if (!isPipelineQuery(synthetic) || synthetic.error) {
6198
+ const error = synthetic.error ?? `Invalid pipeline tail: "${pipeTail}"`;
6199
+ return { parsed: { family: "pipeline", raw: pipeTail, source: { kind: "dataset", name: "" }, transforms: [], error }, error };
6200
+ }
6201
+ if (!content.events) {
6202
+ content.events = await this.sourceEventsFromFind({
6203
+ parsed: { family: "find", raw: "", target: "note", filters: [] },
6204
+ notes: content.notes ?? [],
6205
+ blocks: content.blocks ?? [],
6206
+ stages: { selected: 0, matched: 0 }
6207
+ });
6208
+ }
6209
+ const sourceEvents = content.events;
6210
+ if (synthetic.transforms.length === 0) {
6211
+ return {
6212
+ parsed: synthetic,
6213
+ ...content.notes?.length ? { notes: content.notes } : {},
6214
+ ...content.blocks?.length ? { blocks: content.blocks } : {},
6215
+ ...content.runs?.length ? { runs: content.runs } : {},
6216
+ ...content.table ? { table: content.table } : {},
6217
+ ...synthetic.sink ? { chart: synthetic.sink } : {}
6218
+ };
6219
+ }
6220
+ let carried = await this.runAggregateEvents(synthetic.transforms[0], sourceEvents, options2);
6221
+ for (const transform of synthetic.transforms.slice(1)) {
6222
+ if (carried.error) break;
6223
+ carried = reduceCarriedResult(carried, transform, options2, runContext(options2));
6224
+ }
6225
+ return {
6226
+ parsed: synthetic,
4343
6227
  series: carried.series,
4344
6228
  ...carried.unit ? { unit: carried.unit } : {},
4345
6229
  ...carried.error ? { error: carried.error } : {},
4346
- ...parsed.sink ? { chart: parsed.sink } : {}
6230
+ ...synthetic.sink ? { chart: synthetic.sink } : {}
4347
6231
  };
4348
6232
  }
4349
6233
  /** Events behind a content source — resolved ONLY when a transform needs
@@ -4394,17 +6278,16 @@ var QueryService = class {
4394
6278
  error: `Aggregate "${parsed.agg}" requires a metric \u2014 use ${parsed.agg}{metric:<key>}`
4395
6279
  };
4396
6280
  }
4397
- const matchesMetric = (metricKey, queryMetric) => !queryMetric || metricKey === queryMetric || queryMetric === "rep" && metricKey === "reps" || queryMetric === "reps" && metricKey === "rep";
4398
6281
  const factTime = (row) => {
4399
6282
  if (row.metricDate) return zonedNoon(row.metricDate, ctx.timeZone);
4400
6283
  return row.timestamp;
4401
6284
  };
4402
- const candidates = eventRows.flatMap(projectEventToFacts).filter((row) => matchesMetric(row.metricKey, parsed.metric)).filter((row) => !range || inRange(factTime(row), range));
4403
- const touchesTags = parsed.filters.some((f) => f.key === "tags") || parsed.groupBy.includes("tags");
4404
- const noteTags = await this.loadNoteTags(candidates, touchesTags);
4405
- const matched = candidates.filter((row) => matchesFilters(row, parsed.filters, noteTags));
6285
+ const metricFilter = parsed.metric ? { metricKey: parsed.metric } : void 0;
6286
+ const candidates = eventRows.flatMap((row) => projectEventToFacts(row, metricFilter)).filter((row) => !range || inRange(factTime(row), range));
6287
+ const factDims = await this.loadFactDimensions(candidates, parsed.filters, parsed.groupBy);
6288
+ const matched = candidates.filter((row) => matchesFilters(row, parsed.filters, factDims));
4406
6289
  const { selected, report } = selectContributions(matched, parsed.agg);
4407
- return this.buildResult(selected, parsed, options2, noteTags, range, report);
6290
+ return this.buildResult(selected, parsed, options2, factDims, range, report);
4408
6291
  }
4409
6292
  /**
4410
6293
  * Stages 2–4 — BUCKET → GROUP → AGGREGATE over an already-selected +
@@ -4566,11 +6449,10 @@ var QueryService = class {
4566
6449
  if (joinRange) {
4567
6450
  facts = facts.filter((f) => inRange(f.timestamp, joinRange));
4568
6451
  }
4569
- const touchesTags = parsed.filters.some((f) => f.key === "tags") || parsed.groupBy.includes("tags");
4570
- const noteTags = await this.loadNoteTags(facts, touchesTags);
4571
- const matched = facts.filter((f) => matchesFilters(f, parsed.filters, noteTags));
6452
+ const factDims = await this.loadFactDimensions(facts, parsed.filters, parsed.groupBy);
6453
+ const matched = facts.filter((f) => matchesFilters(f, parsed.filters, factDims));
4572
6454
  const { selected, report } = selectContributions(matched, parsed.agg);
4573
- return this.buildResult(selected, parsed, options2, noteTags, joinRange, report);
6455
+ return this.buildResult(selected, parsed, options2, factDims, joinRange, report);
4574
6456
  }
4575
6457
  /** Direction 1 — keep only content owning a wod block whose raw-log metric
4576
6458
  * aggregate satisfies the predicate. `notes` for :note, `blocks` for
@@ -4593,9 +6475,9 @@ var QueryService = class {
4593
6475
  * and return the ids whose aggregate satisfies the join predicate. */
4594
6476
  async contentIdsSatisfying(contentIds, join) {
4595
6477
  const facts = dedupeById(await this.deriveMetricFacts(contentIds, join.metric));
4596
- const noteTags = await this.loadNoteTags(facts, join.filters.some((f) => f.key === "tags"));
6478
+ const factDims = await this.loadFactDimensions(facts, join.filters, []);
4597
6479
  const filtered = selectContributions(
4598
- facts.filter((f) => matchesFilters(f, join.filters, noteTags)),
6480
+ facts.filter((f) => matchesFilters(f, join.filters, factDims)),
4599
6481
  join.agg
4600
6482
  ).selected;
4601
6483
  const byContent = /* @__PURE__ */ new Map();
@@ -4621,7 +6503,8 @@ var QueryService = class {
4621
6503
  async deriveMetricFacts(contentIds, metricKey) {
4622
6504
  const ids = [...new Set(contentIds)];
4623
6505
  const rows = await Promise.all(ids.map((blockContentId) => this.store.getEventsByContent(blockContentId)));
4624
- return rows.flat().flatMap(projectEventToFacts).filter((f) => f.metricKey === metricKey);
6506
+ const metricFilter = metricKey ? { metricKey } : void 0;
6507
+ return rows.flat().flatMap((row) => projectEventToFacts(row, metricFilter));
4625
6508
  }
4626
6509
  /** All content blocks — journal and seeded corpus in one store. */
4627
6510
  async allContentBlocks() {
@@ -4654,22 +6537,78 @@ var QueryService = class {
4654
6537
  }
4655
6538
  return ids;
4656
6539
  }
4657
- /** Load note tag labels only when the query touches 'tags'. Batched when
4658
- * a relationStore is injected — one bundle read per run instead of a
4659
- * junction read per row's note. */
4660
- async loadNoteTags(rows, touchesTags) {
6540
+ /**
6541
+ * Load 3-tier fact dimensions (`noteTags`, `typedNoteTags`, `effortsBySlug`)
6542
+ * in at most one batched `relationStore.getBatches` call and one
6543
+ * `effortStore.getAllEfforts` call per query run.
6544
+ */
6545
+ async loadFactDimensions(rows, filters, groupBy) {
6546
+ if (rows.length === 0) return EMPTY_FACT_DIMS;
6547
+ const keys = /* @__PURE__ */ new Set([...filters.map((f) => f.key), ...groupBy]);
6548
+ const needsUntypedTags = keys.has("tags") || keys.has("tag");
6549
+ const neededTypedKeys = [...keys].filter((k) => TYPED_TAG_KEY_SET.has(k) || k === "discipline" || k === "intensity");
6550
+ const needsTypedTags = neededTypedKeys.length > 0;
6551
+ const needsEfforts = keys.has("equipment") || keys.has("discipline") || keys.has("intensity");
4661
6552
  const noteTags = /* @__PURE__ */ new Map();
4662
- if (!touchesTags) return noteTags;
4663
- const noteIds = [...new Set(rows.map((r) => r.noteId))];
4664
- if (this.relationStore) {
4665
- const bundles = await this.relationStore.getBatches(noteIds);
4666
- for (const id of noteIds) {
4667
- noteTags.set(id, (bundles.get(id)?.tags ?? []).map((t2) => t2.label));
6553
+ const typedNoteTags = /* @__PURE__ */ new Map();
6554
+ const effortsBySlug = /* @__PURE__ */ new Map();
6555
+ if (needsEfforts) {
6556
+ const efforts = await this.effortStore.getAllEfforts();
6557
+ for (const effort of efforts) {
6558
+ effortsBySlug.set(effort.slug, effort);
6559
+ }
6560
+ }
6561
+ if (needsUntypedTags || needsTypedTags) {
6562
+ const noteIds = [...new Set(rows.map((r) => r.noteId).filter(Boolean))];
6563
+ if (this.relationStore) {
6564
+ const bundles = await this.relationStore.getBatches(noteIds);
6565
+ for (const id of noteIds) {
6566
+ const tags = bundles.get(id)?.tags ?? [];
6567
+ if (needsUntypedTags) {
6568
+ noteTags.set(id, tags.map((t2) => t2.label));
6569
+ }
6570
+ if (needsTypedTags) {
6571
+ const byType = /* @__PURE__ */ new Map();
6572
+ for (const tag of tags) {
6573
+ if (!tag.type) continue;
6574
+ const list = byType.get(tag.type);
6575
+ if (list) {
6576
+ if (!list.includes(tag.label)) list.push(tag.label);
6577
+ } else {
6578
+ byType.set(tag.type, [tag.label]);
6579
+ }
6580
+ }
6581
+ typedNoteTags.set(id, byType);
6582
+ }
6583
+ }
6584
+ } else {
6585
+ if (needsUntypedTags) {
6586
+ await Promise.all(noteIds.map(async (id) => noteTags.set(id, await this.noteStore.getNoteTagLabels(id))));
6587
+ }
6588
+ if (needsTypedTags && this.tagsStore.getAll) {
6589
+ const allTags = await this.tagsStore.getAll();
6590
+ const tagById = new Map(allTags.map((t2) => [t2.id, t2]));
6591
+ await Promise.all(
6592
+ noteIds.map(async (id) => {
6593
+ const links = await this.noteTagsStore.getAllFromIndex("by-note", id);
6594
+ const byType = /* @__PURE__ */ new Map();
6595
+ for (const link of links) {
6596
+ const tag = tagById.get(link.tagId);
6597
+ if (!tag?.type) continue;
6598
+ const list = byType.get(tag.type);
6599
+ if (list) {
6600
+ if (!list.includes(tag.label)) list.push(tag.label);
6601
+ } else {
6602
+ byType.set(tag.type, [tag.label]);
6603
+ }
6604
+ }
6605
+ typedNoteTags.set(id, byType);
6606
+ })
6607
+ );
6608
+ }
4668
6609
  }
4669
- return noteTags;
4670
6610
  }
4671
- await Promise.all(noteIds.map(async (id) => noteTags.set(id, await this.noteStore.getNoteTagLabels(id))));
4672
- return noteTags;
6611
+ return { noteTags, typedNoteTags, effortsBySlug };
4673
6612
  }
4674
6613
  /** Resolve matching note IDs for an exercise slug via the block_efforts containment index. */
4675
6614
  async getNoteIdsForEffort(effortSlug) {
@@ -4678,25 +6617,195 @@ var QueryService = class {
4678
6617
  }
4679
6618
  /** Resolve matching note IDs for a canonical typed tag (e.g. equipment:kettlebell). */
4680
6619
  async getNoteIdsForTypedTag(type, label) {
6620
+ const out = /* @__PURE__ */ new Set();
4681
6621
  const tags = await this.tagsStore.getAllFromIndex("by-label", label);
4682
6622
  const typedTag = tags.find((t2) => t2.type === type);
4683
- if (!typedTag) return /* @__PURE__ */ new Set();
4684
- const links = await this.noteTagsStore.getAllFromIndex("by-tag", typedTag.id);
4685
- return new Set(links.map((l) => l.noteId));
6623
+ if (typedTag) {
6624
+ const links = await this.noteTagsStore.getAllFromIndex("by-tag", typedTag.id);
6625
+ for (const link of links) out.add(link.noteId);
6626
+ }
6627
+ if (type === "equipment") {
6628
+ const slug = label.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
6629
+ const candidates = slug && slug !== label ? [label, slug] : [label];
6630
+ for (const key of candidates) {
6631
+ const rows = await this.blockEquipmentStore.getAllFromIndex("by-equipment", key);
6632
+ for (const row of rows) {
6633
+ if (row?.noteId) out.add(row.noteId);
6634
+ }
6635
+ }
6636
+ }
6637
+ return out;
6638
+ }
6639
+ /**
6640
+ * Execute a :notebook query (ticket 08) — the notebook plane is a NOTE
6641
+ * plane (ticket 05 semantics preserved): bare `:notebook{}` returns the
6642
+ * notes of every notebook — journal, playground, defaults, user-created,
6643
+ * feed-delivered. `notebook:{slug}` narrows to members of those
6644
+ * notebooks and `!notebook:` excludes them; the result shape never
6645
+ * changes with polarity, and container criteria (tags:, effort:, typed
6646
+ * tags, text:) narrow the members exactly like the note plane — this
6647
+ * delegates to it. The notebook CONTAINER listing (the /notebooks
6648
+ * management surface) is the app's NotebookService model, not WQL rows.
6649
+ */
6650
+ async runFindNotebook(parsed, options2 = {}) {
6651
+ const wantsContainers = options2.notebookPlane === "containers" || parsed.filters.some(
6652
+ (f) => f.key === "type" && !f.negate && f.values.some((v) => v.value === "notebook")
6653
+ );
6654
+ if (wantsContainers && this.feedCatalogStore) {
6655
+ return this.runFindNotebookContainers(parsed);
6656
+ }
6657
+ return this.runFind({ ...parsed, target: "note" }, options2);
6658
+ }
6659
+ /**
6660
+ * Execute `:feed{}` — the distributed Feed sources plane.
6661
+ * Returns one synthetic Note-shaped row per registered Feed source
6662
+ * (including "My Published Feed" when local public content or server snapshot exists).
6663
+ */
6664
+ async runFindFeed(parsed, _options = {}) {
6665
+ const allFeeds = this.feedCatalogStore ? await this.feedCatalogStore.getAllFeeds() : [];
6666
+ const selected = allFeeds.length;
6667
+ let matched = allFeeds;
6668
+ for (const filter of parsed.filters) {
6669
+ if (filter.key === "feed") {
6670
+ const wanted = new Set(filter.values.map((v) => v.value));
6671
+ const hasWildcard = filter.values.some((v) => v.wildcard);
6672
+ matched = matched.filter((f) => {
6673
+ const hit = hasWildcard ? f.id !== "local" : wanted.has(f.id);
6674
+ return filter.negate ? !hit : hit;
6675
+ });
6676
+ } else if (filter.key === "subscribed") {
6677
+ const vals = new Set(filter.values.map((v) => v.value));
6678
+ const wantsTrue = vals.has("true");
6679
+ const wantsFalse = vals.has("false");
6680
+ matched = matched.filter((f) => {
6681
+ const hit = wantsTrue && wantsFalse ? true : wantsTrue ? f.subscribed : wantsFalse ? !f.subscribed : true;
6682
+ return filter.negate ? !hit : hit;
6683
+ });
6684
+ } else if (filter.key === "text") {
6685
+ const search = filter.values.map((v) => v.value).join(" ").toLowerCase();
6686
+ matched = matched.filter((f) => {
6687
+ const hit = f.label.toLowerCase().includes(search) || (f.description ?? "").toLowerCase().includes(search) || f.url.toLowerCase().includes(search);
6688
+ return filter.negate ? !hit : hit;
6689
+ });
6690
+ } else if (isTagLabelKey(filter.key)) {
6691
+ const wanted = new Set(filter.values.map((v) => v.value.toLowerCase()));
6692
+ matched = matched.filter((f) => {
6693
+ const rowTags = [
6694
+ `tier:${f.tier}`,
6695
+ f.tier,
6696
+ f.subscribed ? "subscribed:true" : "subscribed:false",
6697
+ ...f.unpublishedChanges ? ["status:unpublished", "unpublished"] : [],
6698
+ ...f.updateAvailable ? ["status:update-available", "update-available"] : []
6699
+ ].map((t2) => t2.toLowerCase());
6700
+ const hit = rowTags.some((t2) => wanted.has(t2));
6701
+ return filter.negate ? !hit : hit;
6702
+ });
6703
+ }
6704
+ }
6705
+ let rows = matched.map((f) => {
6706
+ const summaryParts = [
6707
+ `${f.notebookCount} notebook${f.notebookCount === 1 ? "" : "s"}`,
6708
+ f.itemCount > 0 ? `${f.itemCount} notes` : "",
6709
+ f.unpublishedChanges ? "Unpublished changes" : "",
6710
+ f.updateAvailable ? `Update v${f.localVersion ?? 0} \u2192 v${f.remoteVersion}` : ""
6711
+ ].filter(Boolean);
6712
+ const body = [f.description ?? "", summaryParts.join(" \xB7 "), f.url].filter(Boolean).join("\n");
6713
+ return {
6714
+ id: `feed/${f.id}`,
6715
+ title: f.label,
6716
+ type: "feed",
6717
+ rawContent: body,
6718
+ sourceRevision: 1,
6719
+ blocks: {},
6720
+ createdAt: f.builtAt ? Date.parse(f.builtAt) || 0 : 0,
6721
+ sourceId: `feed:${f.id}`,
6722
+ sourcePath: f.url,
6723
+ ...f.id !== "local" ? { feedOrigin: "feed", feedSourceId: f.id, seedOrigin: "seed" } : { seedOrigin: "user" },
6724
+ tags: [
6725
+ `tier:${f.tier}`,
6726
+ f.subscribed ? "subscribed:true" : "subscribed:false",
6727
+ ...f.unpublishedChanges ? ["status:unpublished"] : [],
6728
+ ...f.updateAvailable ? ["status:update-available"] : []
6729
+ ]
6730
+ };
6731
+ });
6732
+ rows = applyOrderAndPage(rows, parsed);
6733
+ return { parsed, notes: rows, blocks: [], stages: { selected, matched: rows.length } };
4686
6734
  }
4687
6735
  /**
4688
- * Execute a :notebook query (ticket 08) — the notebook plane is a NOTE
4689
- * plane (ticket 05 semantics preserved): bare `:notebook{}` returns the
4690
- * notes of every notebook — journal, playground, defaults, user-created,
4691
- * feed-delivered. `notebook:{slug}` narrows to members of those
4692
- * notebooks and `!notebook:` excludes them; the result shape never
4693
- * changes with polarity, and container criteria (tags:, effort:, typed
4694
- * tags, text:) narrow the members exactly like the note plane — this
4695
- * delegates to it. The notebook CONTAINER listing (the /notebooks
4696
- * management surface) is the app's NotebookService model, not WQL rows.
6736
+ * Execute `:notebook{type:notebook, ...}` — the Notebook container listing
6737
+ * across user-owned local notebooks and manifest-discovered remote Feed notebooks.
4697
6738
  */
4698
- async runFindNotebook(parsed, options2 = {}) {
4699
- return this.runFind({ ...parsed, target: "note" }, options2);
6739
+ async runFindNotebookContainers(parsed) {
6740
+ const allNotebooks = this.feedCatalogStore ? await this.feedCatalogStore.getAllNotebooks() : [];
6741
+ const selected = allNotebooks.length;
6742
+ let matched = allNotebooks;
6743
+ for (const filter of parsed.filters) {
6744
+ if (filter.key === "feed") {
6745
+ const wanted = new Set(filter.values.map((v) => v.value));
6746
+ const hasWildcard = filter.values.some((v) => v.wildcard);
6747
+ matched = matched.filter((nb) => {
6748
+ const hit = hasWildcard ? nb.feedId !== "local" : wanted.has(nb.feedId);
6749
+ return filter.negate ? !hit : hit;
6750
+ });
6751
+ } else if (filter.key === "notebook") {
6752
+ const wanted = new Set(filter.values.map((v) => wodWikiCore.notebookSlugFromId(v.value)));
6753
+ matched = matched.filter((nb) => {
6754
+ const hit = wanted.has(nb.slug) || wanted.has(nb.id) || wanted.has(wodWikiCore.notebookSlugFromId(nb.chunkId));
6755
+ return filter.negate ? !hit : hit;
6756
+ });
6757
+ } else if (filter.key === "text") {
6758
+ const search = filter.values.map((v) => v.value).join(" ").toLowerCase();
6759
+ matched = matched.filter((nb) => {
6760
+ const hit = nb.label.toLowerCase().includes(search) || (nb.description ?? "").toLowerCase().includes(search) || nb.feedLabel.toLowerCase().includes(search);
6761
+ return filter.negate ? !hit : hit;
6762
+ });
6763
+ }
6764
+ }
6765
+ const subFilter = parsed.filters.find((f) => f.key === "subscribed");
6766
+ if (subFilter) {
6767
+ const vals = new Set(subFilter.values.map((v) => v.value));
6768
+ const wantsTrue = vals.has("true");
6769
+ const wantsFalse = vals.has("false");
6770
+ matched = matched.filter((nb) => {
6771
+ const isSub = nb.origin === "user" || nb.origin === "default" || nb.subscribed;
6772
+ const hit = wantsTrue && wantsFalse ? true : wantsTrue ? isSub : wantsFalse ? !isSub : false;
6773
+ return subFilter.negate ? !hit : hit;
6774
+ });
6775
+ } else {
6776
+ matched = matched.filter((nb) => nb.origin === "user" || nb.origin === "default" || nb.subscribed);
6777
+ }
6778
+ let rows = matched.map((nb) => {
6779
+ const isLocal = nb.origin === "user" || nb.origin === "default";
6780
+ return {
6781
+ id: isLocal ? `notebook/${nb.slug}` : `feed-notebook/${nb.feedId}/${nb.slug}`,
6782
+ title: nb.label,
6783
+ type: "notebook",
6784
+ rawContent: nb.description ?? "",
6785
+ sourceRevision: 1,
6786
+ blocks: {},
6787
+ createdAt: nb.updatedAt ? Date.parse(nb.updatedAt) || 0 : 0,
6788
+ notebookId: `notebook/${nb.slug}`,
6789
+ sourceId: `notebook:${nb.slug}`,
6790
+ ...nb.feedUrl ? { sourcePath: nb.feedUrl } : {},
6791
+ ...isLocal ? { seedOrigin: "user", ...nb.public !== void 0 ? { public: nb.public } : {} } : {
6792
+ feedOrigin: "feed",
6793
+ feedSourceId: nb.feedId,
6794
+ seedOrigin: "seed",
6795
+ seedChunkId: nb.chunkId,
6796
+ ...nb.version !== void 0 ? { seedVersion: nb.version } : {}
6797
+ },
6798
+ tags: [
6799
+ ...nb.category ? [`category:${nb.category}`] : [],
6800
+ `origin:${nb.origin}`,
6801
+ `count:${nb.count}`,
6802
+ nb.subscribed ? "subscribed:true" : "subscribed:false",
6803
+ ...nb.public ? ["visibility:public"] : []
6804
+ ]
6805
+ };
6806
+ });
6807
+ rows = applyOrderAndPage(rows, parsed);
6808
+ return { parsed, notes: rows, blocks: [], stages: { selected, matched: rows.length } };
4700
6809
  }
4701
6810
  /**
4702
6811
  * Execute a :page query (ticket 08) — the placement plane. Results are
@@ -4973,194 +7082,6 @@ function hasLegacyIdentity(row) {
4973
7082
  return false;
4974
7083
  }
4975
7084
 
4976
- // src/dashboard/model.ts
4977
- var DASHBOARD_WIDGET_TYPES = [
4978
- "table",
4979
- "list",
4980
- "value",
4981
- "timeseries",
4982
- "bar",
4983
- "donut",
4984
- "toplist",
4985
- "stacked-bar",
4986
- "goal-rings",
4987
- "zone-distribution"
4988
- ];
4989
- function isDashboardWidgetType(type) {
4990
- return DASHBOARD_WIDGET_TYPES.includes(type);
4991
- }
4992
- function resolveWidgetType(type) {
4993
- return type == null || type === "" ? "table" : type;
4994
- }
4995
- function unknownWidgetTypeMessage(type) {
4996
- return `unknown widget type "${type}"`;
4997
- }
4998
- function unknownTokensMessage(missing) {
4999
- return `unknown token${missing.length > 1 ? "s" : ""}: ${missing.map((m) => `$${m}`).join(", ")}`;
5000
- }
5001
- var PLANNED_WIDGET_TYPES = [];
5002
- var KNOWN_CALC_METRICS = new Set(WQL_CALC_TARGETS);
5003
- function isProposedMetric(metricKey) {
5004
- if (!metricKey) return false;
5005
- if (!metricKey.startsWith("calc.")) return false;
5006
- return !KNOWN_CALC_METRICS.has(metricKey);
5007
- }
5008
- var DASHBOARD_GRID_MAX_COLS = 4;
5009
- function parseQueryWidgetSuffix(suffix) {
5010
- if (suffix === "") {
5011
- return { type: "", attributes: {}, attributeOrder: [], error: 'missing widget type after "query:"' };
5012
- }
5013
- const attributes = {};
5014
- const attributeOrder = [];
5015
- const trimmedSuffix = suffix.trim();
5016
- const headEnd = trimmedSuffix.search(/\s/);
5017
- const head = headEnd === -1 ? trimmedSuffix : trimmedSuffix.slice(0, headEnd);
5018
- const attrTail = headEnd === -1 ? "" : trimmedSuffix.slice(headEnd);
5019
- const attrPattern = /([a-zA-Z][a-zA-Z0-9_-]*)=("([^"]*)"|\S+)/g;
5020
- let consumed = 0;
5021
- for (const match of attrTail.matchAll(attrPattern)) {
5022
- consumed += match[0].length;
5023
- const key = match[1].toLowerCase();
5024
- const value = match[3] !== void 0 ? match[3] : match[2];
5025
- if (key in attributes) {
5026
- return { type: head, attributes, attributeOrder, error: `duplicate attribute "${key}"` };
5027
- }
5028
- attributes[key] = value;
5029
- attributeOrder.push(key);
5030
- }
5031
- const leftover = attrTail.replace(attrPattern, "").trim();
5032
- if (leftover !== "") {
5033
- return { type: head, attributes, attributeOrder, error: `malformed attribute "${leftover.split(/\s+/)[0]}"` };
5034
- }
5035
- let rest = head.toLowerCase();
5036
- let spanCols;
5037
- let spanFull;
5038
- if (rest.endsWith("-full")) {
5039
- spanFull = true;
5040
- rest = rest.slice(0, -"-full".length);
5041
- } else {
5042
- const spanMatch = rest.match(/-(\d+)$/);
5043
- if (spanMatch) {
5044
- spanCols = Number(spanMatch[1]);
5045
- rest = rest.slice(0, -spanMatch[0].length);
5046
- }
5047
- }
5048
- if (rest === "" || !/^[a-z][a-z0-9-]*$/.test(rest)) {
5049
- return { type: rest, spanCols, spanFull, attributes, attributeOrder, error: `malformed widget type "${suffix}"` };
5050
- }
5051
- if (/-(\d+|full)$/.test(rest)) {
5052
- return { type: rest, spanCols, spanFull, attributes, attributeOrder, error: `malformed widget suffix "${suffix}"` };
5053
- }
5054
- if (spanCols != null && (spanCols < 1 || spanCols > DASHBOARD_GRID_MAX_COLS)) {
5055
- return { type: rest, spanCols, spanFull, attributes, attributeOrder, error: `span ${spanCols} outside 1..${DASHBOARD_GRID_MAX_COLS}` };
5056
- }
5057
- return { type: rest, spanCols, spanFull, attributes, attributeOrder };
5058
- }
5059
- var TOKEN_PREFIX = "dashboard.";
5060
- function isDashboardMeta(meta) {
5061
- return meta["dashboard"] === "true" || meta["dashboard"] === true;
5062
- }
5063
- function extractDashboardTokens(meta) {
5064
- const tokens = [];
5065
- for (const [key, value] of Object.entries(meta)) {
5066
- if (!key.startsWith(TOKEN_PREFIX) || key === TOKEN_PREFIX) continue;
5067
- const name = key.slice(TOKEN_PREFIX.length);
5068
- if (name === "") continue;
5069
- if (Array.isArray(value)) {
5070
- if (value.length > 0) tokens.push({ name, values: value.map(String), isList: true });
5071
- } else {
5072
- tokens.push({ name, values: [String(value)], isList: false });
5073
- }
5074
- }
5075
- return tokens;
5076
- }
5077
- function defaultTokenValues(tokens) {
5078
- const values = {};
5079
- for (const token of tokens) values[token.name] = token.values[0];
5080
- return values;
5081
- }
5082
- function setDashboardTokenValue(meta, name, value) {
5083
- const key = `${TOKEN_PREFIX}${name}`;
5084
- const current = meta[key];
5085
- const next = { ...meta };
5086
- if (Array.isArray(current)) {
5087
- if (!current.includes(value)) return meta;
5088
- next[key] = [value, ...current.filter((v) => v !== value)];
5089
- } else if (current != null) {
5090
- next[key] = typeof current === "number" && !isNaN(Number(value)) ? Number(value) : value;
5091
- } else {
5092
- next[key] = value;
5093
- }
5094
- return next;
5095
- }
5096
- var TOKEN_REF_RE = /\$([A-Za-z][\w-]*)/g;
5097
- function substituteTokens2(query, values) {
5098
- const missing = /* @__PURE__ */ new Set();
5099
- const substituted = query.replace(TOKEN_REF_RE, (raw, name) => {
5100
- const value = values[name];
5101
- if (value == null) {
5102
- missing.add(name);
5103
- return raw;
5104
- }
5105
- return value;
5106
- });
5107
- return { query: substituted, missing: [...missing] };
5108
- }
5109
- function referencedTokens(query) {
5110
- const names = /* @__PURE__ */ new Set();
5111
- for (const match of query.matchAll(TOKEN_REF_RE)) names.add(match[1]);
5112
- return [...names];
5113
- }
5114
- function headingText(content) {
5115
- const line = content.split("\n", 1)[0].replace(/^#{1,6}\s*/, "").trim();
5116
- return line === "" ? void 0 : line;
5117
- }
5118
- function paragraphText(content) {
5119
- const text = content.replace(/\s*\n\s*/g, " ").trim();
5120
- return text === "" ? void 0 : text;
5121
- }
5122
- function buildDashboardDocument(sections, meta) {
5123
- const widgets = [];
5124
- sections.forEach((section, i) => {
5125
- if (section.type !== "query") return;
5126
- const body = section.content.split("\n").filter((l) => {
5127
- const s = l.trim();
5128
- return s !== "" && !s.startsWith("#");
5129
- }).map((l) => l.trim()).join("\n");
5130
- let title;
5131
- let question;
5132
- const above = sections[i - 1];
5133
- if (above?.type === "markdown" && above.subtype === "paragraph") {
5134
- question = paragraphText(above.content);
5135
- const heading = sections[i - 2];
5136
- if (heading?.type === "markdown" && heading.subtype === "heading") {
5137
- title = headingText(heading.content);
5138
- }
5139
- } else if (above?.type === "markdown" && above.subtype === "heading") {
5140
- title = headingText(above.content);
5141
- }
5142
- widgets.push({
5143
- key: `w${widgets.length}`,
5144
- type: section.widgetType ?? "",
5145
- spanCols: section.spanCols,
5146
- spanFull: section.spanFull,
5147
- widgetError: section.widgetError,
5148
- body,
5149
- attributes: section.attributes ?? {},
5150
- attributeOrder: section.attributeOrder ?? [],
5151
- title,
5152
- question
5153
- });
5154
- });
5155
- const titleMeta = meta["title"];
5156
- return {
5157
- isDashboard: isDashboardMeta(meta),
5158
- title: typeof titleMeta === "string" && titleMeta !== "" ? titleMeta : void 0,
5159
- tokens: extractDashboardTokens(meta),
5160
- widgets
5161
- };
5162
- }
5163
-
5164
7085
  // src/dashboard/parser.ts
5165
7086
  function parseDashboardNote(rawContent) {
5166
7087
  const { meta } = wodWikiCore.parseFrontmatter(rawContent);
@@ -5211,6 +7132,7 @@ function parseDashboardNote(rawContent) {
5211
7132
  sections.push({
5212
7133
  type: "query",
5213
7134
  content: bodyLines.join("\n"),
7135
+ fenceTag: tag,
5214
7136
  widgetType,
5215
7137
  spanCols,
5216
7138
  spanFull,
@@ -5291,11 +7213,11 @@ dashboard.metric:
5291
7213
  - totalVolume
5292
7214
  ---
5293
7215
 
5294
- ## Weekly Trend
5295
- How does $metric trend week over week?
5296
-
5297
- \`\`\`query:timeseries
5298
- sum:$metric{} by {week}
7216
+ \`\`\`query
7217
+ title: Weekly Trend
7218
+ description: How does $metric trend week over week?
7219
+ view: timeseries
7220
+ query: sum:$metric{} by {week}
5299
7221
  \`\`\`
5300
7222
  `;
5301
7223
  }
@@ -5321,34 +7243,59 @@ function widgetFenceTag(spec) {
5321
7243
  function widgetBodyLine(spec) {
5322
7244
  return spec.wql.trim();
5323
7245
  }
7246
+ function widgetSpecToQueryObject(spec) {
7247
+ if (spec.queryObject) {
7248
+ const base = { ...spec.queryObject };
7249
+ if (spec.title !== void 0) base.title = spec.title.trim() || void 0;
7250
+ if (spec.question !== void 0) base.description = spec.question.trim() || void 0;
7251
+ if (spec.spanFull) base.span = "full";
7252
+ else if (spec.spanCols !== void 0) base.span = spec.spanCols;
7253
+ return base;
7254
+ }
7255
+ const tag = widgetFenceTag(spec).slice(3);
7256
+ return parseQueryBlock(spec.wql, {
7257
+ fenceTag: tag,
7258
+ adjacentTitle: spec.title?.trim() || void 0,
7259
+ adjacentDescription: spec.question?.trim() || void 0
7260
+ });
7261
+ }
5324
7262
  function renderWidgetGroup(spec) {
5325
- const lines = [];
5326
- const title = spec.title?.trim();
5327
- const question = spec.question?.trim();
5328
- if (title) lines.push(`## ${title}`);
5329
- if (question) lines.push(question);
5330
- if (lines.length > 0) lines.push("");
5331
- lines.push(widgetFenceTag(spec));
5332
- lines.push(widgetBodyLine(spec));
5333
- lines.push("```");
5334
- return lines;
7263
+ const obj = widgetSpecToQueryObject(spec);
7264
+ return serializeQueryBlockFence(obj).split("\n");
7265
+ }
7266
+ function normalizedBlockBodies(block) {
7267
+ const candidates = /* @__PURE__ */ new Set();
7268
+ const trimmed = block.content.trim();
7269
+ candidates.add(trimmed);
7270
+ const legacyNorm = block.content.split("\n").filter((l) => {
7271
+ const s = l.trim();
7272
+ return s !== "" && !s.startsWith("#");
7273
+ }).map((l) => l.trim()).join("\n");
7274
+ candidates.add(legacyNorm);
7275
+ if (isYamlQueryBlockBody(block.content)) {
7276
+ const obj = parseQueryBlock(block.content, { fenceTag: block.fenceTag });
7277
+ candidates.add(queryBlockToDocumentText(obj).trim());
7278
+ }
7279
+ return candidates;
5335
7280
  }
5336
7281
  function findWidgetGroup(sections, key, expectedBody) {
5337
7282
  const querySections = sections.filter((s) => s.type === "query");
5338
7283
  const widgetIndex = querySections.findIndex((_, i) => `w${i}` === key);
5339
7284
  if (widgetIndex === -1) return { ok: false, reason: "not-found" };
5340
7285
  const block = querySections[widgetIndex];
5341
- const body = block.content.split("\n").filter((l) => {
5342
- const s = l.trim();
5343
- return s !== "" && !s.startsWith("#");
5344
- }).map((l) => l.trim()).join("\n");
5345
- if (body !== expectedBody.trim()) return { ok: false, reason: "stale-body" };
7286
+ if (!normalizedBlockBodies(block).has(expectedBody.trim())) {
7287
+ return { ok: false, reason: "stale-body" };
7288
+ }
5346
7289
  return {
5347
7290
  ok: true,
5348
7291
  group: { widgetIndex, startLine: groupStartLine(sections, block), endLine: block.endLine }
5349
7292
  };
5350
7293
  }
5351
7294
  function groupStartLine(sections, block) {
7295
+ if (isYamlQueryBlockBody(block.content)) {
7296
+ const obj = parseQueryBlock(block.content, { fenceTag: block.fenceTag });
7297
+ if (obj.title) return block.startLine;
7298
+ }
5352
7299
  const idx = sections.indexOf(block);
5353
7300
  const above = sections[idx - 1];
5354
7301
  if (above?.type === "markdown" && above.subtype === "paragraph") {
@@ -5480,6 +7427,16 @@ function resizeWidget(raw, key, expectedBody, span) {
5480
7427
  (s) => s.type === "query" && s.startLine >= group.startLine && s.endLine <= group.endLine
5481
7428
  );
5482
7429
  const lines = raw.split("\n");
7430
+ if (isYamlQueryBlockBody(block.content)) {
7431
+ const obj = parseQueryBlock(block.content, { fenceTag: block.fenceTag });
7432
+ if (span.spanFull) obj.span = "full";
7433
+ else if (span.spanCols != null && span.spanCols > 1) obj.span = span.spanCols;
7434
+ else delete obj.span;
7435
+ const indent2 = lines[block.startLine].match(/^\s*/)[0];
7436
+ const nextFenceLines = serializeQueryBlockFence(obj, { forceYaml: true }).split("\n").map((l) => indent2 + l);
7437
+ lines.splice(block.startLine, block.endLine - block.startLine + 1, ...nextFenceLines);
7438
+ return widgetResult(raw, lines.join("\n"));
7439
+ }
5483
7440
  const indent = lines[block.startLine].match(/^\s*/)[0];
5484
7441
  lines[block.startLine] = indent + widgetFenceTag({
5485
7442
  type: block.widgetType ?? "",
@@ -5590,6 +7547,7 @@ function resolveEntityScope(input) {
5590
7547
  scope: scopeFilter,
5591
7548
  effort: slug,
5592
7549
  note: id,
7550
+ thisnote: id,
5593
7551
  metric: "totalVolume"
5594
7552
  }
5595
7553
  };
@@ -5616,6 +7574,7 @@ function resolveEntityScope(input) {
5616
7574
  scope: scopeFilter,
5617
7575
  equipment: slug,
5618
7576
  note: id,
7577
+ thisnote: id,
5619
7578
  metric: "totalVolume"
5620
7579
  }
5621
7580
  };
@@ -5651,6 +7610,7 @@ function resolveEntityScope(input) {
5651
7610
  tag: slug,
5652
7611
  category: cat,
5653
7612
  note: id,
7613
+ thisnote: id,
5654
7614
  metric: "tis"
5655
7615
  }
5656
7616
  };
@@ -5679,6 +7639,7 @@ function resolveEntityScope(input) {
5679
7639
  scope: scopeFilter,
5680
7640
  category: cat,
5681
7641
  note: id,
7642
+ thisnote: id,
5682
7643
  metric: "tis"
5683
7644
  }
5684
7645
  };
@@ -5800,12 +7761,34 @@ function resolveEntityScope(input) {
5800
7761
  tokens: {
5801
7762
  scope: scopeFilter,
5802
7763
  note: id || slug,
7764
+ thisnote: id || slug,
5803
7765
  metric: "tis"
5804
7766
  }
5805
7767
  };
5806
7768
  }
5807
7769
  }
5808
7770
  }
7771
+ function resolveQueryExecutionScope(input = {}) {
7772
+ const entityTokens = input.noteId?.trim() ? resolveEntityScope({ noteId: input.noteId, rawContent: input.rawContent }).tokens : {};
7773
+ const fmMeta = input.rawContent ? wodWikiCore.parseFrontmatter(input.rawContent).meta : void 0;
7774
+ const fmTokens = fmMeta ? defaultTokenValues(extractDashboardTokens(fmMeta)) : {};
7775
+ const tokens = {
7776
+ ...entityTokens,
7777
+ ...fmTokens,
7778
+ ...input.tokenValues ?? {}
7779
+ };
7780
+ const pageSources = input.pageSources instanceof PageSourceRegistry ? input.pageSources : new PageSourceRegistry(input.pageSources ? [...input.pageSources.entries()] : []);
7781
+ if (input.noteEvents !== void 0 || input.note !== void 0) {
7782
+ const existing = pageSources.get("@note");
7783
+ pageSources.set("@note", {
7784
+ events: input.noteEvents ?? existing?.events ?? [],
7785
+ notes: input.note ? [input.note] : existing?.notes ?? []
7786
+ });
7787
+ } else if (input.noteId?.trim() && !pageSources.has("@note")) {
7788
+ pageSources.set("@note", { events: [], notes: [] });
7789
+ }
7790
+ return { tokens, pageSources };
7791
+ }
5809
7792
  function buildWidgetsForWqlScope(wqlOrScope, extraWidgets = []) {
5810
7793
  if (typeof wqlOrScope === "string") {
5811
7794
  const query = wqlOrScope.trim();
@@ -5983,14 +7966,143 @@ function buildDashboardFromScope(options2) {
5983
7966
  return headerLines.join("\n") + bodyGroups.join("\n\n") + "\n";
5984
7967
  }
5985
7968
 
7969
+ // src/queryCache.ts
7970
+ var MUTATION_MESSAGE = "ANALYTICS_MUTATION";
7971
+ var AnalyticsInvalidationBus = class {
7972
+ makeChannel;
7973
+ channel;
7974
+ generation = 0;
7975
+ listeners = /* @__PURE__ */ new Set();
7976
+ constructor(makeChannel) {
7977
+ this.makeChannel = makeChannel;
7978
+ }
7979
+ get generationId() {
7980
+ return this.generation;
7981
+ }
7982
+ /** Commit hook: bump local generation and broadcast the receipt. */
7983
+ async publishMutation() {
7984
+ this.generation += 1;
7985
+ this.notify();
7986
+ this.channel ??= this.makeChannel();
7987
+ try {
7988
+ this.channel?.postMessage({ type: MUTATION_MESSAGE, generation: this.generation });
7989
+ } catch {
7990
+ }
7991
+ }
7992
+ /** Listen for receipts from OTHER tabs (and this one). */
7993
+ subscribe() {
7994
+ this.channel ??= this.makeChannel();
7995
+ this.channel?.addEventListener("message", (event) => {
7996
+ const data = event.data;
7997
+ if (data?.type !== MUTATION_MESSAGE) return;
7998
+ this.generation = Math.max(this.generation, data.generation ?? 0) + 1;
7999
+ this.notify();
8000
+ });
8001
+ }
8002
+ onChange(listener) {
8003
+ this.listeners.add(listener);
8004
+ return () => this.listeners.delete(listener);
8005
+ }
8006
+ notify() {
8007
+ for (const listener of this.listeners) listener();
8008
+ }
8009
+ };
8010
+ var QueryCache = class {
8011
+ entries = /* @__PURE__ */ new Map();
8012
+ maxEntries;
8013
+ constructor(bus, options2 = {}) {
8014
+ this.maxEntries = Math.max(1, options2.maxEntries ?? DEFAULT_MAX_ENTRIES);
8015
+ bus.onChange(() => this.entries.clear());
8016
+ }
8017
+ get(key, generation) {
8018
+ const entry = this.entries.get(key);
8019
+ if (!entry) return void 0;
8020
+ if (entry.generation !== generation) {
8021
+ this.entries.delete(key);
8022
+ return void 0;
8023
+ }
8024
+ this.entries.delete(key);
8025
+ this.entries.set(key, entry);
8026
+ return entry.value;
8027
+ }
8028
+ set(key, value, generation) {
8029
+ this.entries.delete(key);
8030
+ this.entries.set(key, { generation, value });
8031
+ while (this.entries.size > this.maxEntries) {
8032
+ const oldest = this.entries.keys().next();
8033
+ if (oldest.done) break;
8034
+ this.entries.delete(oldest.value);
8035
+ }
8036
+ }
8037
+ clear() {
8038
+ this.entries.clear();
8039
+ }
8040
+ /** Current entry count — the LRU bound's observable state. */
8041
+ get size() {
8042
+ return this.entries.size;
8043
+ }
8044
+ };
8045
+ var DEFAULT_MAX_ENTRIES = 64;
8046
+ function aggKey(a) {
8047
+ return [a.agg, a.metric, a.filters, a.groupBy, a.rollup ?? null, a.window ?? null, a.displayUnit ?? null, a.join ?? null];
8048
+ }
8049
+ function findKey(f) {
8050
+ return [f.target, f.filters, f.sourceScope ?? null, f.window ?? null, f.groupBy ?? null, f.displayUnit ?? null, f.pipes ?? null, f.join ?? null];
8051
+ }
8052
+ function computeCacheKey(parsed, context) {
8053
+ const canonical = JSON.stringify([
8054
+ parsed.family,
8055
+ parsed.family === "aggregate" ? aggKey(parsed) : null,
8056
+ parsed.family === "find" ? findKey(parsed) : null,
8057
+ parsed.family === "pipeline" ? [
8058
+ // Dataset/source identity participates like a query: a
8059
+ // different registry binding or source content is a
8060
+ // different query even with identical stages.
8061
+ parsed.source.kind === "dataset" ? ["dataset", parsed.source.name] : ["query", parsed.source.query.family === "aggregate" ? aggKey(parsed.source.query) : findKey(parsed.source.query)],
8062
+ parsed.transforms.map(aggKey),
8063
+ parsed.sink ? [parsed.sink.head, parsed.sink.filters] : null
8064
+ ] : null
8065
+ ]);
8066
+ return JSON.stringify([
8067
+ canonical,
8068
+ context.rangeStart ?? null,
8069
+ context.rangeEnd ?? null,
8070
+ context.timeZone ?? null,
8071
+ context.instant ?? null,
8072
+ context.generation,
8073
+ context.unitDefaults ? JSON.stringify(Object.entries(context.unitDefaults).sort()) : null
8074
+ ]);
8075
+ }
8076
+ function coalescingEventStore(inner) {
8077
+ const inFlight = /* @__PURE__ */ new Map();
8078
+ return {
8079
+ ...inner,
8080
+ getEventsByTimeRange(start, end) {
8081
+ const key = `${start}:${end}`;
8082
+ const existing = inFlight.get(key);
8083
+ if (existing) return existing.promise;
8084
+ const entry = {
8085
+ promise: inner.getEventsByTimeRange(start, end).finally(() => {
8086
+ if (inFlight.get(key) === entry) inFlight.delete(key);
8087
+ })
8088
+ };
8089
+ inFlight.set(key, entry);
8090
+ return entry.promise;
8091
+ },
8092
+ invalidateInFlight() {
8093
+ inFlight.clear();
8094
+ }
8095
+ };
8096
+ }
8097
+
5986
8098
  // src/version.ts
5987
- var VERSION = "0.51.2195";
5988
- var GIT_SHA = "79e6e99";
5989
- var BUILD_TIME = "2026-11-05T22:34:59.062Z";
8099
+ var VERSION = "0.52.2198";
8100
+ var GIT_SHA = "15e2fcb";
8101
+ var BUILD_TIME = "2026-11-11T15:07:53.492Z";
5990
8102
  var SEMVER = {
5991
- version: "0.51.2195",
5992
- gitSha: "79e6e99",
5993
- buildTime: "2026-11-05T22:34:59.062Z"
8103
+ version: "0.52.2198",
8104
+ gitSha: "15e2fcb",
8105
+ buildTime: "2026-11-11T15:07:53.492Z"
5994
8106
  };
5995
8107
 
5996
8108
  Object.defineProperty(exports, "DEFAULT_NOTEBOOK_SLUGS", {
@@ -6077,6 +8189,7 @@ Object.defineProperty(exports, "toNotebookId", {
6077
8189
  enumerable: true,
6078
8190
  get: function () { return wodWikiCore.toNotebookId; }
6079
8191
  });
8192
+ exports.AnalyticsInvalidationBus = AnalyticsInvalidationBus;
6080
8193
  exports.BUILD_TIME = BUILD_TIME;
6081
8194
  exports.CONTENT_GROUPING_DIMENSIONS = CONTENT_GROUPING_DIMENSIONS;
6082
8195
  exports.DASHBOARD_GRID_MAX_COLS = DASHBOARD_GRID_MAX_COLS;
@@ -6087,6 +8200,7 @@ exports.EFFORT_DISCIPLINES = EFFORT_DISCIPLINES;
6087
8200
  exports.GIT_SHA = GIT_SHA;
6088
8201
  exports.PLANNED_WIDGET_TYPES = PLANNED_WIDGET_TYPES;
6089
8202
  exports.PageSourceRegistry = PageSourceRegistry;
8203
+ exports.QueryCache = QueryCache;
6090
8204
  exports.QueryDocumentRunner = QueryDocumentRunner;
6091
8205
  exports.QueryService = QueryService;
6092
8206
  exports.SEMVER = SEMVER;
@@ -6125,6 +8239,7 @@ exports.WQL_VIRTUAL_DIMS = WQL_VIRTUAL_DIMS;
6125
8239
  exports.allowedFilterTypesForTarget = allowedFilterTypesForTarget;
6126
8240
  exports.appendSuffixes = appendSuffixes;
6127
8241
  exports.appendWidget = appendWidget;
8242
+ exports.bindQueryParams = bindQueryParams;
6128
8243
  exports.buildDashboardDocument = buildDashboardDocument;
6129
8244
  exports.buildDashboardFromScope = buildDashboardFromScope;
6130
8245
  exports.buildDashboardScaffold = buildDashboardScaffold;
@@ -6137,6 +8252,10 @@ exports.civilDateDiff = civilDateDiff;
6137
8252
  exports.civilDateOf = civilDateOf;
6138
8253
  exports.civilDatesCoveredByRange = civilDatesCoveredByRange;
6139
8254
  exports.civilMonday = civilMonday;
8255
+ exports.coalescingEventStore = coalescingEventStore;
8256
+ exports.collectQueryBlockParams = collectQueryBlockParams;
8257
+ exports.collectQueryParams = collectQueryParams;
8258
+ exports.computeCacheKey = computeCacheKey;
6140
8259
  exports.consumesRollupFacts = consumesRollupFacts;
6141
8260
  exports.convertDisplay = convertDisplay;
6142
8261
  exports.createBuiltinFormulaEvaluator = createBuiltinFormulaEvaluator;
@@ -6156,11 +8275,14 @@ exports.inRange = inRange;
6156
8275
  exports.isAggregateQuery = isAggregateQuery;
6157
8276
  exports.isDashboardMeta = isDashboardMeta;
6158
8277
  exports.isDashboardWidgetType = isDashboardWidgetType;
8278
+ exports.isDefaultShorthandQueryBlock = isDefaultShorthandQueryBlock;
6159
8279
  exports.isEffortDiscipline = isEffortDiscipline;
6160
8280
  exports.isFindQuery = isFindQuery;
6161
8281
  exports.isItemSubscribed = isItemSubscribed;
6162
8282
  exports.isPipelineQuery = isPipelineQuery;
6163
8283
  exports.isProposedMetric = isProposedMetric;
8284
+ exports.isYamlQueryBlockBody = isYamlQueryBlockBody;
8285
+ exports.matchesFactMetricKey = matchesFactMetricKey;
6164
8286
  exports.migrateLegacyChunkId = migrateLegacyChunkId;
6165
8287
  exports.migrateLegacyDashboardBodies = migrateLegacyDashboardBodies;
6166
8288
  exports.migrateLegacyRouteId = migrateLegacyRouteId;
@@ -6173,12 +8295,17 @@ exports.normalizeWql = normalizeWql;
6173
8295
  exports.notebookOfItem = notebookOfItem;
6174
8296
  exports.notebookOriginOfItem = notebookOriginOfItem;
6175
8297
  exports.notebookSlugOfSourceId = notebookSlugOfSourceId;
8298
+ exports.notebooksOfItem = notebooksOfItem;
6176
8299
  exports.parseDashboardNote = parseDashboardNote;
6177
8300
  exports.parseDocument = parseDocument;
8301
+ exports.parseFilterFragment = parseFilterFragment;
6178
8302
  exports.parseQuery = parseQuery;
8303
+ exports.parseQueryBlock = parseQueryBlock;
6179
8304
  exports.parseQueryWidgetSuffix = parseQueryWidgetSuffix;
6180
8305
  exports.parseWqlSuffixes = parseWqlSuffixes;
6181
8306
  exports.projectEventToFacts = projectEventToFacts;
8307
+ exports.queryBlockAttributes = queryBlockAttributes;
8308
+ exports.queryBlockToDocumentText = queryBlockToDocumentText;
6182
8309
  exports.referencedTokens = referencedTokens;
6183
8310
  exports.removeWidget = removeWidget;
6184
8311
  exports.renderWidgetGroup = renderWidgetGroup;
@@ -6186,17 +8313,21 @@ exports.resizeWidget = resizeWidget;
6186
8313
  exports.resolveCanonicalMetricKey = resolveCanonicalMetricKey;
6187
8314
  exports.resolveEntityScope = resolveEntityScope;
6188
8315
  exports.resolveOutputUnit = resolveOutputUnit;
8316
+ exports.resolveQueryExecutionScope = resolveQueryExecutionScope;
6189
8317
  exports.resolveWidgetType = resolveWidgetType;
6190
8318
  exports.resolveWindowRange = resolveWindowRange;
8319
+ exports.resolveWqlCursorStage = resolveWqlCursorStage;
6191
8320
  exports.rowsParseError = rowsParseError;
6192
8321
  exports.serialize = serialize;
6193
8322
  exports.serializeDocument = serializeDocument;
8323
+ exports.serializeQueryBlock = serializeQueryBlock;
8324
+ exports.serializeQueryBlockFence = serializeQueryBlockFence;
6194
8325
  exports.setDashboardTokenValue = setDashboardTokenValue;
6195
8326
  exports.sourceMatches = sourceMatches;
6196
8327
  exports.sourceShape = sourceShape;
6197
8328
  exports.splitAtWhere = splitAtWhere;
6198
- exports.substituteTokens = substituteTokens2;
6199
- exports.substituteTokensRaw = substituteTokens;
8329
+ exports.substituteTokens = substituteTokens;
8330
+ exports.substituteTokensRaw = substituteTokens2;
6200
8331
  exports.supportsWqlFilterKey = supportsWqlFilterKey;
6201
8332
  exports.systemTimeZone = systemTimeZone;
6202
8333
  exports.toEventRows = toEventRows;
@@ -6206,6 +8337,7 @@ exports.unknownWidgetTypeMessage = unknownWidgetTypeMessage;
6206
8337
  exports.updateWidget = updateWidget;
6207
8338
  exports.widgetBodyLine = widgetBodyLine;
6208
8339
  exports.widgetFenceTag = widgetFenceTag;
8340
+ exports.widgetSpecToQueryObject = widgetSpecToQueryObject;
6209
8341
  exports.wql = wql;
6210
8342
  exports.wqlCompletion = wqlCompletion;
6211
8343
  exports.wqlCompletionSource = wqlCompletionSource;