@bitcobblers/wod-wiki-wql 0.8.39 → 0.10.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -55,7 +55,7 @@ var WQL_CALC_TARGETS = [
55
55
  var WQL_INTENSITY_TIERS = ["low", "moderate", "high"];
56
56
  var WQL_GRAINS = ["summary", "event"];
57
57
  var WQL_FIND_TARGETS = ["note", "block", "effort"];
58
- var WQL_SCOPES = ["journal", "collections", "feeds", "all"];
58
+ var WQL_SOURCE_VALUES = ["journal", "collections", "feeds", "all"];
59
59
  var WQL_RESULT_PLANES = wodWikiCore.KNOWN_OUTPUT_TYPES;
60
60
  var WQL_ROWS_TARGETS = [...WQL_FIND_TARGETS, ...WQL_RESULT_PLANES, "all"];
61
61
  var WQL_ROWS_SCOPE_KEYS = ["result", "block", "note"];
@@ -154,6 +154,7 @@ var DISPLAY_UNIT_RE = /\s+in\s+([a-zA-Z0-9_-]+)\s*$/;
154
154
  var ROLLUP_RE = /\.rollup\((\d+)?([a-zA-Z]*)\)\s*$/;
155
155
  var BY_RE = /\s+by\s+\{([^}]*)\}\s*$/;
156
156
  var LAST_RE = /\s+last\s+(\d+)([dw])\s*$/i;
157
+ var FROM_TO_RE = /\s+from\s+(\d{4}-\d{2}-\d{2})(?:\s+to\s+(\d{4}-\d{2}-\d{2}))?\s*$/i;
157
158
  var IN_SCOPE_RE = /\s+in\s+(\w+)\s*$/;
