@bitcobblers/wod-wiki-wql 0.7.38 → 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
@@ -1,16 +1,14 @@
1
+ import { KNOWN_OUTPUT_TYPES, MetricType } from '@bitcobblers/wod-wiki-core';
1
2
  import { LRParser } from '@lezer/lr';
2
3
  import { LRLanguage, HighlightStyle, LanguageSupport, syntaxHighlighting, syntaxTree } from '@codemirror/language';
3
4
  import { autocompletion, snippetCompletion } from '@codemirror/autocomplete';
4
5
  import { styleTags, tags } from '@lezer/highlight';
5
- import { MetricType } from '@bitcobblers/wod-wiki-core';
6
6
 
7
7
  var __defProp = Object.defineProperty;
8
8
  var __export = (target, all) => {
9
9
  for (var name in all)
10
10
  __defProp(target, name, { get: all[name], enumerable: true });
11
11
  };
12
-
13
- // src/vocabulary.ts
14
12
  var WQL_METRIC_FAMILIES = ["reps", "distance", "resistance", "elapsed", "power", "pace"];
15
13
  var WQL_METRIC_AGGREGATES = ["totalVolume", "totalDistance", "tis", "sessionLoad"];
16
14
  var WQL_AGGREGATORS = ["sum", "avg", "min", "max", "count", "last", "delta"];
@@ -55,7 +53,10 @@ var WQL_CALC_TARGETS = [
55
53
  var WQL_INTENSITY_TIERS = ["low", "moderate", "high"];
56
54
  var WQL_GRAINS = ["summary", "event"];
57
55
  var WQL_FIND_TARGETS = ["note", "block", "effort"];
58
- var WQL_SCOPES = ["journal", "collections", "feeds", "all"];
56
+ var WQL_SOURCE_VALUES = ["journal", "collections", "feeds", "all"];
57
+ var WQL_RESULT_PLANES = KNOWN_OUTPUT_TYPES;
58
+ var WQL_ROWS_TARGETS = [...WQL_FIND_TARGETS, ...WQL_RESULT_PLANES, "all"];
59
+ var WQL_ROWS_SCOPE_KEYS = ["result", "block", "note"];
59
60
  var WQL_CONTENT_FILTER_KEYS = ["type", "text", "has", "source", "catalog", ...WQL_TAG_KEYS];
60
61
  var WQL_SOURCES = ["journal", "collections", "feeds", "notes", "blocks", "efforts", "metrics"];
61
62
  var WQL_ROLLUP_PERIODS = ["1d", "1w"];
@@ -151,6 +152,7 @@ var DISPLAY_UNIT_RE = /\s+in\s+([a-zA-Z0-9_-]+)\s*$/;
151
152
  var ROLLUP_RE = /\.rollup\((\d+)?([a-zA-Z]*)\)\s*$/;
152
153
  var BY_RE = /\s+by\s+\{([^}]*)\}\s*$/;
153
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;
154
156
  var IN_SCOPE_RE = /\s+in\s+(\w+)\s*$/;
