@bitcobblers/wod-wiki-wql 0.8.39 → 0.9.40

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,10 @@ 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 { find: { target: fp.target, filters: fp.filters, last: fp.window?.kind === "relative" ? { size: fp.window.size, unit: fp.window.unit } : void 0 } };
259
285
  }
260
286
  const m = CMP_RE.exec(where.trim());
261
287
  if (!m) return { error: cannotParseJoin(where) };
@@ -272,22 +298,69 @@ function parseJoinClause(where) {
272
298
  };
273
299
  }
274
300
  function parseQuery(raw) {
275
- const trimmed = raw.trimStart();
301
+ const norm = normalizeWql(raw);
302
+ const trimmed = norm.query.trimStart();
303
+ let result;
276
304
  if (trimmed.startsWith("find:")) {
277
- return parseFindQuery(raw);
305
+ result = parseFindQuery(norm.query);
306
+ } else if (/^rows(?=[:{]|\s|$)/.test(trimmed)) {
307
+ result = parseRowsQuery(norm.query);
308
+ } else {
309
+ result = parseAnalyticsQuery(norm.query);
310
+ }
311
+ if (norm.advisories.length) {
312
+ result.advisories = [...result.advisories ?? [], ...norm.advisories];
278
313
  }
279
- if (/^rows(?=[:{]|\s|$)/.test(trimmed)) {
280
- return parseRowsQuery(raw);
314
+ result.raw = raw;
315
+ return result;
316
+ }
317
+ function normalizeWql(raw) {
318
+ const advisories = [];
319
+ let text = raw.trim();
320
+ if (/^rows:?\s*[{]/.test(text)) {
321
+ text = text.replace(/^rows:?\s*[{]/, "rows:all{");
322
+ advisories.push("Bare 'rows:{...}' syntax is deprecated; use 'rows:all{...}' instead.");
323
+ }
324
+ const { primary, where } = splitAtWhere(text);
325
+ const isFind = primary.startsWith("find:");
326
+ const isRows = /^rows(?=[:{]|\s|$)/.test(primary);
327
+ if (isFind || isRows) {
328
+ const suffixes = parseWqlSuffixes(primary);
329
+ if (suffixes.legacyScope && !suffixes.conflicts?.length) {
330
+ advisories.push("Legacy 'in <scope>' syntax is deprecated; use 'source:<scope>' filter instead.");
331
+ const scope = suffixes.legacyScope;
332
+ let head = suffixes.primaryText.trim();
333
+ const braceOpen = head.indexOf("{");
334
+ const braceClose = head.lastIndexOf("}");
335
+ if (braceOpen !== -1 && braceClose !== -1 && braceClose > braceOpen) {
336
+ const beforeBrace = head.slice(0, braceOpen + 1);
337
+ const inside = head.slice(braceOpen + 1, braceClose).trim();
338
+ const afterBrace = head.slice(braceClose);
339
+ const newInside = inside ? `${inside},source:${scope}` : `source:${scope}`;
340
+ head = `${beforeBrace}${newInside}${afterBrace}`;
341
+ } else {
342
+ head = `${head}{source:${scope}}`;
343
+ }
344
+ const parts = [head];
345
+ if (suffixes.window) {
346
+ parts.push(suffixes.window.raw);
347
+ }
348
+ if (where) {
349
+ parts.push(`where ${where}`);
350
+ }
351
+ text = parts.join(" ");
352
+ }
281
353
  }
282
- return parseAnalyticsQuery(raw);
354
+ return { query: text, advisories };
283
355
  }
284
356
  function cannotParseRows(text) {
285
357
  return `Cannot parse "${text}". Expected rows:all{result:\u2026|block:\u2026|note:\u2026}, rows:<plane>{\u2026}, or rows:segment{\u2026} last 8w`;
286
358
  }
287
359
  function validateRowsFilters(filters) {
288
360
  const scopeKeys = new Set(WQL_ROWS_SCOPE_KEYS);
361
+ const allowedKeys = /* @__PURE__ */ new Set([...WQL_ROWS_SCOPE_KEYS, "source"]);
289
362
  const unsupported = filters.filter(
290
- (f) => !scopeKeys.has(f.key) || f.negate || f.values.some((v) => v.wildcard)
363
+ (f) => !allowedKeys.has(f.key) || f.negate || f.values.some((v) => v.wildcard)
291
364
  );
292
365
  if (unsupported.length > 0) {
293
366
  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.`;
@@ -297,16 +370,40 @@ function validateRowsFilters(filters) {
297
370
  }
298
371
  return void 0;
299
372
  }
373
+ function validateSourceFilter(filters) {
374
+ const validSources = new Set(WQL_SOURCE_VALUES);
375
+ for (const f of filters) {
376
+ if (f.key !== "source") continue;
377
+ for (const v of f.values) {
378
+ const val = v.value;
379
+ if (validSources.has(val) || val === "collection" || val === "feed" || val.startsWith("collection:") || val.startsWith("feed:")) {
380
+ continue;
381
+ }
382
+ return `Unknown source "${val}". Try: ${WQL_SOURCE_VALUES.join(", ")}`;
383
+ }
384
+ }
385
+ return void 0;
386
+ }
300
387
  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
388
  function parseRowsQuery(raw) {
302
389
  const suffixes = parseWqlSuffixes(raw);
303
- const { where: whereText, last, groupBy, rollup, primaryText } = suffixes;
390
+ const { where: whereText, window: windowSuffix, legacyScope, groupBy, rollup, primaryText } = suffixes;
391
+ const win = toQueryWindow(windowSuffix);
392
+ const advisories = [];
393
+ if (legacyScope) {
394
+ advisories.push("Legacy 'in <scope>' syntax is deprecated; use 'source:<scope>' filter instead.");
395
+ }
304
396
  const result = {
305
397
  family: "rows",
306
398
  raw,
307
399
  filters: [],
308
- last: last ? { size: last.size, unit: last.unit } : void 0
400
+ window: win.window,
401
+ ...advisories.length ? { advisories } : {}
309
402
  };
403
+ if (win.error) {
404
+ result.error = win.error;
405
+ return result;
406
+ }
310
407
  if (suffixes.conflicts?.length) {
311
408
  result.error = suffixes.conflicts.join("; ");
312
409
  return result;
@@ -349,6 +446,18 @@ function parseRowsQuery(raw) {
349
446
  }
350
447
  if (target !== "all") result.outputType = target;
351
448
  result.filters = extractFilters(query, text);
449
+ if (legacyScope) {
450
+ result.filters.push({
451
+ key: "source",
452
+ negate: false,
453
+ values: [{ value: legacyScope, wildcard: false }]
454
+ });
455
+ }
456
+ const sourceError = validateSourceFilter(result.filters);
457
+ if (sourceError) {
458
+ result.error = sourceError;
459
+ return result;
460
+ }
352
461
  const grainError = retiredGrainRollup(result.filters);
353
462
  if (grainError) {
354
463
  result.error = grainError;
@@ -361,6 +470,27 @@ function parseRowsQuery(raw) {
361
470
  }
362
471
  return result;
363
472
  }
473
+ function isCivilDate(s) {
474
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
475
+ if (!m) return false;
476
+ const y = Number(m[1]);
477
+ const mo = Number(m[2]);
478
+ const d = Number(m[3]);
479
+ const dt = new Date(y, mo - 1, d);
480
+ return dt.getFullYear() === y && dt.getMonth() === mo - 1 && dt.getDate() === d;
481
+ }
482
+ function toQueryWindow(w) {
483
+ if (!w) return {};
484
+ if (w.kind === "relative") {
485
+ return { window: { kind: "relative", size: w.size, unit: w.unit } };
486
+ }
487
+ for (const d of [w.start, w.end]) {
488
+ if (d !== void 0 && !isCivilDate(d)) {
489
+ return { error: `Invalid window date "${d}" \u2014 expected a real calendar date YYYY-MM-DD` };
490
+ }
491
+ }
492
+ return { window: { kind: "range", start: w.start, end: w.end } };
493
+ }
364
494
  function retiredGrainRollup(filters) {
365
495
  if (filters.some((f) => f.key === "grain" && f.values.some((v) => v.value === "rollup"))) {
366
496
  return "grain:rollup is retired \u2014 rollup grains are never stored; compute them with the .rollup suffix";
@@ -396,7 +526,8 @@ function extractFilters(query, text) {
396
526
  }
397
527
  function parseAnalyticsQuery(raw) {
398
528
  const suffixes = parseWqlSuffixes(raw);
399
- const { where: whereText, displayUnit, groupBy, rollup, primaryText: text } = suffixes;
529
+ const { where: whereText, displayUnit, groupBy, rollup, window: windowSuffix, primaryText: text } = suffixes;
530
+ const win = toQueryWindow(windowSuffix);
400
531
  const base = {
401
532
  family: "aggregate",
402
533
  raw,
@@ -405,8 +536,13 @@ function parseAnalyticsQuery(raw) {
405
536
  filters: [],
406
537
  groupBy: groupBy ?? [],
407
538
  displayUnit,
408
- rollup: rollup ? { size: rollup.size, unit: rollup.unit } : void 0
539
+ rollup: rollup ? { size: rollup.size, unit: rollup.unit } : void 0,
540
+ window: win.window
409
541
  };
542
+ if (win.error) {
543
+ base.error = win.error;
544
+ return base;
545
+ }
410
546
  if (suffixes.conflicts?.length) {
411
547
  base.error = suffixes.conflicts.join("; ");
412
548
  return base;
@@ -460,19 +596,28 @@ function parseAnalyticsQuery(raw) {
460
596
  return base;
461
597
  }
462
598
  function cannotParseFind(text) {
463
- return `Cannot parse "${text}". Expected find:target{filters} in scope last 8w`;
599
+ return `Cannot parse "${text}". Expected find:target{filters} last 8w`;
464
600
  }
465
601
  function parseFindQuery(raw) {
466
602
  const suffixes = parseWqlSuffixes(raw);
467
- const { where: whereText, last, scope, primaryText: text } = suffixes;
603
+ const { where: whereText, window: windowSuffix, legacyScope, primaryText: text } = suffixes;
604
+ const win = toQueryWindow(windowSuffix);
605
+ const advisories = [];
606
+ if (legacyScope) {
607
+ advisories.push("Legacy 'in <scope>' syntax is deprecated; use 'source:<scope>' filter instead.");
608
+ }
468
609
  const result = {
469
610
  family: "find",
470
611
  raw,
471
612
  target: "",
472
613
  filters: [],
473
- scope,
474
- last: last ? { size: last.size, unit: last.unit } : void 0
614
+ window: win.window,
615
+ ...advisories.length ? { advisories } : {}
475
616
  };
617
+ if (win.error) {
618
+ result.error = win.error;
619
+ return result;
620
+ }
476
621
  if (suffixes.conflicts?.length) {
477
622
  result.error = suffixes.conflicts.join("; ");
478
623
  return result;
@@ -505,6 +650,18 @@ function parseFindQuery(raw) {
505
650
  return result;
506
651
  }
507
652
  result.filters = extractFilters(query, text);
653
+ if (legacyScope) {
654
+ result.filters.push({
655
+ key: "source",
656
+ negate: false,
657
+ values: [{ value: legacyScope, wildcard: false }]
658
+ });
659
+ }
660
+ const sourceError = validateSourceFilter(result.filters);
661
+ if (sourceError) {
662
+ result.error = sourceError;
663
+ return result;
664
+ }
508
665
  const findGrainError = retiredGrainRollup(result.filters);
509
666
  if (findGrainError) {
510
667
  result.error = findGrainError;
@@ -629,6 +786,8 @@ function wqlCompletionSource(options_ = {}) {
629
786
  return options(WQL_GRAINS);
630
787
  case "metric":
631
788
  return metricOptions();
789
+ case "source":
790
+ return options(WQL_SOURCE_VALUES);
632
791
  default:
633
792
  return null;
634
793
  }
@@ -1123,7 +1282,10 @@ function staticTagIndexFromBlocks(blocks) {
1123
1282
  var DAY = 864e5;
1124
1283
  var ROWS_CONTENT_PLANES = new Set(WQL_FIND_TARGETS);
1125
1284
  function localDateString(ts) {
1126
- return new Date(ts).toLocaleDateString(void 0, { year: "numeric", month: "short", day: "numeric" });
1285
+ const d = new Date(ts);
1286
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
1287
+ const dd = String(d.getDate()).padStart(2, "0");
1288
+ return `${d.getFullYear()}-${mm}-${dd}`;
1127
1289
  }
1128
1290
  function catalogOfItem(item) {
1129
1291
  if (item.catalog) return item.catalog;
@@ -1133,9 +1295,13 @@ function catalogOfItem(item) {
1133
1295
  return clean.split("/")[0];
1134
1296
  }
1135
1297
  function sourceMatches(sourceId, kind) {
1298
+ if (kind === "all") return true;
1136
1299
  if (kind === "journal") return !sourceId || sourceId === "journal";
1137
- if (kind === "collection" || kind === "feed") {
1138
- return !!sourceId && sourceId.startsWith(`${kind}:`);
1300
+ if (kind === "collection" || kind === "collections") {
1301
+ return !!sourceId && sourceId.startsWith("collection:");
1302
+ }
1303
+ if (kind === "feed" || kind === "feeds") {
1304
+ return !!sourceId && sourceId.startsWith("feed:");
1139
1305
  }
1140
1306
  return sourceId === kind;
1141
1307
  }
@@ -1150,12 +1316,26 @@ function applySourceFilter(items, filters) {
1150
1316
  }
1151
1317
  return items;
1152
1318
  }
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;
1319
+ function civilMidnight(iso) {
1320
+ const [y, m, d] = iso.split("-").map(Number);
1321
+ return new Date(y, m - 1, d).getTime();
1322
+ }
1323
+ function nextCivilMidnight(iso) {
1324
+ const [y, m, d] = iso.split("-").map(Number);
1325
+ return new Date(y, m - 1, d + 1).getTime();
1326
+ }
1327
+ function windowRange(w, anchorNow) {
1328
+ if (!w) return void 0;
1329
+ if (w.kind === "relative") {
1330
+ return { start: (anchorNow ?? Date.now()) - w.size * (w.unit === "w" ? 7 : 1) * DAY, end: Number.MAX_SAFE_INTEGER };
1158
1331
  }
1332
+ const start = civilMidnight(w.start);
1333
+ const end = w.end !== void 0 ? nextCivilMidnight(w.end) - 1 : Number.MAX_SAFE_INTEGER;
1334
+ return { start, end };
1335
+ }
1336
+ function effectiveTimeWindow(createdAt, window, range, anchorNow) {
1337
+ const resolved = range ?? windowRange(window, anchorNow);
1338
+ if (resolved) return createdAt >= resolved.start && createdAt <= resolved.end;
1159
1339
  return true;
1160
1340
  }
1161
1341
  function latestActivity(rows) {
@@ -1165,7 +1345,7 @@ function latestActivity(rows) {
1165
1345
  }
1166
1346
  function windowAnchor(selected, parsed, options2) {
1167
1347
  if (options2.anchorNow !== void 0) return options2.anchorNow;
1168
- if (options2.anchor === "latest-activity" && parsed.last) return latestActivity(selected);
1348
+ if (options2.anchor === "latest-activity" && parsed.window?.kind === "relative") return latestActivity(selected);
1169
1349
  return void 0;
1170
1350
  }
1171
1351
  var defaultEventStore = {
@@ -1235,9 +1415,11 @@ function matchesFilters(row, filters, noteTags) {
1235
1415
  const negate = group[0].negate;
1236
1416
  const raw = factTagValue(row, key, noteTags);
1237
1417
  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)
1418
+ const hit = rowValues.some(
1419
+ (value) => group.some(
1420
+ (f) => f.values.some(
1421
+ (a) => a.wildcard ? value.startsWith(a.value) : value === a.value
1422
+ )
1241
1423
  )
1242
1424
  );
1243
1425
  return negate ? !hit : hit;
@@ -1247,8 +1429,8 @@ function dimValue(row, dim, noteTags) {
1247
1429
  if (dim === "day") return localDateString(row.timestamp);
1248
1430
  if (dim === "week") {
1249
1431
  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)}`;
1432
+ const monday = new Date(d.getFullYear(), d.getMonth(), d.getDate() - (d.getDay() + 6) % 7);
1433
+ return localDateString(monday.getTime());
1252
1434
  }
1253
1435
  if (dim === "session") return row.resultId;
1254
1436
  const raw = factTagValue(row, dim, noteTags);
@@ -1374,9 +1556,9 @@ var QueryService = class {
1374
1556
  for (const blockContentId of blockIds) collect(await this.store.getEventsByContent(blockContentId));
1375
1557
  for (const noteId of noteIds) collect(await this.store.getEventsForNote(noteId));
1376
1558
  let groups = [...byResult.entries()].filter(([, rows]) => rows.length > 0);
1377
- if (parsed.last) {
1559
+ if (parsed.window) {
1378
1560
  groups = groups.filter(
1379
- ([, rows]) => effectiveTimeWindow(rows[0].timestamp, parsed.last, void 0, options2.anchorNow)
1561
+ ([, rows]) => effectiveTimeWindow(rows[0].timestamp, parsed.window, void 0, options2.anchorNow)
1380
1562
  );
1381
1563
  }
1382
1564
  groups.sort((a, b) => b[1][0].timestamp - a[1][0].timestamp);
@@ -1404,15 +1586,9 @@ var QueryService = class {
1404
1586
  return this.runFindEffort(parsed);
1405
1587
  }
1406
1588
  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);
1589
+ notes = notes.concat(await this.noteStore.getAllNotes());
1590
+ if (this.staticNoteStore) {
1591
+ notes = notes.concat(await this.staticNoteStore.getAllNotes());
1416
1592
  }
1417
1593
  const selectedCount = notes.length;
1418
1594
  const anchorNow = windowAnchor(notes, parsed, options2);
@@ -1458,8 +1634,8 @@ var QueryService = class {
1458
1634
  });
1459
1635
  }
1460
1636
  }
1461
- if (parsed.last || options2.range) {
1462
- notes = notes.filter((n) => effectiveTimeWindow(n.createdAt, parsed.last, options2.range, anchorNow));
1637
+ if (parsed.window || options2.range) {
1638
+ notes = notes.filter((n) => effectiveTimeWindow(n.createdAt, parsed.window, options2.range, anchorNow));
1463
1639
  }
1464
1640
  if (parsed.join) {
1465
1641
  const joined = await this.applyMetricJoin(parsed, notes, []);
@@ -1472,15 +1648,9 @@ var QueryService = class {
1472
1648
  */
1473
1649
  async runFindBlock(parsed, options2 = {}) {
1474
1650
  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);
1651
+ blocks = blocks.concat(await this.blockStore.getAllBlocks());
1652
+ if (this.staticBlockStore) {
1653
+ blocks = blocks.concat(await this.staticBlockStore.getAllBlocks());
1484
1654
  }
1485
1655
  const selectedCount = blocks.length;
1486
1656
  const anchorNow = windowAnchor(blocks, parsed, options2);
@@ -1520,8 +1690,8 @@ var QueryService = class {
1520
1690
  blocks = blocks.filter((b) => matchingNoteIds.has(b.noteId));
1521
1691
  }
1522
1692
  }
1523
- if (parsed.last || options2.range) {
1524
- blocks = blocks.filter((b) => effectiveTimeWindow(b.createdAt, parsed.last, options2.range, anchorNow));
1693
+ if (parsed.window || options2.range) {
1694
+ blocks = blocks.filter((b) => effectiveTimeWindow(b.createdAt, parsed.window, options2.range, anchorNow));
1525
1695
  }
1526
1696
  if (parsed.join) {
1527
1697
  const joined = await this.applyMetricJoin(parsed, [], blocks);
@@ -1574,10 +1744,8 @@ var QueryService = class {
1574
1744
  };
1575
1745
  if (parsed.error) return empty;
1576
1746
  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();
1747
+ const range = options2.rangeStart !== void 0 || options2.rangeEnd !== void 0 ? { start: options2.rangeStart ?? 0, end: options2.rangeEnd ?? Number.MAX_SAFE_INTEGER } : windowRange(parsed.window);
1748
+ const eventRows = range ? await this.store.getEventsByTimeRange(range.start, range.end) : await this.store.scanAll();
1581
1749
  const candidates = eventRows.flatMap(projectEventToFacts).filter((row) => row.metricKey === parsed.metric);
1582
1750
  const touchesTags = parsed.filters.some((f) => f.key === "tags") || parsed.groupBy.includes("tags");
1583
1751
  const noteTags = await this.loadNoteTags(candidates, touchesTags);
@@ -1595,8 +1763,24 @@ var QueryService = class {
1595
1763
  const timeDim = parsed.groupBy.find((d) => d === "day" || d === "week");
1596
1764
  const tagDims = parsed.groupBy.filter((d) => d !== "day" && d !== "week");
1597
1765
  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;
1766
+ const civilDay = (ts) => {
1767
+ const d = new Date(ts);
1768
+ return Math.floor(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()) / DAY);
1769
+ };
1770
+ const bucketKey = (ts) => {
1771
+ if (timeDim === "day") return civilDay(ts);
1772
+ if (timeDim === "week") {
1773
+ const d = new Date(ts);
1774
+ return civilDay(ts) - (d.getDay() + 6) % 7;
1775
+ }
1776
+ return bucketMs ? Math.floor(ts / bucketMs) : 0;
1777
+ };
1599
1778
  const bucketCount = bucketMs ? new Set(matched.map((p) => bucketKey(p.timestamp))).size : matched.length ? 1 : 0;
1779
+ const civilBucketInstant = (anchor, dim) => {
1780
+ const d = new Date(anchor);
1781
+ const back = dim === "week" ? (d.getDay() + 6) % 7 : 0;
1782
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate() - back, 12).getTime();
1783
+ };
1600
1784
  const { unit: targetUnit, convert: shouldConvert } = resolveDisplayUnit(matched, {
1601
1785
  directive: parsed.displayUnit,
1602
1786
  preferred: options2.preferredUnit
@@ -1621,7 +1805,7 @@ var QueryService = class {
1621
1805
  (m) => toDisplayValue(m.value, m.unit ?? m.metricUnit, shouldConvert ? targetUnit : void 0)
1622
1806
  );
1623
1807
  return {
1624
- ts: bucketMs ? b * bucketMs + bucketMs / 2 : Math.min(...members.map((m) => m.timestamp)),
1808
+ ts: timeDim ? civilBucketInstant(members[0].timestamp, timeDim) : bucketMs ? b * bucketMs + bucketMs / 2 : Math.min(...members.map((m) => m.timestamp)),
1625
1809
  value: Math.round(aggregate(values, parsed.agg, members, shouldConvert ? targetUnit : void 0) * 100) / 100
1626
1810
  };
1627
1811
  });
@@ -1655,17 +1839,14 @@ var QueryService = class {
1655
1839
  raw: "",
1656
1840
  target: join.target,
1657
1841
  filters: join.filters,
1658
- scope: join.scope,
1659
- last: join.last
1842
+ window: join.last ? { kind: "relative", size: join.last.size, unit: join.last.unit } : void 0
1660
1843
  });
1661
1844
  const contentIds = await this.contentIdsFromFindResult(findResult);
1662
1845
  if (contentIds.size === 0) return empty;
1663
1846
  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);
1847
+ const joinRange = options2.rangeStart !== void 0 || options2.rangeEnd !== void 0 ? { start: options2.rangeStart ?? 0, end: options2.rangeEnd ?? Number.MAX_SAFE_INTEGER } : windowRange(parsed.window);
1848
+ if (joinRange) {
1849
+ facts = facts.filter((f) => f.timestamp >= joinRange.start && f.timestamp <= joinRange.end);
1669
1850
  }
1670
1851
  const touchesTags = parsed.filters.some((f) => f.key === "tags") || parsed.groupBy.includes("tags");
1671
1852
  const noteTags = await this.loadNoteTags(facts, touchesTags);
@@ -2070,13 +2251,13 @@ sum:$metric{} rollup:1w
2070
2251
  }
2071
2252
 
2072
2253
  // src/version.ts
2073
- var VERSION = "0.8.39";
2074
- var GIT_SHA = "3c19185";
2075
- var BUILD_TIME = "2026-08-26T00:07:31.934Z";
2254
+ var VERSION = "0.9.40";
2255
+ var GIT_SHA = "2462ec9";
2256
+ var BUILD_TIME = "2026-08-26T01:04:47.412Z";
2076
2257
  var SEMVER = {
2077
- version: "0.8.39",
2078
- gitSha: "3c19185",
2079
- buildTime: "2026-08-26T00:07:31.934Z"
2258
+ version: "0.9.40",
2259
+ gitSha: "2462ec9",
2260
+ buildTime: "2026-08-26T01:04:47.412Z"
2080
2261
  };
2081
2262
 
2082
2263
  exports.BUILD_TIME = BUILD_TIME;
@@ -2107,8 +2288,8 @@ exports.WQL_RESULT_PLANES = WQL_RESULT_PLANES;
2107
2288
  exports.WQL_ROLLUP_PERIODS = WQL_ROLLUP_PERIODS;
2108
2289
  exports.WQL_ROWS_SCOPE_KEYS = WQL_ROWS_SCOPE_KEYS;
2109
2290
  exports.WQL_ROWS_TARGETS = WQL_ROWS_TARGETS;
2110
- exports.WQL_SCOPES = WQL_SCOPES;
2111
2291
  exports.WQL_SOURCES = WQL_SOURCES;
2292
+ exports.WQL_SOURCE_VALUES = WQL_SOURCE_VALUES;
2112
2293
  exports.WQL_TAG_KEYS = WQL_TAG_KEYS;
2113
2294
  exports.WQL_VIRTUAL_DIMS = WQL_VIRTUAL_DIMS;
2114
2295
  exports.buildDashboardDocument = buildDashboardDocument;
@@ -2130,6 +2311,7 @@ exports.isFindQuery = isFindQuery;
2130
2311
  exports.isProposedMetric = isProposedMetric;
2131
2312
  exports.isRowsQuery = isRowsQuery;
2132
2313
  exports.normalizeSummaryFacts = normalizeSummaryFacts;
2314
+ exports.normalizeWql = normalizeWql;
2133
2315
  exports.parseDashboardNote = parseDashboardNote;
2134
2316
  exports.parseFlatProperties = parseFlatProperties;
2135
2317
  exports.parseFrontmatter = parseFrontmatter;