@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.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,10 @@ 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 { find: { target: fp.target, filters: fp.filters, last: fp.window?.kind === "relative" ? { size: fp.window.size, unit: fp.window.unit } : void 0 } };
257
283
  }
258
284
  const m = CMP_RE.exec(where.trim());
259
285
  if (!m) return { error: cannotParseJoin(where) };
@@ -270,22 +296,69 @@ function parseJoinClause(where) {
270
296
  };
271
297
  }
272
298
  function parseQuery(raw) {
273
- const trimmed = raw.trimStart();
299
+ const norm = normalizeWql(raw);
300
+ const trimmed = norm.query.trimStart();
301
+ let result;
274
302
  if (trimmed.startsWith("find:")) {
275
- return parseFindQuery(raw);
303
+ result = parseFindQuery(norm.query);
304
+ } else if (/^rows(?=[:{]|\s|$)/.test(trimmed)) {
305
+ result = parseRowsQuery(norm.query);
306
+ } else {
307
+ result = parseAnalyticsQuery(norm.query);
308
+ }
309
+ if (norm.advisories.length) {
310
+ result.advisories = [...result.advisories ?? [], ...norm.advisories];
276
311
  }
277
- if (/^rows(?=[:{]|\s|$)/.test(trimmed)) {
278
- return parseRowsQuery(raw);
312
+ result.raw = raw;
313
+ return result;
314
+ }
315
+ function normalizeWql(raw) {
316
+ const advisories = [];
317
+ let text = raw.trim();
318
+ if (/^rows:?\s*[{]/.test(text)) {
319
+ text = text.replace(/^rows:?\s*[{]/, "rows:all{");
320
+ advisories.push("Bare 'rows:{...}' syntax is deprecated; use 'rows:all{...}' instead.");
321
+ }
322
+ const { primary, where } = splitAtWhere(text);
323
+ const isFind = primary.startsWith("find:");
324
+ const isRows = /^rows(?=[:{]|\s|$)/.test(primary);
325
+ if (isFind || isRows) {
326
+ const suffixes = parseWqlSuffixes(primary);
327
+ if (suffixes.legacyScope && !suffixes.conflicts?.length) {
328
+ advisories.push("Legacy 'in <scope>' syntax is deprecated; use 'source:<scope>' filter instead.");
329
+ const scope = suffixes.legacyScope;
330
+ let head = suffixes.primaryText.trim();
331
+ const braceOpen = head.indexOf("{");
332
+ const braceClose = head.lastIndexOf("}");
333
+ if (braceOpen !== -1 && braceClose !== -1 && braceClose > braceOpen) {
334
+ const beforeBrace = head.slice(0, braceOpen + 1);
335
+ const inside = head.slice(braceOpen + 1, braceClose).trim();
336
+ const afterBrace = head.slice(braceClose);
337
+ const newInside = inside ? `${inside},source:${scope}` : `source:${scope}`;
338
+ head = `${beforeBrace}${newInside}${afterBrace}`;
339
+ } else {
340
+ head = `${head}{source:${scope}}`;
341
+ }
342
+ const parts = [head];
343
+ if (suffixes.window) {
344
+ parts.push(suffixes.window.raw);
345
+ }
346
+ if (where) {
347
+ parts.push(`where ${where}`);
348
+ }
349
+ text = parts.join(" ");
350
+ }
279
351
  }
280
- return parseAnalyticsQuery(raw);
352
+ return { query: text, advisories };
281
353
  }
282
354
  function cannotParseRows(text) {
283
355
  return `Cannot parse "${text}". Expected rows:all{result:\u2026|block:\u2026|note:\u2026}, rows:<plane>{\u2026}, or rows:segment{\u2026} last 8w`;
284
356
  }
285
357
  function validateRowsFilters(filters) {
286
358
  const scopeKeys = new Set(WQL_ROWS_SCOPE_KEYS);
359
+ const allowedKeys = /* @__PURE__ */ new Set([...WQL_ROWS_SCOPE_KEYS, "source"]);
287
360
  const unsupported = filters.filter(
288
- (f) => !scopeKeys.has(f.key) || f.negate || f.values.some((v) => v.wildcard)
361
+ (f) => !allowedKeys.has(f.key) || f.negate || f.values.some((v) => v.wildcard)
289
362
  );
290
363
  if (unsupported.length > 0) {
291
364
  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.`;
@@ -295,16 +368,40 @@ function validateRowsFilters(filters) {
295
368
  }
296
369
  return void 0;
297
370
  }
371
+ function validateSourceFilter(filters) {
372
+ const validSources = new Set(WQL_SOURCE_VALUES);
373
+ for (const f of filters) {
374
+ if (f.key !== "source") continue;
375
+ for (const v of f.values) {
376
+ const val = v.value;
377
+ if (validSources.has(val) || val === "collection" || val === "feed" || val.startsWith("collection:") || val.startsWith("feed:")) {
378
+ continue;
379
+ }
380
+ return `Unknown source "${val}". Try: ${WQL_SOURCE_VALUES.join(", ")}`;
381
+ }
382
+ }
383
+ return void 0;
384
+ }
298
385
  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
386
  function parseRowsQuery(raw) {
300
387
  const suffixes = parseWqlSuffixes(raw);
301
- const { where: whereText, last, groupBy, rollup, primaryText } = suffixes;
388
+ const { where: whereText, window: windowSuffix, legacyScope, groupBy, rollup, primaryText } = suffixes;
389
+ const win = toQueryWindow(windowSuffix);
390
+ const advisories = [];
391
+ if (legacyScope) {
392
+ advisories.push("Legacy 'in <scope>' syntax is deprecated; use 'source:<scope>' filter instead.");
393
+ }
302
394
  const result = {
303
395
  family: "rows",
304
396
  raw,
305
397
  filters: [],
306
- last: last ? { size: last.size, unit: last.unit } : void 0
398
+ window: win.window,
399
+ ...advisories.length ? { advisories } : {}
307
400
  };
401
+ if (win.error) {
402
+ result.error = win.error;
403
+ return result;
404
+ }
308
405
  if (suffixes.conflicts?.length) {
309
406
  result.error = suffixes.conflicts.join("; ");
310
407
  return result;
@@ -347,6 +444,18 @@ function parseRowsQuery(raw) {
347
444
  }
348
445
  if (target !== "all") result.outputType = target;
349
446
  result.filters = extractFilters(query, text);
447
+ if (legacyScope) {
448
+ result.filters.push({
449
+ key: "source",
450
+ negate: false,
451
+ values: [{ value: legacyScope, wildcard: false }]
452
+ });
453
+ }
454
+ const sourceError = validateSourceFilter(result.filters);
455
+ if (sourceError) {
456
+ result.error = sourceError;
457
+ return result;
458
+ }
350
459
  const grainError = retiredGrainRollup(result.filters);
351
460
  if (grainError) {
352
461
  result.error = grainError;
@@ -359,6 +468,27 @@ function parseRowsQuery(raw) {
359
468
  }
360
469
  return result;
361
470
  }
471
+ function isCivilDate(s) {
472
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
473
+ if (!m) return false;
474
+ const y = Number(m[1]);
475
+ const mo = Number(m[2]);
476
+ const d = Number(m[3]);
477
+ const dt = new Date(y, mo - 1, d);
478
+ return dt.getFullYear() === y && dt.getMonth() === mo - 1 && dt.getDate() === d;
479
+ }
480
+ function toQueryWindow(w) {
481
+ if (!w) return {};
482
+ if (w.kind === "relative") {
483
+ return { window: { kind: "relative", size: w.size, unit: w.unit } };
484
+ }
485
+ for (const d of [w.start, w.end]) {
486
+ if (d !== void 0 && !isCivilDate(d)) {
487
+ return { error: `Invalid window date "${d}" \u2014 expected a real calendar date YYYY-MM-DD` };
488
+ }
489
+ }
490
+ return { window: { kind: "range", start: w.start, end: w.end } };
491
+ }
362
492
  function retiredGrainRollup(filters) {
363
493
  if (filters.some((f) => f.key === "grain" && f.values.some((v) => v.value === "rollup"))) {
364
494
  return "grain:rollup is retired \u2014 rollup grains are never stored; compute them with the .rollup suffix";
@@ -394,7 +524,8 @@ function extractFilters(query, text) {
394
524
  }
395
525
  function parseAnalyticsQuery(raw) {
396
526
  const suffixes = parseWqlSuffixes(raw);
397
- const { where: whereText, displayUnit, groupBy, rollup, primaryText: text } = suffixes;
527
+ const { where: whereText, displayUnit, groupBy, rollup, window: windowSuffix, primaryText: text } = suffixes;
528
+ const win = toQueryWindow(windowSuffix);
398
529
  const base = {
399
530
  family: "aggregate",
400
531
  raw,
@@ -403,8 +534,13 @@ function parseAnalyticsQuery(raw) {
403
534
  filters: [],
404
535
  groupBy: groupBy ?? [],
405
536
  displayUnit,
406
- rollup: rollup ? { size: rollup.size, unit: rollup.unit } : void 0
537
+ rollup: rollup ? { size: rollup.size, unit: rollup.unit } : void 0,
538
+ window: win.window
407
539
  };
540
+ if (win.error) {
541
+ base.error = win.error;
542
+ return base;
543
+ }
408
544
  if (suffixes.conflicts?.length) {
409
545
  base.error = suffixes.conflicts.join("; ");
410
546
  return base;
@@ -458,19 +594,28 @@ function parseAnalyticsQuery(raw) {
458
594
  return base;
459
595
  }
460
596
  function cannotParseFind(text) {
461
- return `Cannot parse "${text}". Expected find:target{filters} in scope last 8w`;
597
+ return `Cannot parse "${text}". Expected find:target{filters} last 8w`;
462
598
  }
463
599
  function parseFindQuery(raw) {
464
600
  const suffixes = parseWqlSuffixes(raw);
465
- const { where: whereText, last, scope, primaryText: text } = suffixes;
601
+ const { where: whereText, window: windowSuffix, legacyScope, primaryText: text } = suffixes;
602
+ const win = toQueryWindow(windowSuffix);
603
+ const advisories = [];
604
+ if (legacyScope) {
605
+ advisories.push("Legacy 'in <scope>' syntax is deprecated; use 'source:<scope>' filter instead.");
606
+ }
466
607
  const result = {
467
608
  family: "find",
468
609
  raw,
469
610
  target: "",
470
611
  filters: [],
471
- scope,
472
- last: last ? { size: last.size, unit: last.unit } : void 0
612
+ window: win.window,
613
+ ...advisories.length ? { advisories } : {}
473
614
  };
615
+ if (win.error) {
616
+ result.error = win.error;
617
+ return result;
618
+ }
474
619
  if (suffixes.conflicts?.length) {
475
620
  result.error = suffixes.conflicts.join("; ");
476
621
  return result;
@@ -503,6 +648,18 @@ function parseFindQuery(raw) {
503
648
  return result;
504
649
  }
505
650
  result.filters = extractFilters(query, text);
651
+ if (legacyScope) {
652
+ result.filters.push({
653
+ key: "source",
654
+ negate: false,
655
+ values: [{ value: legacyScope, wildcard: false }]
656
+ });
657
+ }
658
+ const sourceError = validateSourceFilter(result.filters);
659
+ if (sourceError) {
660
+ result.error = sourceError;
661
+ return result;
662
+ }
506
663
  const findGrainError = retiredGrainRollup(result.filters);
507
664
  if (findGrainError) {
508
665
  result.error = findGrainError;
@@ -627,6 +784,8 @@ function wqlCompletionSource(options_ = {}) {
627
784
  return options(WQL_GRAINS);
628
785
  case "metric":
629
786
  return metricOptions();
787
+ case "source":
788
+ return options(WQL_SOURCE_VALUES);
630
789
  default:
631
790
  return null;
632
791
  }
@@ -1121,7 +1280,10 @@ function staticTagIndexFromBlocks(blocks) {
1121
1280
  var DAY = 864e5;
1122
1281
  var ROWS_CONTENT_PLANES = new Set(WQL_FIND_TARGETS);
1123
1282
  function localDateString(ts) {
1124
- return new Date(ts).toLocaleDateString(void 0, { year: "numeric", month: "short", day: "numeric" });
1283
+ const d = new Date(ts);
1284
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
1285
+ const dd = String(d.getDate()).padStart(2, "0");
1286
+ return `${d.getFullYear()}-${mm}-${dd}`;
1125
1287
  }
1126
1288
  function catalogOfItem(item) {
1127
1289
  if (item.catalog) return item.catalog;
@@ -1131,9 +1293,13 @@ function catalogOfItem(item) {
1131
1293
  return clean.split("/")[0];
1132
1294
  }
1133
1295
  function sourceMatches(sourceId, kind) {
1296
+ if (kind === "all") return true;
1134
1297
  if (kind === "journal") return !sourceId || sourceId === "journal";
1135
- if (kind === "collection" || kind === "feed") {
1136
- return !!sourceId && sourceId.startsWith(`${kind}:`);
1298
+ if (kind === "collection" || kind === "collections") {
1299
+ return !!sourceId && sourceId.startsWith("collection:");
1300
+ }
1301
+ if (kind === "feed" || kind === "feeds") {
1302
+ return !!sourceId && sourceId.startsWith("feed:");
1137
1303
  }
1138
1304
  return sourceId === kind;
1139
1305
  }
@@ -1148,12 +1314,26 @@ function applySourceFilter(items, filters) {
1148
1314
  }
1149
1315
  return items;
1150
1316
  }
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;
1317
+ function civilMidnight(iso) {
1318
+ const [y, m, d] = iso.split("-").map(Number);
1319
+ return new Date(y, m - 1, d).getTime();
1320
+ }
1321
+ function nextCivilMidnight(iso) {
1322
+ const [y, m, d] = iso.split("-").map(Number);
1323
+ return new Date(y, m - 1, d + 1).getTime();
1324
+ }
1325
+ function windowRange(w, anchorNow) {
1326
+ if (!w) return void 0;
1327
+ if (w.kind === "relative") {
1328
+ return { start: (anchorNow ?? Date.now()) - w.size * (w.unit === "w" ? 7 : 1) * DAY, end: Number.MAX_SAFE_INTEGER };
1156
1329
  }
1330
+ const start = civilMidnight(w.start);
1331
+ const end = w.end !== void 0 ? nextCivilMidnight(w.end) - 1 : Number.MAX_SAFE_INTEGER;
1332
+ return { start, end };
1333
+ }
1334
+ function effectiveTimeWindow(createdAt, window, range, anchorNow) {
1335
+ const resolved = range ?? windowRange(window, anchorNow);
1336
+ if (resolved) return createdAt >= resolved.start && createdAt <= resolved.end;
1157
1337
  return true;
1158
1338
  }
1159
1339
  function latestActivity(rows) {
@@ -1163,7 +1343,7 @@ function latestActivity(rows) {
1163
1343
  }
1164
1344
  function windowAnchor(selected, parsed, options2) {
1165
1345
  if (options2.anchorNow !== void 0) return options2.anchorNow;
1166
- if (options2.anchor === "latest-activity" && parsed.last) return latestActivity(selected);
1346
+ if (options2.anchor === "latest-activity" && parsed.window?.kind === "relative") return latestActivity(selected);
1167
1347
  return void 0;
1168
1348
  }
1169
1349
  var defaultEventStore = {
@@ -1233,9 +1413,11 @@ function matchesFilters(row, filters, noteTags) {
1233
1413
  const negate = group[0].negate;
1234
1414
  const raw = factTagValue(row, key, noteTags);
1235
1415
  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)
1416
+ const hit = rowValues.some(
1417
+ (value) => group.some(
1418
+ (f) => f.values.some(
1419
+ (a) => a.wildcard ? value.startsWith(a.value) : value === a.value
1420
+ )
1239
1421
  )
1240
1422
  );
1241
1423
  return negate ? !hit : hit;
@@ -1245,8 +1427,8 @@ function dimValue(row, dim, noteTags) {
1245
1427
  if (dim === "day") return localDateString(row.timestamp);
1246
1428
  if (dim === "week") {
1247
1429
  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)}`;
1430
+ const monday = new Date(d.getFullYear(), d.getMonth(), d.getDate() - (d.getDay() + 6) % 7);
1431
+ return localDateString(monday.getTime());
1250
1432
  }
1251
1433
  if (dim === "session") return row.resultId;
1252
1434
  const raw = factTagValue(row, dim, noteTags);
@@ -1372,9 +1554,9 @@ var QueryService = class {
1372
1554
  for (const blockContentId of blockIds) collect(await this.store.getEventsByContent(blockContentId));
1373
1555
  for (const noteId of noteIds) collect(await this.store.getEventsForNote(noteId));
1374
1556
  let groups = [...byResult.entries()].filter(([, rows]) => rows.length > 0);
1375
- if (parsed.last) {
1557
+ if (parsed.window) {
1376
1558
  groups = groups.filter(
1377
- ([, rows]) => effectiveTimeWindow(rows[0].timestamp, parsed.last, void 0, options2.anchorNow)
1559
+ ([, rows]) => effectiveTimeWindow(rows[0].timestamp, parsed.window, void 0, options2.anchorNow)
1378
1560
  );
1379
1561
  }
1380
1562
  groups.sort((a, b) => b[1][0].timestamp - a[1][0].timestamp);
@@ -1402,15 +1584,9 @@ var QueryService = class {
1402
1584
  return this.runFindEffort(parsed);
1403
1585
  }
1404
1586
  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);
1587
+ notes = notes.concat(await this.noteStore.getAllNotes());
1588
+ if (this.staticNoteStore) {
1589
+ notes = notes.concat(await this.staticNoteStore.getAllNotes());
1414
1590
  }
1415
1591
  const selectedCount = notes.length;
1416
1592
  const anchorNow = windowAnchor(notes, parsed, options2);
@@ -1456,8 +1632,8 @@ var QueryService = class {
1456
1632
  });
1457
1633
  }
1458
1634
  }
1459
- if (parsed.last || options2.range) {
1460
- notes = notes.filter((n) => effectiveTimeWindow(n.createdAt, parsed.last, options2.range, anchorNow));
1635
+ if (parsed.window || options2.range) {
1636
+ notes = notes.filter((n) => effectiveTimeWindow(n.createdAt, parsed.window, options2.range, anchorNow));
1461
1637
  }
1462
1638
  if (parsed.join) {
1463
1639
  const joined = await this.applyMetricJoin(parsed, notes, []);
@@ -1470,15 +1646,9 @@ var QueryService = class {
1470
1646
  */
1471
1647
  async runFindBlock(parsed, options2 = {}) {
1472
1648
  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);
1649
+ blocks = blocks.concat(await this.blockStore.getAllBlocks());
1650
+ if (this.staticBlockStore) {
1651
+ blocks = blocks.concat(await this.staticBlockStore.getAllBlocks());
1482
1652
  }
1483
1653
  const selectedCount = blocks.length;
1484
1654
  const anchorNow = windowAnchor(blocks, parsed, options2);
@@ -1518,8 +1688,8 @@ var QueryService = class {
1518
1688
  blocks = blocks.filter((b) => matchingNoteIds.has(b.noteId));
1519
1689
  }
1520
1690
  }
1521
- if (parsed.last || options2.range) {
1522
- blocks = blocks.filter((b) => effectiveTimeWindow(b.createdAt, parsed.last, options2.range, anchorNow));
1691
+ if (parsed.window || options2.range) {
1692
+ blocks = blocks.filter((b) => effectiveTimeWindow(b.createdAt, parsed.window, options2.range, anchorNow));
1523
1693
  }
1524
1694
  if (parsed.join) {
1525
1695
  const joined = await this.applyMetricJoin(parsed, [], blocks);
@@ -1572,10 +1742,8 @@ var QueryService = class {
1572
1742
  };
1573
1743
  if (parsed.error) return empty;
1574
1744
  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();
1745
+ const range = options2.rangeStart !== void 0 || options2.rangeEnd !== void 0 ? { start: options2.rangeStart ?? 0, end: options2.rangeEnd ?? Number.MAX_SAFE_INTEGER } : windowRange(parsed.window);
1746
+ const eventRows = range ? await this.store.getEventsByTimeRange(range.start, range.end) : await this.store.scanAll();
1579
1747
  const candidates = eventRows.flatMap(projectEventToFacts).filter((row) => row.metricKey === parsed.metric);
1580
1748
  const touchesTags = parsed.filters.some((f) => f.key === "tags") || parsed.groupBy.includes("tags");
1581
1749
  const noteTags = await this.loadNoteTags(candidates, touchesTags);
@@ -1593,8 +1761,24 @@ var QueryService = class {
1593
1761
  const timeDim = parsed.groupBy.find((d) => d === "day" || d === "week");
1594
1762
  const tagDims = parsed.groupBy.filter((d) => d !== "day" && d !== "week");
1595
1763
  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;
1764
+ const civilDay = (ts) => {
1765
+ const d = new Date(ts);
1766
+ return Math.floor(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()) / DAY);
1767
+ };
1768
+ const bucketKey = (ts) => {
1769
+ if (timeDim === "day") return civilDay(ts);
1770
+ if (timeDim === "week") {
1771
+ const d = new Date(ts);
1772
+ return civilDay(ts) - (d.getDay() + 6) % 7;
1773
+ }
1774
+ return bucketMs ? Math.floor(ts / bucketMs) : 0;
1775
+ };
1597
1776
  const bucketCount = bucketMs ? new Set(matched.map((p) => bucketKey(p.timestamp))).size : matched.length ? 1 : 0;
1777
+ const civilBucketInstant = (anchor, dim) => {
1778
+ const d = new Date(anchor);
1779
+ const back = dim === "week" ? (d.getDay() + 6) % 7 : 0;
1780
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate() - back, 12).getTime();
1781
+ };
1598
1782
  const { unit: targetUnit, convert: shouldConvert } = resolveDisplayUnit(matched, {
1599
1783
  directive: parsed.displayUnit,
1600
1784
  preferred: options2.preferredUnit
@@ -1619,7 +1803,7 @@ var QueryService = class {
1619
1803
  (m) => toDisplayValue(m.value, m.unit ?? m.metricUnit, shouldConvert ? targetUnit : void 0)
1620
1804
  );
1621
1805
  return {
1622
- ts: bucketMs ? b * bucketMs + bucketMs / 2 : Math.min(...members.map((m) => m.timestamp)),
1806
+ ts: timeDim ? civilBucketInstant(members[0].timestamp, timeDim) : bucketMs ? b * bucketMs + bucketMs / 2 : Math.min(...members.map((m) => m.timestamp)),
1623
1807
  value: Math.round(aggregate(values, parsed.agg, members, shouldConvert ? targetUnit : void 0) * 100) / 100
1624
1808
  };
1625
1809
  });
@@ -1653,17 +1837,14 @@ var QueryService = class {
1653
1837
  raw: "",
1654
1838
  target: join.target,
1655
1839
  filters: join.filters,
1656
- scope: join.scope,
1657
- last: join.last
1840
+ window: join.last ? { kind: "relative", size: join.last.size, unit: join.last.unit } : void 0
1658
1841
  });
1659
1842
  const contentIds = await this.contentIdsFromFindResult(findResult);
1660
1843
  if (contentIds.size === 0) return empty;
1661
1844
  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);
1845
+ const joinRange = options2.rangeStart !== void 0 || options2.rangeEnd !== void 0 ? { start: options2.rangeStart ?? 0, end: options2.rangeEnd ?? Number.MAX_SAFE_INTEGER } : windowRange(parsed.window);
1846
+ if (joinRange) {
1847
+ facts = facts.filter((f) => f.timestamp >= joinRange.start && f.timestamp <= joinRange.end);
1667
1848
  }
1668
1849
  const touchesTags = parsed.filters.some((f) => f.key === "tags") || parsed.groupBy.includes("tags");
1669
1850
  const noteTags = await this.loadNoteTags(facts, touchesTags);
@@ -2068,15 +2249,15 @@ sum:$metric{} rollup:1w
2068
2249
  }
2069
2250
 
2070
2251
  // src/version.ts
2071
- var VERSION = "0.8.39";
2072
- var GIT_SHA = "3c19185";
2073
- var BUILD_TIME = "2026-08-26T00:07:31.934Z";
2252
+ var VERSION = "0.9.40";
2253
+ var GIT_SHA = "2462ec9";
2254
+ var BUILD_TIME = "2026-08-26T01:04:47.412Z";
2074
2255
  var SEMVER = {
2075
- version: "0.8.39",
2076
- gitSha: "3c19185",
2077
- buildTime: "2026-08-26T00:07:31.934Z"
2256
+ version: "0.9.40",
2257
+ gitSha: "2462ec9",
2258
+ buildTime: "2026-08-26T01:04:47.412Z"
2078
2259
  };
2079
2260
 
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 };
2261
+ 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, 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
2262
  //# sourceMappingURL=index.mjs.map
2082
2263
  //# sourceMappingURL=index.mjs.map