155
157
  function parseWqlSuffixes(raw) {
156
158
  const { primary, where } = splitAtWhere(raw);
@@ -158,8 +160,8 @@ function parseWqlSuffixes(raw) {
158
160
  let displayUnit;
159
161
  let rollup;
160
162
  let groupBy;
161
- let last;
162
- let scope;
163
+ let window;
164
+ let legacyScope;
163
165
  const conflicts = [];
164
166
  const isFind = text.startsWith("find:");
165
167
  const isRows = /^rows(?=[:{]|\s|$)/.test(text);
@@ -178,18 +180,40 @@ function parseWqlSuffixes(raw) {
178
180
  );
179
181
  }
180
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
+ }
181
212
  if (isFind || isRows) {
182
- const lasts = stripRepeated(LAST_RE);
183
- conflictFrom("window", lasts);
184
- if (lasts.length) {
185
- const m = lasts[lasts.length - 1];
186
- last = { size: parseInt(m[1], 10), unit: m[2].toLowerCase(), raw: m[0].trim() };
187
- }
188
- if (isFind) {
189
- const scopes = stripRepeated(IN_SCOPE_RE);
190
- conflictFrom("scope", scopes);
191
- if (scopes.length) scope = scopes[scopes.length - 1][1];
192
- } 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) {
193
217
  const rollups = stripRepeated(ROLLUP_RE);
194
218
  conflictFrom(".rollup", rollups);
195
219
  if (rollups.length) {
@@ -223,8 +247,8 @@ function parseWqlSuffixes(raw) {
223
247
  displayUnit,
224
248
  rollup,
225
249
  groupBy,
226
- last,
227
- scope,
250
+ window,
251
+ legacyScope,
228
252
  conflicts: conflicts.length ? conflicts : void 0,
229
253
  primaryText: text
230
254
  };
@@ -252,7 +276,10 @@ function parseJoinClause(where) {
252
276
  if (where.trimStart().startsWith("find:")) {
253
277
  const fp = parseFindQuery(where);
254
278
  if (fp.error) return { error: fp.error };
255
- 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 } };
256
283
  }
257
284
  const m = CMP_RE.exec(where.trim());
258
285
  if (!m) return { error: cannotParseJoin(where) };
@@ -269,27 +296,112 @@ function parseJoinClause(where) {
269
296
  };
270
297
  }
271
298
  function parseQuery(raw) {
272
- const trimmed = raw.trimStart();
299
+ const norm = normalizeWql(raw);
300
+ const trimmed = norm.query.trimStart();
301
+ let result;
273
302
  if (trimmed.startsWith("find:")) {
274
- 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];
311
+ }
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.");
275
321
  }
276
- if (/^rows(?=[:{]|\s|$)/.test(trimmed)) {
277
- return parseRowsQuery(raw);
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
+ }
278
351
  }
279
- return parseAnalyticsQuery(raw);
352
+ return { query: text, advisories };
280
353
  }