158
159
  function parseWqlSuffixes(raw) {
159
160
  const { primary, where } = splitAtWhere(raw);
@@ -161,8 +162,8 @@ function parseWqlSuffixes(raw) {
161
162
  let displayUnit;
162
163
  let rollup;
163
164
  let groupBy;
164
- let last;
165
- let scope;
165
+ let window;
166
+ let legacyScope;
166
167
  const conflicts = [];
167
168
  const isFind = text.startsWith("find:");
168
169
  const isRows = /^rows(?=[:{]|\s|$)/.test(text);
@@ -181,18 +182,40 @@ function parseWqlSuffixes(raw) {
181
182
  );
182
183
  }
183
184
  };
185
+ const froms = stripRepeated(FROM_TO_RE);
186
+ const lasts = stripRepeated(LAST_RE);
187
+ {
188
+ const windows = [
189
+ ...froms.map((m) => ({
190
+ kind: "range",
191
+ start: m[1],
192
+ end: m[2],
193
+ raw: m[0].trim(),
194
+ at: m.index
195
+ })),
196
+ ...lasts.map((m) => ({
197
+ kind: "relative",
198
+ size: parseInt(m[1], 10),
199
+ unit: m[2].toLowerCase(),
200
+ raw: m[0].trim(),
201
+ at: m.index
202
+ }))
203
+ ].sort((a, b) => a.at - b.at);
204
+ if (windows.length > 1) {
205
+ conflicts.push(
206
+ `Duplicate 'window' clause: '${windows[0].raw}' conflicts with '${windows[windows.length - 1].raw}'`
207
+ );
208
+ }
209
+ if (windows.length === 1) {
210
+ const w = windows[0];
211
+ 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 };
212
+ }
213
+ }
184
214
  if (isFind || isRows) {
185
- const lasts = stripRepeated(LAST_RE);
186
- conflictFrom("window", lasts);
187
- if (lasts.length) {
188
- const m = lasts[lasts.length - 1];
189
- last = { size: parseInt(m[1], 10), unit: m[2].toLowerCase(), raw: m[0].trim() };
190
- }
191
- if (isFind) {
192
- const scopes = stripRepeated(IN_SCOPE_RE);
193
- conflictFrom("scope", scopes);
194
- if (scopes.length) scope = scopes[scopes.length - 1][1];
195
- } else {
215
+ const scopes = stripRepeated(IN_SCOPE_RE);
216
+ conflictFrom("scope", scopes);
217
+ if (scopes.length) legacyScope = scopes[scopes.length - 1][1];
218
+ if (isRows) {
196
219
  const rollups = stripRepeated(ROLLUP_RE);
197
220
  conflictFrom(".rollup", rollups);
198
221
  if (rollups.length) {
@@ -226,8 +249,8 @@ function parseWqlSuffixes(raw) {
226
249
  displayUnit,
227
250
  rollup,
228
251
  groupBy,
229
- last,
230
- scope,
252
+ window,
253
+ legacyScope,
231
254
  conflicts: conflicts.length ? conflicts : void 0,
232
255
  primaryText: text
233
256
  };
@@ -255,7 +278,13 @@ function parseJoinClause(where) {
255
278
  if (where.trimStart().startsWith("find:")) {
256
279
  const fp = parseFindQuery(where);
257
280
  if (fp.error) return { error: fp.error };
258
- return { find: { target: fp.target, filters: fp.filters, scope: fp.scope, last: fp.last } };
281
+ if (fp.window?.kind === "range") {
282
+ return { error: "Range windows are not supported on join halves \u2014 use last <n>d|w" };
283
+ }
284
+ return {
285
+ find: { target: fp.target, filters: fp.filters, last: fp.window?.kind === "relative" ? { size: fp.window.size, unit: fp.window.unit } : void 0 },
286
+ advisories: fp.advisories
287
+ };
259
288
  }
260
289
  const m = CMP_RE.exec(where.trim());
261
290
  if (!m) return { error: cannotParseJoin(where) };
@@ -272,41 +301,119 @@ function parseJoinClause(where) {
272
301
  };
273
302
  }
274
303
  function parseQuery(raw) {
275
- const trimmed = raw.trimStart();
304
+ const norm = normalizeWql(raw);
305
+ const trimmed = norm.query.trimStart();
306
+ let result;
276
307
  if (trimmed.startsWith("find:")) {
277
- return parseFindQuery(raw);
308
+ result = parseFindQuery(norm.query);
309
+ } else if (/^rows(?=[:{]|\s|$)/.test(trimmed)) {
310
+ result = parseRowsQuery(norm.query);
311
+ } else {
312
+ result = parseAnalyticsQuery(norm.query);
278
313
  }
279
- if (/^rows(?=[:{]|\s|$)/.test(trimmed)) {
280
- return parseRowsQuery(raw);
314
+ if (norm.advisories.length) {
315
+ result.advisories = [...result.advisories ?? [], ...norm.advisories];
281
316
  }
282
- return parseAnalyticsQuery(raw);
317
+ result.raw = raw;
318
+ return result;
319
+ }
320
+ function normalizeWql(raw) {
321
+ const advisories = [];
322
+ let text = raw.trim();
323
+ if (/^rows:?\s*[{]/.test(text)) {
324
+ text = text.replace(/^rows:?\s*[{]/, "rows:all{");
325
+ advisories.push("Bare 'rows:{...}' syntax is deprecated; use 'rows:all{...}' instead.");
326
+ }
327
+ const { primary, where } = splitAtWhere(text);
328
+ const isFind = primary.startsWith("find:");
329
+ const isRows = /^rows(?=[:{]|\s|$)/.test(primary);
330
+ if (isFind || isRows) {
331
+ const suffixes = parseWqlSuffixes(primary);
332
+ if (suffixes.legacyScope && !suffixes.conflicts?.length) {
333
+ advisories.push("Legacy 'in <scope>' syntax is deprecated; use 'source:<scope>' filter instead.");
334
+ const scope = suffixes.legacyScope;
335
+ let head = suffixes.primaryText.trim();
336
+ const braceOpen = head.indexOf("{");
337
+ const braceClose = head.lastIndexOf("}");
338
+ if (braceOpen !== -1 && braceClose !== -1 && braceClose > braceOpen) {
339
+ const beforeBrace = head.slice(0, braceOpen + 1);
340
+ const inside = head.slice(braceOpen + 1, braceClose).trim();
341
+ const afterBrace = head.slice(braceClose);
342
+ const newInside = inside ? `${inside},source:${scope}` : `source:${scope}`;
343
+ head = `${beforeBrace}${newInside}${afterBrace}`;
344
+ } else {
345
+ head = `${head}{source:${scope}}`;
346
+ }
347
+ const parts = [head];
348
+ if (suffixes.groupBy) {
349
+ parts.push(`by {${suffixes.groupBy.join(", ")}}`);
350
+ }
351
+ if (suffixes.rollup) {
352
+ parts.push(`.rollup(${suffixes.rollup.raw})`);
353
+ }
354
+ if (suffixes.window) {
355
+ parts.push(suffixes.window.raw);
356
+ }
357
+ if (where) {
358
+ parts.push(`where ${where}`);
359
+ }
360
+ text = parts.join(" ");
361
+ }
362
+ }
363
+ return { query: text, advisories };
283
364
  }
284
365
  function cannotParseRows(text) {
285
366
  return `Cannot parse "${text}". Expected rows:all{result:\u2026|block:\u2026|note:\u2026}, rows:<plane>{\u2026}, or rows:segment{\u2026} last 8w`;
286
367
  }
287
368
  function validateRowsFilters(filters) {
288
369
  const scopeKeys = new Set(WQL_ROWS_SCOPE_KEYS);
370
+ const allowedKeys = /* @__PURE__ */ new Set([...WQL_ROWS_SCOPE_KEYS, "source"]);
289
371
  const unsupported = filters.filter(
290
- (f) => !scopeKeys.has(f.key) || f.negate || f.values.some((v) => v.wildcard)
372
+ (f) => !allowedKeys.has(f.key) || f.negate || f.values.some((v) => v.wildcard)
291
373
  );
292
374
  if (unsupported.length > 0) {
293
- return `Unsupported rows filter(s): ${unsupported.map((f) => (f.negate ? "!" : "") + f.key).join(", ")}. Rows queries support exact ${WQL_ROWS_SCOPE_KEYS.map((k) => `${k}:`).join(", ")} values.`;
375
+ const allowed = [...WQL_ROWS_SCOPE_KEYS, "source"];
376
+ return `Unsupported rows filter(s): ${unsupported.map((f) => (f.negate ? "!" : "") + f.key).join(", ")}. Rows queries support exact ${allowed.map((k) => `${k}:`).join(", ")} values.`;
294
377
  }
295
378
  if (!filters.some((f) => scopeKeys.has(f.key))) {
296
379
  return `Rows query needs a scope: ${WQL_ROWS_SCOPE_KEYS.map((k) => `${k}:`).join(", ")}.`;
297
380
  }
298
381
  return void 0;
299
382
  }
383
+ function validateSourceFilter(filters) {
384
+ const validSources = new Set(WQL_SOURCE_VALUES);
385
+ for (const f of filters) {
386
+ if (f.key !== "source") continue;
387
+ for (const v of f.values) {
388
+ const val = v.value;
389
+ if (validSources.has(val) || val === "collection" || val === "feed" || val.startsWith("collection:") || val.startsWith("feed:")) {
390
+ continue;
391
+ }
392
+ return `Unknown source "${val}". Try: ${WQL_SOURCE_VALUES.join(", ")} (or collection:<id>, feed:<id>)`;
393
+ }
394
+ }
395
+ return void 0;
396
+ }
300
397
  var BARE_ROWS_RETIRED = 'Bare "rows:" is retired \u2014 name a target: rows:all{\u2026} for every output type, or a plane like rows:segment{\u2026}.';
301
398
  function parseRowsQuery(raw) {
302
399
  const suffixes = parseWqlSuffixes(raw);
303
- const { where: whereText, last, groupBy, rollup, primaryText } = suffixes;
400
+ const { where: whereText, window: windowSuffix, legacyScope, groupBy, rollup, primaryText } = suffixes;
401
+ const win = toQueryWindow(windowSuffix);
402
+ const advisories = [];
403
+ if (legacyScope) {
404
+ advisories.push("Legacy 'in <scope>' syntax is deprecated; use 'source:<scope>' filter instead.");
405
+ }
304
406
  const result = {
305
407
  family: "rows",
306
408
  raw,
307
409
  filters: [],
308
- last: last ? { size: last.size, unit: last.unit } : void 0
410
+ window: win.window,
411
+ ...advisories.length ? { advisories } : {}
309
412
  };
413
+ if (win.error) {
414
+ result.error = win.error;
415
+ return result;
416
+ }
310
417
  if (suffixes.conflicts?.length) {
311
418
  result.error = suffixes.conflicts.join("; ");
312
419
  return result;
@@ -349,6 +456,18 @@ function parseRowsQuery(raw) {
349
456
  }
350
457
  if (target !== "all") result.outputType = target;
351
458
  result.filters = extractFilters(query, text);
459
+ if (legacyScope) {
460
+ result.filters.push({
461
+ key: "source",
462
+ negate: false,
463
+ values: [{ value: legacyScope, wildcard: false }]
464
+ });
465
+ }
466
+ const sourceError = validateSourceFilter(result.filters);
467
+ if (sourceError) {
468
+ result.error = sourceError;
469
+ return result;
470
+ }
352
471
  const grainError = retiredGrainRollup(result.filters);
353
472
  if (grainError) {
354
473
  result.error = grainError;
@@ -361,6 +480,27 @@ function parseRowsQuery(raw) {
361
480
  }
362
481
  return result;
363
482
  }
483
+ function isCivilDate(s) {
484
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
485
+ if (!m) return false;
486
+ const y = Number(m[1]);
487
+ const mo = Number(m[2]);
488
+ const d = Number(m[3]);
489
+ const dt = new Date(y, mo - 1, d);
490
+ return dt.getFullYear() === y && dt.getMonth() === mo - 1 && dt.getDate() === d;
491
+ }
492
+ function toQueryWindow(w) {
493
+ if (!w) return {};
494
+ if (w.kind === "relative") {
495
+ return { window: { kind: "relative", size: w.size, unit: w.unit } };
496
+ }
497
+ for (const d of [w.start, w.end]) {
498
+ if (d !== void 0 && !isCivilDate(d)) {
499
+ return { error: `Invalid window date "${d}" \u2014 expected a real calendar date YYYY-MM-DD` };
500
+ }
501
+ }
502
+ return { window: { kind: "range", start: w.start, end: w.end } };
503
+ }
364
504
  function retiredGrainRollup(filters) {
365
505
  if (filters.some((f) => f.key === "grain" && f.values.some((v) => v.value === "rollup"))) {
366
506
  return "grain:rollup is retired \u2014 rollup grains are never stored; compute them with the .rollup suffix";
@@ -396,7 +536,8 @@ function extractFilters(query, text) {
396
536
  }
397
537
  function parseAnalyticsQuery(raw) {
398
538
  const suffixes = parseWqlSuffixes(raw);
399
- const { where: whereText, displayUnit, groupBy, rollup, primaryText: text } = suffixes;
539
+ const { where: whereText, displayUnit, groupBy, rollup, window: windowSuffix, primaryText: text } = suffixes;
540
+ const win = toQueryWindow(windowSuffix);
400
541
  const base = {
401
542
  family: "aggregate",
402
543
  raw,
@@ -405,8 +546,13 @@ function parseAnalyticsQuery(raw) {
405
546
  filters: [],
406
547
  groupBy: groupBy ?? [],
407
548
  displayUnit,
408
- rollup: rollup ? { size: rollup.size, unit: rollup.unit } : void 0
549
+ rollup: rollup ? { size: rollup.size, unit: rollup.unit } : void 0,
550
+ window: win.window
409
551
  };
552
+ if (win.error) {
553
+ base.error = win.error;
554
+ return base;
555
+ }
410
556
  if (suffixes.conflicts?.length) {
411
557
  base.error = suffixes.conflicts.join("; ");
412
558
  return base;
@@ -451,6 +597,9 @@ function parseAnalyticsQuery(raw) {
451
597
  base.error = join.error;
452
598
  return base;
453
599
  }
600
+ if (join.advisories?.length) {
601
+ base.advisories = [...base.advisories ?? [], ...join.advisories];
602
+ }
454
603
  if (!join.find) {
455
604
  base.error = `Cross-store join on an analytics query must be find:\u2026, got "${whereText}"`;
456
605
  return base;
@@ -460,19 +609,28 @@ function parseAnalyticsQuery(raw) {
460
609
  return base;
461
610
  }
462
611
  function cannotParseFind(text) {
463
- return `Cannot parse "${text}". Expected find:target{filters} in scope last 8w`;
612
+ return `Cannot parse "${text}". Expected find:target{filters} last 8w`;
464
613
  }
465
614
  function parseFindQuery(raw) {
466
615
  const suffixes = parseWqlSuffixes(raw);
467
- const { where: whereText, last, scope, primaryText: text } = suffixes;
616
+ const { where: whereText, window: windowSuffix, legacyScope, primaryText: text } = suffixes;
617
+ const win = toQueryWindow(windowSuffix);
618
+ const advisories = [];
619
+ if (legacyScope) {
620
+ advisories.push("Legacy 'in <scope>' syntax is deprecated; use 'source:<scope>' filter instead.");
621
+ }
468
622
  const result = {
469
623
  family: "find",
470
624
  raw,
471
625
  target: "",
472
626
  filters: [],
473
- scope,
474
- last: last ? { size: last.size, unit: last.unit } : void 0
627
+ window: win.window,
628
+ ...advisories.length ? { advisories } : {}
475
629
  };
630
+ if (win.error) {
631
+ result.error = win.error;
632
+ return result;
633
+ }
476
634
  if (suffixes.conflicts?.length) {
477
635
  result.error = suffixes.conflicts.join("; ");
478
636
  return result;
@@ -505,6 +663,18 @@ function parseFindQuery(raw) {
505
663
  return result;
506
664
  }
507
665
  result.filters = extractFilters(query, text);
666
+ if (legacyScope) {
667
+ result.filters.push({
668
+ key: "source",
669
+ negate: false,
670
+ values: [{ value: legacyScope, wildcard: false }]
671
+ });
672
+ }
673
+ const sourceError = validateSourceFilter(result.filters);
674
+ if (sourceError) {
675
+ result.error = sourceError;
676
+ return result;
677
+ }
508
678
  const findGrainError = retiredGrainRollup(result.filters);
509
679
  if (findGrainError) {
510
680
  result.error = findGrainError;
@@ -525,6 +695,64 @@ function parseFindQuery(raw) {
525
695
  return result;
526
696
  }
527
697
 
698
+ // src/serialize.ts
699
+ function serializeValue(value) {
700
+ if (/^[a-zA-Z0-9_-]+(:[a-zA-Z0-9_-]+)?$/.test(value)) return value;
701
+ return `"${value}"`;
702
+ }
703
+ function serializeFilters(filters) {
704
+ return filters.map((f) => `${f.negate ? "!" : ""}${f.key}:${f.values.map((v) => serializeValue(v.value) + (v.wildcard ? "*" : "")).join("|")}`).join(",");
705
+ }
706
+ function serializeAggHead(agg, metric, filters) {
707
+ return `${agg}:${metric}{${serializeFilters(filters)}}`;
708
+ }
709
+ function serializeAggregateHead(a) {
710
+ return serializeAggHead(a.agg, a.metric, a.filters);
711
+ }
712
+ function serializeFindHalf(target, filters, last) {
713
+ const head = `find:${target}${filters.length ? `{${serializeFilters(filters)}}` : ""}`;
714
+ return last ? `${head} last ${last.size}${last.unit}` : head;
715
+ }
716
+ function serializeMetricHalf(j) {
717
+ return `${serializeAggHead(j.agg, j.metric, j.filters)} ${j.operator} ${j.threshold}`;
718
+ }
719
+ function serializeFindHead(f) {
720
+ return serializeFindHalf(f.target, f.filters);
721
+ }
722
+ function serializeRowsHead(r) {
723
+ return `rows:${r.outputType ?? "all"}{${serializeFilters(r.filters)}}`;
724
+ }
725
+ function serializeWindow(w) {
726
+ if (!w) return "";
727
+ if (w.kind === "relative") return `last ${w.size}${w.unit}`;
728
+ return w.end ? `from ${w.start} to ${w.end}` : `from ${w.start}`;
729
+ }
730
+ function serialize(parsed) {
731
+ if (parsed.error) return parsed.raw;
732
+ if (parsed.family === "aggregate") {
733
+ const a = parsed;
734
+ let text = serializeAggregateHead(a);
735
+ if (a.groupBy.length) text += ` by {${a.groupBy.join(", ")}}`;
736
+ if (a.rollup) text += `.rollup(${a.rollup.size}${a.rollup.unit})`;
737
+ if (a.displayUnit) text += ` in ${a.displayUnit}`;
738
+ const win = serializeWindow(a.window);
739
+ if (win) text += ` ${win}`;
740
+ if (a.join) text += ` where ${serializeFindHalf(a.join.target, a.join.filters, a.join.last)}`;
741
+ return text;
742
+ }
743
+ if (parsed.family === "find") {
744
+ const parts = [serializeFindHead(parsed), serializeWindow(parsed.window)];
745
+ if (parsed.join) parts.push(`where ${serializeMetricHalf(parsed.join)}`);
746
+ return parts.filter(Boolean).join(" ");
747
+ }
748
+ if (parsed.family === "rows") {
749
+ const win = serializeWindow(parsed.window);
750
+ return [serializeRowsHead(parsed), win].filter(Boolean).join(" ");
751
+ }
752
+ const unhandled = parsed;
753
+ return unhandled.raw ?? "";
754
+ }
755
+
528
756
  // src/disciplines.ts
529
757
  var EFFORT_DISCIPLINES = [
530
758
  "bodyweight",
@@ -629,6 +857,8 @@ function wqlCompletionSource(options_ = {}) {
629
857
  return options(WQL_GRAINS);
630
858
  case "metric":
631
859
  return metricOptions();
860
+ case "source":
861
+ return options(WQL_SOURCE_VALUES);
632
862
  default:
633
863
  return null;
634
864
  }
@@ -1123,7 +1353,10 @@ function staticTagIndexFromBlocks(blocks) {
1123
1353
  var DAY = 864e5;
1124
1354
  var ROWS_CONTENT_PLANES = new Set(WQL_FIND_TARGETS);
1125
1355
  function localDateString(ts) {
1126
- return new Date(ts).toLocaleDateString(void 0, { year: "numeric", month: "short", day: "numeric" });
1356
+ const d = new Date(ts);
1357
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
1358
+ const dd = String(d.getDate()).padStart(2, "0");
1359
+ return `${d.getFullYear()}-${mm}-${dd}`;
1127
1360
  }
1128
1361
  function catalogOfItem(item) {
1129
1362
  if (item.catalog) return item.catalog;
@@ -1133,9 +1366,13 @@ function catalogOfItem(item) {
1133
1366
  return clean.split("/")[0];
1134
1367
  }
1135
1368
  function sourceMatches(sourceId, kind) {
1369
+ if (kind === "all") return true;
1136
1370
  if (kind === "journal") return !sourceId || sourceId === "journal";
1137
- if (kind === "collection" || kind === "feed") {
1138
- return !!sourceId && sourceId.startsWith(`${kind}:`);
1371
+ if (kind === "collection" || kind === "collections") {
1372
+ return !!sourceId && sourceId.startsWith("collection:");
1373
+ }
1374
+ if (kind === "feed" || kind === "feeds") {
1375
+ return !!sourceId && sourceId.startsWith("feed:");
1139
1376
  }
1140
1377
  return sourceId === kind;
1141
1378
  }
@@ -1150,12 +1387,26 @@ function applySourceFilter(items, filters) {
1150
1387
  }
1151
1388
  return items;
1152
1389
  }
1153
- function effectiveTimeWindow(createdAt, last, range, anchorNow) {
1154
- if (range) return createdAt >= range.start && createdAt <= range.end;
1155
- if (last) {
1156
- const cutoff = (anchorNow ?? Date.now()) - last.size * (last.unit === "w" ? 7 : 1) * DAY;
1157
- return createdAt >= cutoff;
1390
+ function civilMidnight(iso) {
1391
+ const [y, m, d] = iso.split("-").map(Number);
1392
+ return new Date(y, m - 1, d).getTime();
1393
+ }
1394
+ function nextCivilMidnight(iso) {
1395
+ const [y, m, d] = iso.split("-").map(Number);
1396
+ return new Date(y, m - 1, d + 1).getTime();
1397
+ }
1398
+ function windowRange(w, anchorNow) {
1399
+ if (!w) return void 0;
1400
+ if (w.kind === "relative") {
1401
+ return { start: (anchorNow ?? Date.now()) - w.size * (w.unit === "w" ? 7 : 1) * DAY, end: Number.MAX_SAFE_INTEGER };
1158
1402
  }
1403
+ const start = civilMidnight(w.start);
1404
+ const end = w.end !== void 0 ? nextCivilMidnight(w.end) - 1 : Number.MAX_SAFE_INTEGER;
1405
+ return { start, end };
1406
+ }
1407
+ function effectiveTimeWindow(createdAt, window, range, anchorNow) {
1408
+ const resolved = range ?? windowRange(window, anchorNow);
1409
+ if (resolved) return createdAt >= resolved.start && createdAt <= resolved.end;
1159
1410
  return true;
1160
1411
  }
1161
1412
  function latestActivity(rows) {
@@ -1165,7 +1416,7 @@ function latestActivity(rows) {
1165
1416
  }
1166
1417
  function windowAnchor(selected, parsed, options2) {
1167
1418
  if (options2.anchorNow !== void 0) return options2.anchorNow;
1168
- if (options2.anchor === "latest-activity" && parsed.last) return latestActivity(selected);
1419
+ if (options2.anchor === "latest-activity" && parsed.window?.kind === "relative") return latestActivity(selected);
1169
1420
  return void 0;
1170
1421
  }
1171
1422
  var defaultEventStore = {
@@ -1235,9 +1486,11 @@ function matchesFilters(row, filters, noteTags) {
1235
1486
  const negate = group[0].negate;
1236
1487
  const raw = factTagValue(row, key, noteTags);
1237
1488
  const rowValues = Array.isArray(raw) ? raw : raw !== void 0 ? [raw] : [];
1238
- const hit = group.some(
1239
- (f) => f.values.some(
1240
- (a) => rowValues.some((v) => a.wildcard ? v.startsWith(a.value) : v === a.value)
1489
+ const hit = rowValues.some(
1490
+ (value) => group.some(
1491
+ (f) => f.values.some(
1492
+ (a) => a.wildcard ? value.startsWith(a.value) : value === a.value
1493
+ )
1241
1494
  )
1242
1495
  );
1243
1496
  return negate ? !hit : hit;
@@ -1247,8 +1500,8 @@ function dimValue(row, dim, noteTags) {
1247
1500
  if (dim === "day") return localDateString(row.timestamp);
1248
1501
  if (dim === "week") {
1249
1502
  const d = new Date(row.timestamp);
1250
- const monday = new Date(row.timestamp - (d.getDay() + 6) % 7 * DAY);
1251
- return `w/${monday.toISOString().slice(5, 10)}`;
1503
+ const monday = new Date(d.getFullYear(), d.getMonth(), d.getDate() - (d.getDay() + 6) % 7);
1504
+ return localDateString(monday.getTime());
1252
1505
  }
1253
1506
  if (dim === "session") return row.resultId;
1254
1507
  const raw = factTagValue(row, dim, noteTags);
@@ -1374,9 +1627,9 @@ var QueryService = class {
1374
1627
  for (const blockContentId of blockIds) collect(await this.store.getEventsByContent(blockContentId));
1375
1628
  for (const noteId of noteIds) collect(await this.store.getEventsForNote(noteId));
1376
1629
  let groups = [...byResult.entries()].filter(([, rows]) => rows.length > 0);
1377
- if (parsed.last) {
1630
+ if (parsed.window) {
1378
1631
  groups = groups.filter(
1379
- ([, rows]) => effectiveTimeWindow(rows[0].timestamp, parsed.last, void 0, options2.anchorNow)
1632
+ ([, rows]) => effectiveTimeWindow(rows[0].timestamp, parsed.window, void 0, options2.anchorNow)
1380
1633
  );
1381
1634
  }
1382
1635
  groups.sort((a, b) => b[1][0].timestamp - a[1][0].timestamp);
@@ -1404,19 +1657,13 @@ var QueryService = class {
1404
1657
  return this.runFindEffort(parsed);
1405
1658
  }
1406
1659
  let notes = [];
1407
- const scope = parsed.scope || "journal";
1408
- if (scope === "journal" || scope === "all") {
1409
- notes = notes.concat(await this.noteStore.getAllNotes());
1410
- }
1411
- if (scope === "collections" || scope === "feeds" || scope === "all") {
1412
- let sNotes = this.staticNoteStore ? await this.staticNoteStore.getAllNotes() : [];
1413
- if (scope === "collections") sNotes = sNotes.filter((n) => n.sourceId?.startsWith("collection:"));
1414
- if (scope === "feeds") sNotes = sNotes.filter((n) => n.sourceId?.startsWith("feed:"));
1415
- notes = notes.concat(sNotes);
1660
+ notes = notes.concat(await this.noteStore.getAllNotes());
1661
+ if (this.staticNoteStore) {
1662
+ notes = notes.concat(await this.staticNoteStore.getAllNotes());
1416
1663
  }
1664
+ notes = applySourceFilter(notes, parsed.filters);
1417
1665
  const selectedCount = notes.length;
1418
1666
  const anchorNow = windowAnchor(notes, parsed, options2);
1419
- notes = applySourceFilter(notes, parsed.filters);
1420
1667
  for (const filter of parsed.filters) {
1421
1668
  if (filter.key === "tags" && !filter.negate) {
1422
1669
  const matchingIds = /* @__PURE__ */ new Set();
@@ -1458,8 +1705,8 @@ var QueryService = class {
1458
1705
  });
1459
1706
  }
1460
1707
  }
1461
- if (parsed.last || options2.range) {
1462
- notes = notes.filter((n) => effectiveTimeWindow(n.createdAt, parsed.last, options2.range, anchorNow));
1708
+ if (parsed.window || options2.range) {
1709
+ notes = notes.filter((n) => effectiveTimeWindow(n.createdAt, parsed.window, options2.range, anchorNow));
1463
1710
  }
1464
1711
  if (parsed.join) {
1465
1712
  const joined = await this.applyMetricJoin(parsed, notes, []);
@@ -1472,19 +1719,13 @@ var QueryService = class {
1472
1719
  */
1473
1720
  async runFindBlock(parsed, options2 = {}) {
1474
1721
  let blocks = [];
1475
- const scope = parsed.scope || "journal";
1476
- if (scope === "journal" || scope === "all") {
1477
- blocks = blocks.concat(await this.blockStore.getAllBlocks());
1478
- }
1479
- if (scope === "collections" || scope === "feeds" || scope === "all") {
1480
- let sBlocks = this.staticBlockStore ? await this.staticBlockStore.getAllBlocks() : [];
1481
- if (scope === "collections") sBlocks = sBlocks.filter((b) => b.sourceId?.startsWith("collection:"));
1482
- if (scope === "feeds") sBlocks = sBlocks.filter((b) => b.sourceId?.startsWith("feed:"));
1483
- blocks = blocks.concat(sBlocks);
1722
+ blocks = blocks.concat(await this.blockStore.getAllBlocks());
1723
+ if (this.staticBlockStore) {
1724
+ blocks = blocks.concat(await this.staticBlockStore.getAllBlocks());
1484
1725
  }
1726
+ blocks = applySourceFilter(blocks, parsed.filters);
1485
1727
  const selectedCount = blocks.length;
1486
1728
  const anchorNow = windowAnchor(blocks, parsed, options2);
1487
- blocks = applySourceFilter(blocks, parsed.filters);
1488
1729
  for (const filter of parsed.filters) {
1489
1730
  if (filter.key === "text" && !filter.negate) {
1490
1731
  const search = filter.values.map((v) => v.value).join(" ").toLowerCase();
@@ -1520,8 +1761,8 @@ var QueryService = class {
1520
1761
  blocks = blocks.filter((b) => matchingNoteIds.has(b.noteId));
1521
1762
  }
1522
1763
  }
1523
- if (parsed.last || options2.range) {
1524
- blocks = blocks.filter((b) => effectiveTimeWindow(b.createdAt, parsed.last, options2.range, anchorNow));
1764
+ if (parsed.window || options2.range) {
1765
+ blocks = blocks.filter((b) => effectiveTimeWindow(b.createdAt, parsed.window, options2.range, anchorNow));
1525
1766
  }
1526
1767
  if (parsed.join) {
1527
1768
  const joined = await this.applyMetricJoin(parsed, [], blocks);
@@ -1574,10 +1815,8 @@ var QueryService = class {
1574
1815
  };
1575
1816
  if (parsed.error) return empty;
1576
1817
  if (parsed.join) return this.runJoined(parsed, options2);
1577
- const windowed = options2.rangeStart !== void 0 || options2.rangeEnd !== void 0;
1578
- const start = options2.rangeStart ?? 0;
1579
- const end = options2.rangeEnd ?? Number.MAX_SAFE_INTEGER;
1580
- const eventRows = windowed ? await this.store.getEventsByTimeRange(start, end) : await this.store.scanAll();
1818
+ const range = options2.rangeStart !== void 0 || options2.rangeEnd !== void 0 ? { start: options2.rangeStart ?? 0, end: options2.rangeEnd ?? Number.MAX_SAFE_INTEGER } : windowRange(parsed.window);
1819
+ const eventRows = range ? await this.store.getEventsByTimeRange(range.start, range.end) : await this.store.scanAll();
1581
1820
  const candidates = eventRows.flatMap(projectEventToFacts).filter((row) => row.metricKey === parsed.metric);
1582
1821
  const touchesTags = parsed.filters.some((f) => f.key === "tags") || parsed.groupBy.includes("tags");
1583
1822
  const noteTags = await this.loadNoteTags(candidates, touchesTags);
@@ -1595,8 +1834,24 @@ var QueryService = class {
1595
1834
  const timeDim = parsed.groupBy.find((d) => d === "day" || d === "week");
1596
1835
  const tagDims = parsed.groupBy.filter((d) => d !== "day" && d !== "week");
1597
1836
  const bucketMs = timeDim ? (timeDim === "week" ? 7 : 1) * DAY : parsed.rollup ? parsed.rollup.size * (parsed.rollup.unit === "w" ? 7 : 1) * DAY : null;
1598
- const bucketKey = (ts) => bucketMs ? Math.floor(ts / bucketMs) : 0;
1837
+ const civilDay = (ts) => {
1838
+ const d = new Date(ts);
1839
+ return Math.floor(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()) / DAY);
1840
+ };
1841
+ const bucketKey = (ts) => {
1842
+ if (timeDim === "day") return civilDay(ts);
1843
+ if (timeDim === "week") {
1844
+ const d = new Date(ts);
1845
+ return civilDay(ts) - (d.getDay() + 6) % 7;
1846
+ }
1847
+ return bucketMs ? Math.floor(ts / bucketMs) : 0;
1848
+ };
1599
1849
  const bucketCount = bucketMs ? new Set(matched.map((p) => bucketKey(p.timestamp))).size : matched.length ? 1 : 0;
1850
+ const civilBucketInstant = (anchor, dim) => {
1851
+ const d = new Date(anchor);
1852
+ const back = dim === "week" ? (d.getDay() + 6) % 7 : 0;
1853
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate() - back, 12).getTime();
1854
+ };
1600
1855
  const { unit: targetUnit, convert: shouldConvert } = resolveDisplayUnit(matched, {
1601
1856
  directive: parsed.displayUnit,
1602
1857
  preferred: options2.preferredUnit
@@ -1621,7 +1876,7 @@ var QueryService = class {
1621
1876
  (m) => toDisplayValue(m.value, m.unit ?? m.metricUnit, shouldConvert ? targetUnit : void 0)
1622
1877
  );
1623
1878
  return {
1624
- ts: bucketMs ? b * bucketMs + bucketMs / 2 : Math.min(...members.map((m) => m.timestamp)),
1879
+ ts: timeDim ? civilBucketInstant(members[0].timestamp, timeDim) : bucketMs ? b * bucketMs + bucketMs / 2 : Math.min(...members.map((m) => m.timestamp)),
1625
1880
  value: Math.round(aggregate(values, parsed.agg, members, shouldConvert ? targetUnit : void 0) * 100) / 100
1626
1881
  };
1627
1882
  });
@@ -1655,17 +1910,14 @@ var QueryService = class {
1655
1910
  raw: "",
1656
1911
  target: join.target,
1657
1912
  filters: join.filters,
1658
- scope: join.scope,
1659
- last: join.last
1913
+ window: join.last ? { kind: "relative", size: join.last.size, unit: join.last.unit } : void 0
1660
1914
  });
1661
1915
  const contentIds = await this.contentIdsFromFindResult(findResult);
1662
1916
  if (contentIds.size === 0) return empty;
1663
1917
  let facts = await this.deriveMetricFacts(contentIds, parsed.metric);
1664
- if (facts.length === 0) return empty;
1665
- if (options2.rangeStart !== void 0 || options2.rangeEnd !== void 0) {
1666
- const start = options2.rangeStart ?? 0;
1667
- const end = options2.rangeEnd ?? Number.MAX_SAFE_INTEGER;
1668
- facts = facts.filter((f) => f.timestamp >= start && f.timestamp <= end);
1918
+ const joinRange = options2.rangeStart !== void 0 || options2.rangeEnd !== void 0 ? { start: options2.rangeStart ?? 0, end: options2.rangeEnd ?? Number.MAX_SAFE_INTEGER } : windowRange(parsed.window);
1919
+ if (joinRange) {
1920
+ facts = facts.filter((f) => f.timestamp >= joinRange.start && f.timestamp <= joinRange.end);
1669
1921
  }
1670
1922
  const touchesTags = parsed.filters.some((f) => f.key === "tags") || parsed.groupBy.includes("tags");
1671
1923
  const noteTags = await this.loadNoteTags(facts, touchesTags);
@@ -2070,13 +2322,13 @@ sum:$metric{} rollup:1w
2070
2322
  }
2071
2323
 
2072
2324
  // src/version.ts
2073
- var VERSION = "0.8.39";
2074
- var GIT_SHA = "3c19185";
2075
- var BUILD_TIME = "2026-08-26T00:07:31.934Z";
2325
+ var VERSION = "0.10.41";
2326
+ var GIT_SHA = "1b9b771";
2327
+ var BUILD_TIME = "2026-08-26T04:16:38.418Z";
2076
2328
  var SEMVER = {
2077
- version: "0.8.39",
2078
- gitSha: "3c19185",
2079
- buildTime: "2026-08-26T00:07:31.934Z"
2329
+ version: "0.10.41",
2330
+ gitSha: "1b9b771",
2331
+ buildTime: "2026-08-26T04:16:38.418Z"
2080
2332
  };
2081
2333
 
2082
2334
  exports.BUILD_TIME = BUILD_TIME;
@@ -2107,8 +2359,8 @@ exports.WQL_RESULT_PLANES = WQL_RESULT_PLANES;
2107
2359
  exports.WQL_ROLLUP_PERIODS = WQL_ROLLUP_PERIODS;
2108
2360
  exports.WQL_ROWS_SCOPE_KEYS = WQL_ROWS_SCOPE_KEYS;
2109
2361
  exports.WQL_ROWS_TARGETS = WQL_ROWS_TARGETS;
2110
- exports.WQL_SCOPES = WQL_SCOPES;
2111
2362
  exports.WQL_SOURCES = WQL_SOURCES;
2363
+ exports.WQL_SOURCE_VALUES = WQL_SOURCE_VALUES;
2112
2364
  exports.WQL_TAG_KEYS = WQL_TAG_KEYS;
2113
2365
  exports.WQL_VIRTUAL_DIMS = WQL_VIRTUAL_DIMS;
2114
2366
  exports.buildDashboardDocument = buildDashboardDocument;
@@ -2130,6 +2382,7 @@ exports.isFindQuery = isFindQuery;
2130
2382
  exports.isProposedMetric = isProposedMetric;
2131
2383
  exports.isRowsQuery = isRowsQuery;
2132
2384
  exports.normalizeSummaryFacts = normalizeSummaryFacts;
2385
+ exports.normalizeWql = normalizeWql;
2133
2386
  exports.parseDashboardNote = parseDashboardNote;
2134
2387
  exports.parseFlatProperties = parseFlatProperties;
2135
2388
  exports.parseFrontmatter = parseFrontmatter;
@@ -2144,6 +2397,7 @@ exports.referencedTokens = referencedTokens;
2144
2397
  exports.resolveCanonicalMetricKey = resolveCanonicalMetricKey;
2145
2398
  exports.resolveDisplayUnit = resolveDisplayUnit;
2146
2399
  exports.resolveWidgetType = resolveWidgetType;
2400
+ exports.serialize = serialize;
2147
2401
  exports.serializeFrontmatter = serializeFrontmatter;
2148
2402
  exports.setDashboardTokenValue = setDashboardTokenValue;
2149
2403
  exports.splitAtWhere = splitAtWhere;