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