281
354
  function cannotParseRows(text) {
282
- return `Cannot parse "${text}". Expected rows:{result:\u2026|block:\u2026|note:\u2026} or rows:segment{\u2026} last 8w`;
355
+ return `Cannot parse "${text}". Expected rows:all{result:\u2026|block:\u2026|note:\u2026}, rows:<plane>{\u2026}, or rows:segment{\u2026} last 8w`;
356
+ }
357
+ function validateRowsFilters(filters) {
358
+ const scopeKeys = new Set(WQL_ROWS_SCOPE_KEYS);
359
+ const allowedKeys = /* @__PURE__ */ new Set([...WQL_ROWS_SCOPE_KEYS, "source"]);
360
+ const unsupported = filters.filter(
361
+ (f) => !allowedKeys.has(f.key) || f.negate || f.values.some((v) => v.wildcard)
362
+ );
363
+ if (unsupported.length > 0) {
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.`;
365
+ }
366
+ if (!filters.some((f) => scopeKeys.has(f.key))) {
367
+ return `Rows query needs a scope: ${WQL_ROWS_SCOPE_KEYS.map((k) => `${k}:`).join(", ")}.`;
368
+ }
369
+ return void 0;
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;
283
384
  }
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}.';
284
386
  function parseRowsQuery(raw) {
285
387
  const suffixes = parseWqlSuffixes(raw);
286
- 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
+ }
287
394
  const result = {
288
- raw,
289
395
  family: "rows",
396
+ raw,
290
397
  filters: [],
291
- last: last ? { size: last.size, unit: last.unit } : void 0
398
+ window: win.window,
399
+ ...advisories.length ? { advisories } : {}
292
400
  };
401
+ if (win.error) {
402
+ result.error = win.error;
403
+ return result;
404
+ }
293
405
  if (suffixes.conflicts?.length) {
294
406
  result.error = suffixes.conflicts.join("; ");
295
407
  return result;
@@ -299,44 +411,84 @@ function parseRowsQuery(raw) {
299
411
  return result;
300
412
  }
301
413
  const text = primaryText.trim();
302
- const brace = text.indexOf("{");
303
- const head = (brace === -1 ? text : text.slice(0, brace)).trim();
304
- const filterText = brace === -1 ? "" : text.slice(brace).trim();
305
- if (head === "rows" || head === "rows:") ; else if (head.startsWith("rows:")) {
306
- const target = head.slice("rows:".length).trim();
307
- if (!target || /\s/.test(target)) {
308
- result.error = cannotParseRows(text);
309
- return result;
310
- }
311
- result.outputType = target;
312
- } else {
414
+ if (/^rows:?\s*[{]?$/.test(text) || /^rows:?\s*[{]/.test(text)) {
415
+ result.error = BARE_ROWS_RETIRED;
416
+ return result;
417
+ }
418
+ const tree = parser.parse(text);
419
+ let syntaxError = false;
420
+ tree.iterate({ enter(node) {
421
+ if (node.type.isError) syntaxError = true;
422
+ } });
423
+ if (syntaxError) {
313
424
  result.error = cannotParseRows(text);
314
425
  return result;
315
426
  }
316
- if (filterText) {
317
- if (!/^\{.*\}$/.test(filterText)) {
318
- result.error = cannotParseRows(text);
319
- return result;
320
- }
321
- const synthetic = `find:_${filterText}`;
322
- const tree = parser.parse(synthetic);
323
- let syntaxError = false;
324
- tree.iterate({ enter(node) {
325
- if (node.type.isError) syntaxError = true;
326
- } });
327
- if (syntaxError) {
328
- result.error = cannotParseRows(text);
329
- return result;
330
- }
331
- result.filters = extractFilters(tree.topNode, synthetic);
332
- const grainError = retiredGrainRollup(result.filters);
333
- if (grainError) {
334
- result.error = grainError;
335
- return result;
336
- }
427
+ const query = tree.topNode;
428
+ const head = query.getChild(Head);
429
+ const aggNode = head?.getChild(Aggregator);
430
+ const metricNode = head?.getChild(Metric);
431
+ if (!head || !aggNode || !metricNode) {
432
+ result.error = BARE_ROWS_RETIRED;
433
+ return result;
434
+ }
435
+ const aggText = text.slice(aggNode.from, aggNode.to);
436
+ if (aggText !== "rows") {
437
+ result.error = cannotParseRows(text);
438
+ return result;
439
+ }
440
+ const target = text.slice(metricNode.from, metricNode.to);
441
+ if (!WQL_ROWS_TARGETS.includes(target)) {
442
+ result.error = `Unknown rows target "${target}". Try: ${WQL_ROWS_TARGETS.join(", ")}`;
443
+ return result;
444
+ }
445
+ if (target !== "all") result.outputType = target;
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
+ }
459
+ const grainError = retiredGrainRollup(result.filters);
460
+ if (grainError) {
461
+ result.error = grainError;
462
+ return result;
463
+ }
464
+ const filterError = validateRowsFilters(result.filters);
465
+ if (filterError) {
466
+ result.error = filterError;
467
+ return result;
337
468
  }
338
469
  return result;
339
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
+ }
340
492
  function retiredGrainRollup(filters) {
341
493
  if (filters.some((f) => f.key === "grain" && f.values.some((v) => v.value === "rollup"))) {
342
494
  return "grain:rollup is retired \u2014 rollup grains are never stored; compute them with the .rollup suffix";
@@ -372,7 +524,8 @@ function extractFilters(query, text) {
372
524
  }
373
525
  function parseAnalyticsQuery(raw) {
374
526
  const suffixes = parseWqlSuffixes(raw);
375
- 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);
376
529
  const base = {
377
530
  family: "aggregate",
378
531
  raw,
@@ -381,8 +534,13 @@ function parseAnalyticsQuery(raw) {
381
534
  filters: [],
382
535
  groupBy: groupBy ?? [],
383
536
  displayUnit,
384
- 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
385
539
  };
540
+ if (win.error) {
541
+ base.error = win.error;
542
+ return base;
543
+ }
386
544
  if (suffixes.conflicts?.length) {
387
545
  base.error = suffixes.conflicts.join("; ");
388
546
  return base;
@@ -436,19 +594,28 @@ function parseAnalyticsQuery(raw) {
436
594
  return base;
437
595
  }
438
596
  function cannotParseFind(text) {
439
- return `Cannot parse "${text}". Expected find:target{filters} in scope last 8w`;
597
+ return `Cannot parse "${text}". Expected find:target{filters} last 8w`;
440
598
  }
441
599
  function parseFindQuery(raw) {
442
600
  const suffixes = parseWqlSuffixes(raw);
443
- 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
+ }
444
607
  const result = {
445
608
  family: "find",
446
609
  raw,
447
610
  target: "",
448
611
  filters: [],
449
- scope,
450
- last: last ? { size: last.size, unit: last.unit } : void 0
612
+ window: win.window,
613
+ ...advisories.length ? { advisories } : {}
451
614
  };
615
+ if (win.error) {
616
+ result.error = win.error;
617
+ return result;
618
+ }
452
619
  if (suffixes.conflicts?.length) {
453
620
  result.error = suffixes.conflicts.join("; ");
454
621
  return result;
@@ -476,7 +643,23 @@ function parseFindQuery(raw) {
476
643
  return result;
477
644
  }
478
645
  result.target = text.slice(metricNode.from, metricNode.to);
646
+ if (!WQL_FIND_TARGETS.includes(result.target)) {
647
+ result.error = `Unknown find target "${result.target}". Try: ${WQL_FIND_TARGETS.join(", ")}`;
648
+ return result;
649
+ }
479
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
+ }
480
663
  const findGrainError = retiredGrainRollup(result.filters);
481
664
  if (findGrainError) {
482
665
  result.error = findGrainError;
@@ -601,6 +784,8 @@ function wqlCompletionSource(options_ = {}) {
601
784
  return options(WQL_GRAINS);
602
785
  case "metric":
603
786
  return metricOptions();
787
+ case "source":
788
+ return options(WQL_SOURCE_VALUES);
604
789
  default:
605
790
  return null;
606
791
  }
@@ -1093,8 +1278,12 @@ function staticTagIndexFromBlocks(blocks) {
1093
1278
 
1094
1279
  // src/QueryService.ts
1095
1280
  var DAY = 864e5;
1281
+ var ROWS_CONTENT_PLANES = new Set(WQL_FIND_TARGETS);
1096
1282
  function localDateString(ts) {
1097
- 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}`;
1098
1287
  }
1099
1288
  function catalogOfItem(item) {
1100
1289
  if (item.catalog) return item.catalog;
@@ -1104,9 +1293,13 @@ function catalogOfItem(item) {
1104
1293
  return clean.split("/")[0];
1105
1294
  }
1106
1295
  function sourceMatches(sourceId, kind) {
1296
+ if (kind === "all") return true;
1107
1297
  if (kind === "journal") return !sourceId || sourceId === "journal";
1108
- if (kind === "collection" || kind === "feed") {
1109
- 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:");
1110
1303
  }
1111
1304
  return sourceId === kind;
1112
1305
  }
@@ -1121,12 +1314,26 @@ function applySourceFilter(items, filters) {
1121
1314
  }
1122
1315
  return items;
1123
1316
  }
1124
- function effectiveTimeWindow(createdAt, last, range, anchorNow) {
1125
- if (range) return createdAt >= range.start && createdAt <= range.end;
1126
- if (last) {
1127
- const cutoff = (anchorNow ?? Date.now()) - last.size * (last.unit === "w" ? 7 : 1) * DAY;
1128
- 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 };
1129
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;
1130
1337
  return true;
1131
1338
  }
1132
1339
  function latestActivity(rows) {
@@ -1136,7 +1343,7 @@ function latestActivity(rows) {
1136
1343
  }
1137
1344
  function windowAnchor(selected, parsed, options2) {
1138
1345
  if (options2.anchorNow !== void 0) return options2.anchorNow;
1139
- if (options2.anchor === "latest-activity" && parsed.last) return latestActivity(selected);
1346
+ if (options2.anchor === "latest-activity" && parsed.window?.kind === "relative") return latestActivity(selected);
1140
1347
  return void 0;
1141
1348
  }
1142
1349
  var defaultEventStore = {
@@ -1206,9 +1413,11 @@ function matchesFilters(row, filters, noteTags) {
1206
1413
  const negate = group[0].negate;
1207
1414
  const raw = factTagValue(row, key, noteTags);
1208
1415
  const rowValues = Array.isArray(raw) ? raw : raw !== void 0 ? [raw] : [];
1209
- const hit = group.some(
1210
- (f) => f.values.some(
1211
- (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
+ )
1212
1421
  )
1213
1422
  );
1214
1423
  return negate ? !hit : hit;
@@ -1218,8 +1427,8 @@ function dimValue(row, dim, noteTags) {
1218
1427
  if (dim === "day") return localDateString(row.timestamp);
1219
1428
  if (dim === "week") {
1220
1429
  const d = new Date(row.timestamp);
1221
- const monday = new Date(row.timestamp - (d.getDay() + 6) % 7 * DAY);
1222
- 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());
1223
1432
  }
1224
1433
  if (dim === "session") return row.resultId;
1225
1434
  const raw = factTagValue(row, dim, noteTags);
@@ -1318,29 +1527,20 @@ var QueryService = class {
1318
1527
  return this.run(parsed, options2);
1319
1528
  }
1320
1529
  /**
1321
- * Execute a rows query (rows:{…}, #949) — the session results table plane.
1322
- * Reads event rows directly over the unified store (ticket 003): no
1323
- * WorkoutResult blob parsing; outputType narrowing hits the promoted column.
1530
+ * Execute a rows query (rows:<target>{…}, #949/C4) — the session results
1531
+ * table plane. Filter rules are validated at parse; this executes only.
1532
+ * Reads event rows directly over the unified store: outputType narrowing
1533
+ * hits the promoted column; content-plane targets scope by content.
1324
1534
  */
1325
1535
  async runRows(parsed, options2 = {}) {
1326
1536
  const empty = { parsed, runs: [] };
1327
1537
  if (parsed.error) return { ...empty, error: parsed.error };
1328
- const ROWS_SCOPE_KEYS = /* @__PURE__ */ new Set(["result", "block", "note"]);
1329
- const unsupported = parsed.filters.filter(
1330
- (f) => !ROWS_SCOPE_KEYS.has(f.key) || f.negate || f.values.some((v) => v.wildcard)
1331
- );
1332
- if (unsupported.length > 0) {
1333
- return {
1334
- ...empty,
1335
- error: `Unsupported rows filter(s): ${unsupported.map((f) => (f.negate ? "!" : "") + f.key).join(", ")}. Rows queries support exact result:, block:, note: values.`
1336
- };
1337
- }
1338
1538
  const scopeValues = (key) => parsed.filters.filter((f) => f.key === key).flatMap((f) => f.values.map((v) => v.value));
1339
1539
  const resultIds = scopeValues("result");
1340
1540
  const blockIds = scopeValues("block");
1341
1541
  const noteIds = scopeValues("note");
1342
1542
  if (resultIds.length + blockIds.length + noteIds.length === 0) {
1343
- return { ...empty, error: "Rows query needs a scope: result:, block:, or note:." };
1543
+ return { ...empty, runs: [] };
1344
1544
  }
1345
1545
  const byResult = /* @__PURE__ */ new Map();
1346
1546
  const collect = (rows) => {
@@ -1354,9 +1554,9 @@ var QueryService = class {
1354
1554
  for (const blockContentId of blockIds) collect(await this.store.getEventsByContent(blockContentId));
1355
1555
  for (const noteId of noteIds) collect(await this.store.getEventsForNote(noteId));
1356
1556
  let groups = [...byResult.entries()].filter(([, rows]) => rows.length > 0);
1357
- if (parsed.last) {
1557
+ if (parsed.window) {
1358
1558
  groups = groups.filter(
1359
- ([, rows]) => effectiveTimeWindow(rows[0].timestamp, parsed.last, void 0, options2.anchorNow)
1559
+ ([, rows]) => effectiveTimeWindow(rows[0].timestamp, parsed.window, void 0, options2.anchorNow)
1360
1560
  );
1361
1561
  }
1362
1562
  groups.sort((a, b) => b[1][0].timestamp - a[1][0].timestamp);
@@ -1364,7 +1564,7 @@ var QueryService = class {
1364
1564
  resultId,
1365
1565
  noteId: rows[0].noteId,
1366
1566
  timestamp: rows[0].timestamp,
1367
- events: parsed.outputType ? rows.filter((row) => row.outputType === parsed.outputType) : rows
1567
+ events: parsed.outputType && !ROWS_CONTENT_PLANES.has(parsed.outputType) ? rows.filter((row) => row.outputType === parsed.outputType) : rows
1368
1568
  })).filter((run) => run.events.length > 0);
1369
1569
  return { parsed, runs };
1370
1570
  }
@@ -1384,15 +1584,9 @@ var QueryService = class {
1384
1584
  return this.runFindEffort(parsed);
1385
1585
  }
1386
1586
  let notes = [];
1387
- const scope = parsed.scope || "journal";
1388
- if (scope === "journal" || scope === "all") {
1389
- notes = notes.concat(await this.noteStore.getAllNotes());
1390
- }
1391
- if (scope === "collections" || scope === "feeds" || scope === "all") {
1392
- let sNotes = this.staticNoteStore ? await this.staticNoteStore.getAllNotes() : [];
1393
- if (scope === "collections") sNotes = sNotes.filter((n) => n.sourceId?.startsWith("collection:"));
1394
- if (scope === "feeds") sNotes = sNotes.filter((n) => n.sourceId?.startsWith("feed:"));
1395
- notes = notes.concat(sNotes);
1587
+ notes = notes.concat(await this.noteStore.getAllNotes());
1588
+ if (this.staticNoteStore) {
1589
+ notes = notes.concat(await this.staticNoteStore.getAllNotes());
1396
1590
  }
1397
1591
  const selectedCount = notes.length;
1398
1592
  const anchorNow = windowAnchor(notes, parsed, options2);
@@ -1438,8 +1632,8 @@ var QueryService = class {
1438
1632
  });
1439
1633
  }
1440
1634
  }
1441
- if (parsed.last || options2.range) {
1442
- 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));
1443
1637
  }
1444
1638
  if (parsed.join) {
1445
1639
  const joined = await this.applyMetricJoin(parsed, notes, []);
@@ -1452,15 +1646,9 @@ var QueryService = class {
1452
1646
  */
1453
1647
  async runFindBlock(parsed, options2 = {}) {
1454
1648
  let blocks = [];
1455
- const scope = parsed.scope || "journal";
1456
- if (scope === "journal" || scope === "all") {
1457
- blocks = blocks.concat(await this.blockStore.getAllBlocks());
1458
- }
1459
- if (scope === "collections" || scope === "feeds" || scope === "all") {
1460
- let sBlocks = this.staticBlockStore ? await this.staticBlockStore.getAllBlocks() : [];
1461
- if (scope === "collections") sBlocks = sBlocks.filter((b) => b.sourceId?.startsWith("collection:"));
1462
- if (scope === "feeds") sBlocks = sBlocks.filter((b) => b.sourceId?.startsWith("feed:"));
1463
- blocks = blocks.concat(sBlocks);
1649
+ blocks = blocks.concat(await this.blockStore.getAllBlocks());
1650
+ if (this.staticBlockStore) {
1651
+ blocks = blocks.concat(await this.staticBlockStore.getAllBlocks());
1464
1652
  }
1465
1653
  const selectedCount = blocks.length;
1466
1654
  const anchorNow = windowAnchor(blocks, parsed, options2);
@@ -1500,8 +1688,8 @@ var QueryService = class {
1500
1688
  blocks = blocks.filter((b) => matchingNoteIds.has(b.noteId));
1501
1689
  }
1502
1690
  }
1503
- if (parsed.last || options2.range) {
1504
- 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));
1505
1693
  }
1506
1694
  if (parsed.join) {
1507
1695
  const joined = await this.applyMetricJoin(parsed, [], blocks);
@@ -1554,10 +1742,8 @@ var QueryService = class {
1554
1742
  };
1555
1743
  if (parsed.error) return empty;
1556
1744
  if (parsed.join) return this.runJoined(parsed, options2);
1557
- const windowed = options2.rangeStart !== void 0 || options2.rangeEnd !== void 0;
1558
- const start = options2.rangeStart ?? 0;
1559
- const end = options2.rangeEnd ?? Number.MAX_SAFE_INTEGER;
1560
- 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();
1561
1747
  const candidates = eventRows.flatMap(projectEventToFacts).filter((row) => row.metricKey === parsed.metric);
1562
1748
  const touchesTags = parsed.filters.some((f) => f.key === "tags") || parsed.groupBy.includes("tags");
1563
1749
  const noteTags = await this.loadNoteTags(candidates, touchesTags);
@@ -1575,8 +1761,24 @@ var QueryService = class {
1575
1761
  const timeDim = parsed.groupBy.find((d) => d === "day" || d === "week");
1576
1762
  const tagDims = parsed.groupBy.filter((d) => d !== "day" && d !== "week");
1577
1763
  const bucketMs = timeDim ? (timeDim === "week" ? 7 : 1) * DAY : parsed.rollup ? parsed.rollup.size * (parsed.rollup.unit === "w" ? 7 : 1) * DAY : null;
1578
- 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
+ };
1579
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
+ };
1580
1782
  const { unit: targetUnit, convert: shouldConvert } = resolveDisplayUnit(matched, {
1581
1783
  directive: parsed.displayUnit,
1582
1784
  preferred: options2.preferredUnit
@@ -1601,7 +1803,7 @@ var QueryService = class {
1601
1803
  (m) => toDisplayValue(m.value, m.unit ?? m.metricUnit, shouldConvert ? targetUnit : void 0)
1602
1804
  );
1603
1805
  return {
1604
- 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)),
1605
1807
  value: Math.round(aggregate(values, parsed.agg, members, shouldConvert ? targetUnit : void 0) * 100) / 100
1606
1808
  };
1607
1809
  });
