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