@@ -1635,17 +1837,14 @@ var QueryService = class {
1635
1837
  raw: "",
1636
1838
  target: join.target,
1637
1839
  filters: join.filters,
1638
- scope: join.scope,
1639
- last: join.last
1840
+ window: join.last ? { kind: "relative", size: join.last.size, unit: join.last.unit } : void 0
1640
1841
  });
1641
1842
  const contentIds = await this.contentIdsFromFindResult(findResult);
1642
1843
  if (contentIds.size === 0) return empty;
1643
1844
  let facts = await this.deriveMetricFacts(contentIds, parsed.metric);
1644
- if (facts.length === 0) return empty;
1645
- if (options2.rangeStart !== void 0 || options2.rangeEnd !== void 0) {
1646
- const start = options2.rangeStart ?? 0;
1647
- const end = options2.rangeEnd ?? Number.MAX_SAFE_INTEGER;
1648
- 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);
1649
1848
  }
1650
1849
  const touchesTags = parsed.filters.some((f) => f.key === "tags") || parsed.groupBy.includes("tags");
1651
1850
  const noteTags = await this.loadNoteTags(facts, touchesTags);
@@ -2050,15 +2249,15 @@ sum:$metric{} rollup:1w
2050
2249
  }
2051
2250
 
2052
2251
  // src/version.ts
2053
- var VERSION = "0.7.38";
2054
- var GIT_SHA = "172cc75";
2055
- var BUILD_TIME = "2026-08-25T23:00:14.937Z";
2252
+ var VERSION = "0.9.40";
2253
+ var GIT_SHA = "2462ec9";
2254
+ var BUILD_TIME = "2026-08-26T01:04:47.412Z";
2056
2255
  var SEMVER = {
2057
- version: "0.7.38",
2058
- gitSha: "172cc75",
2059
- buildTime: "2026-08-25T23:00:14.937Z"
2256
+ version: "0.9.40",
2257
+ gitSha: "2462ec9",
2258
+ buildTime: "2026-08-26T01:04:47.412Z"
2060
2259
  };
2061
2260
 
2062
- 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_ROLLUP_PERIODS, 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 };
2063
2262
  //# sourceMappingURL=index.mjs.map
2064
2263
  //# sourceMappingURL=index.mjs.map