@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.cjs CHANGED
@@ -1,18 +1,16 @@
1
1
  'use strict';
2
2
 
3
+ var wodWikiCore = require('@bitcobblers/wod-wiki-core');
3
4
  var lr = require('@lezer/lr');
4
5
  var language = require('@codemirror/language');
5
6
  var autocomplete = require('@codemirror/autocomplete');
6
7
  var highlight = require('@lezer/highlight');
7
- var wodWikiCore = require('@bitcobblers/wod-wiki-core');
8
8
 
9
9
  var __defProp = Object.defineProperty;
10
10
  var __export = (target, all) => {
11
11
  for (var name in all)
12
12
  __defProp(target, name, { get: all[name], enumerable: true });
13
13
  };
14
-
15
- // src/vocabulary.ts
16
14
  var WQL_METRIC_FAMILIES = ["reps", "distance", "resistance", "elapsed", "power", "pace"];
17
15
  var WQL_METRIC_AGGREGATES = ["totalVolume", "totalDistance", "tis", "sessionLoad"];
18
16
  var WQL_AGGREGATORS = ["sum", "avg", "min", "max", "count", "last", "delta"];
@@ -57,7 +55,10 @@ var WQL_CALC_TARGETS = [
57
55
  var WQL_INTENSITY_TIERS = ["low", "moderate", "high"];
58
56
  var WQL_GRAINS = ["summary", "event"];
59
57
  var WQL_FIND_TARGETS = ["note", "block", "effort"];
60
- var WQL_SCOPES = ["journal", "collections", "feeds", "all"];
58
+ var WQL_SOURCE_VALUES = ["journal", "collections", "feeds", "all"];
59
+ var WQL_RESULT_PLANES = wodWikiCore.KNOWN_OUTPUT_TYPES;
60
+ var WQL_ROWS_TARGETS = [...WQL_FIND_TARGETS, ...WQL_RESULT_PLANES, "all"];
61
+ var WQL_ROWS_SCOPE_KEYS = ["result", "block", "note"];
61
62
  var WQL_CONTENT_FILTER_KEYS = ["type", "text", "has", "source", "catalog", ...WQL_TAG_KEYS];
62
63
  var WQL_SOURCES = ["journal", "collections", "feeds", "notes", "blocks", "efforts", "metrics"];
63
64
  var WQL_ROLLUP_PERIODS = ["1d", "1w"];
@@ -153,6 +154,7 @@ var DISPLAY_UNIT_RE = /\s+in\s+([a-zA-Z0-9_-]+)\s*$/;
153
154
  var ROLLUP_RE = /\.rollup\((\d+)?([a-zA-Z]*)\)\s*$/;
154
155
  var BY_RE = /\s+by\s+\{([^}]*)\}\s*$/;
155
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;
156
158
  var IN_SCOPE_RE = /\s+in\s+(\w+)\s*$/;
157
159
  function parseWqlSuffixes(raw) {
158
160
  const { primary, where } = splitAtWhere(raw);
@@ -160,8 +162,8 @@ function parseWqlSuffixes(raw) {
160
162
  let displayUnit;
161
163
  let rollup;
162
164
  let groupBy;
163
- let last;
164
- let scope;
165
+ let window;
166
+ let legacyScope;
165
167
  const conflicts = [];
166
168
  const isFind = text.startsWith("find:");
167
169
  const isRows = /^rows(?=[:{]|\s|$)/.test(text);
@@ -180,18 +182,40 @@ function parseWqlSuffixes(raw) {
180
182
  );
181
183
  }
182
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
+ }
183
214
  if (isFind || isRows) {
184
- const lasts = stripRepeated(LAST_RE);
185
- conflictFrom("window", lasts);
186
- if (lasts.length) {
187
- const m = lasts[lasts.length - 1];
188
- last = { size: parseInt(m[1], 10), unit: m[2].toLowerCase(), raw: m[0].trim() };
189
- }
190
- if (isFind) {
191
- const scopes = stripRepeated(IN_SCOPE_RE);
192
- conflictFrom("scope", scopes);
193
- if (scopes.length) scope = scopes[scopes.length - 1][1];
194
- } 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) {
195
219
  const rollups = stripRepeated(ROLLUP_RE);
196
220
  conflictFrom(".rollup", rollups);
197
221
  if (rollups.length) {
@@ -225,8 +249,8 @@ function parseWqlSuffixes(raw) {
225
249
  displayUnit,
226
250
  rollup,
227
251
  groupBy,
228
- last,
229
- scope,
252
+ window,
253
+ legacyScope,
230
254
  conflicts: conflicts.length ? conflicts : void 0,
231
255
  primaryText: text
232
256
  };
@@ -254,7 +278,10 @@ function parseJoinClause(where) {
254
278
  if (where.trimStart().startsWith("find:")) {
255
279
  const fp = parseFindQuery(where);
256
280
  if (fp.error) return { error: fp.error };
257
- 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 } };
258
285
  }
259
286
  const m = CMP_RE.exec(where.trim());
260
287
  if (!m) return { error: cannotParseJoin(where) };
@@ -271,27 +298,112 @@ function parseJoinClause(where) {
271
298
  };
272
299
  }
273
300
  function parseQuery(raw) {
274
- const trimmed = raw.trimStart();
301
+ const norm = normalizeWql(raw);
302
+ const trimmed = norm.query.trimStart();
303
+ let result;
275
304
  if (trimmed.startsWith("find:")) {
276
- 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);
277
310
  }
278
- if (/^rows(?=[:{]|\s|$)/.test(trimmed)) {
279
- return parseRowsQuery(raw);
311
+ if (norm.advisories.length) {
312
+ result.advisories = [...result.advisories ?? [], ...norm.advisories];
280
313
  }
281
- return parseAnalyticsQuery(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
+ }
353
+ }
354
+ return { query: text, advisories };
282
355
  }
283
356
  function cannotParseRows(text) {
284
- return `Cannot parse "${text}". Expected rows:{result:\u2026|block:\u2026|note:\u2026} or rows:segment{\u2026} last 8w`;
357
+ return `Cannot parse "${text}". Expected rows:all{result:\u2026|block:\u2026|note:\u2026}, rows:<plane>{\u2026}, or rows:segment{\u2026} last 8w`;
285
358
  }
359
+ function validateRowsFilters(filters) {
360
+ const scopeKeys = new Set(WQL_ROWS_SCOPE_KEYS);
361
+ const allowedKeys = /* @__PURE__ */ new Set([...WQL_ROWS_SCOPE_KEYS, "source"]);
362
+ const unsupported = filters.filter(
363
+ (f) => !allowedKeys.has(f.key) || f.negate || f.values.some((v) => v.wildcard)
364
+ );
365
+ if (unsupported.length > 0) {
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.`;
367
+ }
368
+ if (!filters.some((f) => scopeKeys.has(f.key))) {
369
+ return `Rows query needs a scope: ${WQL_ROWS_SCOPE_KEYS.map((k) => `${k}:`).join(", ")}.`;
370
+ }
371
+ return void 0;
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
+ }
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}.';
286
388
  function parseRowsQuery(raw) {
287
389
  const suffixes = parseWqlSuffixes(raw);
288
- 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
+ }
289
396
  const result = {
290
- raw,
291
397
  family: "rows",
398
+ raw,
292
399
  filters: [],
293
- last: last ? { size: last.size, unit: last.unit } : void 0
400
+ window: win.window,
401
+ ...advisories.length ? { advisories } : {}
294
402
  };
403
+ if (win.error) {
404
+ result.error = win.error;
405
+ return result;
406
+ }
295
407
  if (suffixes.conflicts?.length) {
296
408
  result.error = suffixes.conflicts.join("; ");
297
409
  return result;
@@ -301,44 +413,84 @@ function parseRowsQuery(raw) {
301
413
  return result;
302
414
  }
303
415
  const text = primaryText.trim();
304
- const brace = text.indexOf("{");
305
- const head = (brace === -1 ? text : text.slice(0, brace)).trim();
306
- const filterText = brace === -1 ? "" : text.slice(brace).trim();
307
- if (head === "rows" || head === "rows:") ; else if (head.startsWith("rows:")) {
308
- const target = head.slice("rows:".length).trim();
309
- if (!target || /\s/.test(target)) {
310
- result.error = cannotParseRows(text);
311
- return result;
312
- }
313
- result.outputType = target;
314
- } else {
416
+ if (/^rows:?\s*[{]?$/.test(text) || /^rows:?\s*[{]/.test(text)) {
417
+ result.error = BARE_ROWS_RETIRED;
418
+ return result;
419
+ }
420
+ const tree = parser.parse(text);
421
+ let syntaxError = false;
422
+ tree.iterate({ enter(node) {
423
+ if (node.type.isError) syntaxError = true;
424
+ } });
425
+ if (syntaxError) {
315
426
  result.error = cannotParseRows(text);
316
427
  return result;
317
428
  }
318
- if (filterText) {
319
- if (!/^\{.*\}$/.test(filterText)) {
320
- result.error = cannotParseRows(text);
321
- return result;
322
- }
323
- const synthetic = `find:_${filterText}`;
324
- const tree = parser.parse(synthetic);
325
- let syntaxError = false;
326
- tree.iterate({ enter(node) {
327
- if (node.type.isError) syntaxError = true;
328
- } });
329
- if (syntaxError) {
330
- result.error = cannotParseRows(text);
331
- return result;
332
- }
333
- result.filters = extractFilters(tree.topNode, synthetic);
334
- const grainError = retiredGrainRollup(result.filters);
335
- if (grainError) {
336
- result.error = grainError;
337
- return result;
338
- }
429
+ const query = tree.topNode;
430
+ const head = query.getChild(Head);
431
+ const aggNode = head?.getChild(Aggregator);
432
+ const metricNode = head?.getChild(Metric);
433
+ if (!head || !aggNode || !metricNode) {
434
+ result.error = BARE_ROWS_RETIRED;
435
+ return result;
436
+ }
437
+ const aggText = text.slice(aggNode.from, aggNode.to);
438
+ if (aggText !== "rows") {
439
+ result.error = cannotParseRows(text);
440
+ return result;
441
+ }
442
+ const target = text.slice(metricNode.from, metricNode.to);
443
+ if (!WQL_ROWS_TARGETS.includes(target)) {
444
+ result.error = `Unknown rows target "${target}". Try: ${WQL_ROWS_TARGETS.join(", ")}`;
445
+ return result;
446
+ }
447
+ if (target !== "all") result.outputType = target;
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
+ }
461
+ const grainError = retiredGrainRollup(result.filters);
462
+ if (grainError) {
463
+ result.error = grainError;
464
+ return result;
465
+ }
466
+ const filterError = validateRowsFilters(result.filters);
467
+ if (filterError) {
468
+ result.error = filterError;
469
+ return result;
339
470
  }
340
471
  return result;
341
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
+ }
342
494
  function retiredGrainRollup(filters) {
343
495
  if (filters.some((f) => f.key === "grain" && f.values.some((v) => v.value === "rollup"))) {
344
496
  return "grain:rollup is retired \u2014 rollup grains are never stored; compute them with the .rollup suffix";
@@ -374,7 +526,8 @@ function extractFilters(query, text) {
374
526
  }
375
527
  function parseAnalyticsQuery(raw) {
376
528
  const suffixes = parseWqlSuffixes(raw);
377
- 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);
378
531
  const base = {
379
532
  family: "aggregate",
380
533
  raw,
@@ -383,8 +536,13 @@ function parseAnalyticsQuery(raw) {
383
536
  filters: [],
384
537
  groupBy: groupBy ?? [],
385
538
  displayUnit,
386
- 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
387
541
  };
542
+ if (win.error) {
543
+ base.error = win.error;
544
+ return base;
545
+ }
388
546
  if (suffixes.conflicts?.length) {
389
547
  base.error = suffixes.conflicts.join("; ");
390
548
  return base;
@@ -438,19 +596,28 @@ function parseAnalyticsQuery(raw) {
438
596
  return base;
439
597
  }
440
598
  function cannotParseFind(text) {
441
- return `Cannot parse "${text}". Expected find:target{filters} in scope last 8w`;
599
+ return `Cannot parse "${text}". Expected find:target{filters} last 8w`;
442
600
  }
443
601
  function parseFindQuery(raw) {
444
602
  const suffixes = parseWqlSuffixes(raw);
445
- 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
+ }
446
609
  const result = {
447
610
  family: "find",
448
611
  raw,
449
612
  target: "",
450
613
  filters: [],
451
- scope,
452
- last: last ? { size: last.size, unit: last.unit } : void 0
614
+ window: win.window,
615
+ ...advisories.length ? { advisories } : {}
453
616
  };
617
+ if (win.error) {
618
+ result.error = win.error;
619
+ return result;
620
+ }
454
621
  if (suffixes.conflicts?.length) {
455
622
  result.error = suffixes.conflicts.join("; ");
456
623
  return result;
@@ -478,7 +645,23 @@ function parseFindQuery(raw) {
478
645
  return result;
479
646
  }
480
647
  result.target = text.slice(metricNode.from, metricNode.to);
648
+ if (!WQL_FIND_TARGETS.includes(result.target)) {
649
+ result.error = `Unknown find target "${result.target}". Try: ${WQL_FIND_TARGETS.join(", ")}`;
650
+ return result;
651
+ }
481
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
+ }
482
665
  const findGrainError = retiredGrainRollup(result.filters);
483
666
  if (findGrainError) {
484
667
  result.error = findGrainError;
@@ -603,6 +786,8 @@ function wqlCompletionSource(options_ = {}) {
603
786
  return options(WQL_GRAINS);
604
787
  case "metric":
605
788
  return metricOptions();
789
+ case "source":
790
+ return options(WQL_SOURCE_VALUES);
606
791
  default:
607
792
  return null;
608
793
  }
@@ -1095,8 +1280,12 @@ function staticTagIndexFromBlocks(blocks) {
1095
1280
 
1096
1281
  // src/QueryService.ts
1097
1282
  var DAY = 864e5;
1283
+ var ROWS_CONTENT_PLANES = new Set(WQL_FIND_TARGETS);
1098
1284
  function localDateString(ts) {
1099
- 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}`;
1100
1289
  }
1101
1290
  function catalogOfItem(item) {
1102
1291
  if (item.catalog) return item.catalog;
@@ -1106,9 +1295,13 @@ function catalogOfItem(item) {
1106
1295
  return clean.split("/")[0];
1107
1296
  }
1108
1297
  function sourceMatches(sourceId, kind) {
1298
+ if (kind === "all") return true;
1109
1299
  if (kind === "journal") return !sourceId || sourceId === "journal";
1110
- if (kind === "collection" || kind === "feed") {
1111
- 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:");
1112
1305
  }
1113
1306
  return sourceId === kind;
1114
1307
  }
@@ -1123,12 +1316,26 @@ function applySourceFilter(items, filters) {
1123
1316
  }
1124
1317
  return items;
1125
1318
  }
1126
- function effectiveTimeWindow(createdAt, last, range, anchorNow) {
1127
- if (range) return createdAt >= range.start && createdAt <= range.end;
1128
- if (last) {
1129
- const cutoff = (anchorNow ?? Date.now()) - last.size * (last.unit === "w" ? 7 : 1) * DAY;
1130
- 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 };
1131
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;
1132
1339
  return true;
1133
1340
  }
1134
1341
  function latestActivity(rows) {
@@ -1138,7 +1345,7 @@ function latestActivity(rows) {
1138
1345
  }
1139
1346
  function windowAnchor(selected, parsed, options2) {
1140
1347
  if (options2.anchorNow !== void 0) return options2.anchorNow;
1141
- if (options2.anchor === "latest-activity" && parsed.last) return latestActivity(selected);
1348
+ if (options2.anchor === "latest-activity" && parsed.window?.kind === "relative") return latestActivity(selected);
1142
1349
  return void 0;
1143
1350
  }
1144
1351
  var defaultEventStore = {
@@ -1208,9 +1415,11 @@ function matchesFilters(row, filters, noteTags) {
1208
1415
  const negate = group[0].negate;
1209
1416
  const raw = factTagValue(row, key, noteTags);
1210
1417
  const rowValues = Array.isArray(raw) ? raw : raw !== void 0 ? [raw] : [];
1211
- const hit = group.some(
1212
- (f) => f.values.some(
1213
- (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
+ )
1214
1423
  )
1215
1424
  );
1216
1425
  return negate ? !hit : hit;
@@ -1220,8 +1429,8 @@ function dimValue(row, dim, noteTags) {
1220
1429
  if (dim === "day") return localDateString(row.timestamp);
1221
1430
  if (dim === "week") {
1222
1431
  const d = new Date(row.timestamp);
1223
- const monday = new Date(row.timestamp - (d.getDay() + 6) % 7 * DAY);
1224
- 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());
1225
1434
  }
1226
1435
  if (dim === "session") return row.resultId;
1227
1436
  const raw = factTagValue(row, dim, noteTags);
@@ -1320,29 +1529,20 @@ var QueryService = class {
1320
1529
  return this.run(parsed, options2);
1321
1530
  }
1322
1531
  /**
1323
- * Execute a rows query (rows:{…}, #949) — the session results table plane.
1324
- * Reads event rows directly over the unified store (ticket 003): no
1325
- * WorkoutResult blob parsing; outputType narrowing hits the promoted column.
1532
+ * Execute a rows query (rows:<target>{…}, #949/C4) — the session results
1533
+ * table plane. Filter rules are validated at parse; this executes only.
1534
+ * Reads event rows directly over the unified store: outputType narrowing
1535
+ * hits the promoted column; content-plane targets scope by content.
1326
1536
  */
1327
1537
  async runRows(parsed, options2 = {}) {
1328
1538
  const empty = { parsed, runs: [] };
1329
1539
  if (parsed.error) return { ...empty, error: parsed.error };
1330
- const ROWS_SCOPE_KEYS = /* @__PURE__ */ new Set(["result", "block", "note"]);
1331
- const unsupported = parsed.filters.filter(
1332
- (f) => !ROWS_SCOPE_KEYS.has(f.key) || f.negate || f.values.some((v) => v.wildcard)
1333
- );
1334
- if (unsupported.length > 0) {
1335
- return {
1336
- ...empty,
1337
- error: `Unsupported rows filter(s): ${unsupported.map((f) => (f.negate ? "!" : "") + f.key).join(", ")}. Rows queries support exact result:, block:, note: values.`
1338
- };
1339
- }
1340
1540
  const scopeValues = (key) => parsed.filters.filter((f) => f.key === key).flatMap((f) => f.values.map((v) => v.value));
1341
1541
  const resultIds = scopeValues("result");
1342
1542
  const blockIds = scopeValues("block");
1343
1543
  const noteIds = scopeValues("note");
1344
1544
  if (resultIds.length + blockIds.length + noteIds.length === 0) {
1345
- return { ...empty, error: "Rows query needs a scope: result:, block:, or note:." };
1545
+ return { ...empty, runs: [] };
1346
1546
  }
1347
1547
  const byResult = /* @__PURE__ */ new Map();
1348
1548
  const collect = (rows) => {
@@ -1356,9 +1556,9 @@ var QueryService = class {
1356
1556
  for (const blockContentId of blockIds) collect(await this.store.getEventsByContent(blockContentId));
1357
1557
  for (const noteId of noteIds) collect(await this.store.getEventsForNote(noteId));
1358
1558
  let groups = [...byResult.entries()].filter(([, rows]) => rows.length > 0);
1359
- if (parsed.last) {
1559
+ if (parsed.window) {
1360
1560
  groups = groups.filter(
1361
- ([, rows]) => effectiveTimeWindow(rows[0].timestamp, parsed.last, void 0, options2.anchorNow)
1561
+ ([, rows]) => effectiveTimeWindow(rows[0].timestamp, parsed.window, void 0, options2.anchorNow)
1362
1562
  );
1363
1563
  }
1364
1564
  groups.sort((a, b) => b[1][0].timestamp - a[1][0].timestamp);
@@ -1366,7 +1566,7 @@ var QueryService = class {
1366
1566
  resultId,
1367
1567
  noteId: rows[0].noteId,
1368
1568
  timestamp: rows[0].timestamp,
1369
- events: parsed.outputType ? rows.filter((row) => row.outputType === parsed.outputType) : rows
1569
+ events: parsed.outputType && !ROWS_CONTENT_PLANES.has(parsed.outputType) ? rows.filter((row) => row.outputType === parsed.outputType) : rows
1370
1570
  })).filter((run) => run.events.length > 0);
1371
1571
  return { parsed, runs };
1372
1572
  }
@@ -1386,15 +1586,9 @@ var QueryService = class {
1386
1586
  return this.runFindEffort(parsed);
1387
1587
  }
1388
1588
  let notes = [];
1389
- const scope = parsed.scope || "journal";
1390
- if (scope === "journal" || scope === "all") {
1391
- notes = notes.concat(await this.noteStore.getAllNotes());
1392
- }
1393
- if (scope === "collections" || scope === "feeds" || scope === "all") {
1394
- let sNotes = this.staticNoteStore ? await this.staticNoteStore.getAllNotes() : [];
1395
- if (scope === "collections") sNotes = sNotes.filter((n) => n.sourceId?.startsWith("collection:"));
1396
- if (scope === "feeds") sNotes = sNotes.filter((n) => n.sourceId?.startsWith("feed:"));
1397
- notes = notes.concat(sNotes);
1589
+ notes = notes.concat(await this.noteStore.getAllNotes());
1590
+ if (this.staticNoteStore) {
1591
+ notes = notes.concat(await this.staticNoteStore.getAllNotes());
1398
1592
  }
1399
1593
  const selectedCount = notes.length;
1400
1594
  const anchorNow = windowAnchor(notes, parsed, options2);
@@ -1440,8 +1634,8 @@ var QueryService = class {
1440
1634
  });
1441
1635
  }
1442
1636
  }
1443
- if (parsed.last || options2.range) {
1444
- 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));
1445
1639
  }
1446
1640
  if (parsed.join) {
1447
1641
  const joined = await this.applyMetricJoin(parsed, notes, []);
@@ -1454,15 +1648,9 @@ var QueryService = class {
1454
1648
  */
1455
1649
  async runFindBlock(parsed, options2 = {}) {
1456
1650
  let blocks = [];
1457
- const scope = parsed.scope || "journal";
1458
- if (scope === "journal" || scope === "all") {
1459
- blocks = blocks.concat(await this.blockStore.getAllBlocks());
1460
- }
1461
- if (scope === "collections" || scope === "feeds" || scope === "all") {
1462
- let sBlocks = this.staticBlockStore ? await this.staticBlockStore.getAllBlocks() : [];
1463
- if (scope === "collections") sBlocks = sBlocks.filter((b) => b.sourceId?.startsWith("collection:"));
1464
- if (scope === "feeds") sBlocks = sBlocks.filter((b) => b.sourceId?.startsWith("feed:"));
1465
- blocks = blocks.concat(sBlocks);
1651
+ blocks = blocks.concat(await this.blockStore.getAllBlocks());
1652
+ if (this.staticBlockStore) {
1653
+ blocks = blocks.concat(await this.staticBlockStore.getAllBlocks());
1466
1654
  }
1467
1655
  const selectedCount = blocks.length;
1468
1656
  const anchorNow = windowAnchor(blocks, parsed, options2);
@@ -1502,8 +1690,8 @@ var QueryService = class {
1502
1690
  blocks = blocks.filter((b) => matchingNoteIds.has(b.noteId));
1503
1691
  }
1504
1692
  }
1505
- if (parsed.last || options2.range) {
1506
- 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));
1507
1695
  }
1508
1696
  if (parsed.join) {
1509
1697
  const joined = await this.applyMetricJoin(parsed, [], blocks);
@@ -1556,10 +1744,8 @@ var QueryService = class {
1556
1744
  };
1557
1745
  if (parsed.error) return empty;
1558
1746
  if (parsed.join) return this.runJoined(parsed, options2);
1559
- const windowed = options2.rangeStart !== void 0 || options2.rangeEnd !== void 0;
1560
- const start = options2.rangeStart ?? 0;
1561
- const end = options2.rangeEnd ?? Number.MAX_SAFE_INTEGER;
1562
- 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();
1563
1749
  const candidates = eventRows.flatMap(projectEventToFacts).filter((row) => row.metricKey === parsed.metric);
1564
1750
  const touchesTags = parsed.filters.some((f) => f.key === "tags") || parsed.groupBy.includes("tags");
1565
1751
  const noteTags = await this.loadNoteTags(candidates, touchesTags);
@@ -1577,8 +1763,24 @@ var QueryService = class {
1577
1763
  const timeDim = parsed.groupBy.find((d) => d === "day" || d === "week");
1578
1764
  const tagDims = parsed.groupBy.filter((d) => d !== "day" && d !== "week");
1579
1765
  const bucketMs = timeDim ? (timeDim === "week" ? 7 : 1) * DAY : parsed.rollup ? parsed.rollup.size * (parsed.rollup.unit === "w" ? 7 : 1) * DAY : null;
1580
- 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
+ };
1581
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
+ };
1582
1784
  const { unit: targetUnit, convert: shouldConvert } = resolveDisplayUnit(matched, {
1583
1785
  directive: parsed.displayUnit,
1584
1786
  preferred: options2.preferredUnit
@@ -1603,7 +1805,7 @@ var QueryService = class {
1603
1805
  (m) => toDisplayValue(m.value, m.unit ?? m.metricUnit, shouldConvert ? targetUnit : void 0)
1604
1806
  );
1605
1807
  return {
1606
- 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)),
1607
1809
  value: Math.round(aggregate(values, parsed.agg, members, shouldConvert ? targetUnit : void 0) * 100) / 100
1608
1810
  };
1609
1811
  });
@@ -1637,17 +1839,14 @@ var QueryService = class {
1637
1839
  raw: "",
1638
1840
  target: join.target,
1639
1841
  filters: join.filters,
1640
- scope: join.scope,
1641
- last: join.last
1842
+ window: join.last ? { kind: "relative", size: join.last.size, unit: join.last.unit } : void 0
1642
1843
  });
1643
1844
  const contentIds = await this.contentIdsFromFindResult(findResult);
1644
1845
  if (contentIds.size === 0) return empty;
1645
1846
  let facts = await this.deriveMetricFacts(contentIds, parsed.metric);
1646
- if (facts.length === 0) return empty;
1647
- if (options2.rangeStart !== void 0 || options2.rangeEnd !== void 0) {
1648
- const start = options2.rangeStart ?? 0;
1649
- const end = options2.rangeEnd ?? Number.MAX_SAFE_INTEGER;
1650
- 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);
1651
1850
  }
1652
1851
  const touchesTags = parsed.filters.some((f) => f.key === "tags") || parsed.groupBy.includes("tags");
1653
1852
  const noteTags = await this.loadNoteTags(facts, touchesTags);
@@ -2052,13 +2251,13 @@ sum:$metric{} rollup:1w
2052
2251
  }
2053
2252
 
2054
2253
  // src/version.ts
2055
- var VERSION = "0.7.38";
2056
- var GIT_SHA = "172cc75";
2057
- var BUILD_TIME = "2026-08-25T23:00:14.937Z";
2254
+ var VERSION = "0.9.40";
2255
+ var GIT_SHA = "2462ec9";
2256
+ var BUILD_TIME = "2026-08-26T01:04:47.412Z";
2058
2257
  var SEMVER = {
2059
- version: "0.7.38",
2060
- gitSha: "172cc75",
2061
- buildTime: "2026-08-25T23:00:14.937Z"
2258
+ version: "0.9.40",
2259
+ gitSha: "2462ec9",
2260
+ buildTime: "2026-08-26T01:04:47.412Z"
2062
2261
  };
2063
2262
 
2064
2263
  exports.BUILD_TIME = BUILD_TIME;
@@ -2085,9 +2284,12 @@ exports.WQL_INTENSITY_TIERS = WQL_INTENSITY_TIERS;
2085
2284
  exports.WQL_KEYWORDS = WQL_KEYWORDS;
2086
2285
  exports.WQL_METRIC_AGGREGATES = WQL_METRIC_AGGREGATES;
2087
2286
  exports.WQL_METRIC_FAMILIES = WQL_METRIC_FAMILIES;
2287
+ exports.WQL_RESULT_PLANES = WQL_RESULT_PLANES;
2088
2288
  exports.WQL_ROLLUP_PERIODS = WQL_ROLLUP_PERIODS;
2089
- exports.WQL_SCOPES = WQL_SCOPES;
2289
+ exports.WQL_ROWS_SCOPE_KEYS = WQL_ROWS_SCOPE_KEYS;
2290
+ exports.WQL_ROWS_TARGETS = WQL_ROWS_TARGETS;
2090
2291
  exports.WQL_SOURCES = WQL_SOURCES;
2292
+ exports.WQL_SOURCE_VALUES = WQL_SOURCE_VALUES;
2091
2293
  exports.WQL_TAG_KEYS = WQL_TAG_KEYS;
2092
2294
  exports.WQL_VIRTUAL_DIMS = WQL_VIRTUAL_DIMS;
2093
2295
  exports.buildDashboardDocument = buildDashboardDocument;
@@ -2109,6 +2311,7 @@ exports.isFindQuery = isFindQuery;
2109
2311
  exports.isProposedMetric = isProposedMetric;
2110
2312
  exports.isRowsQuery = isRowsQuery;
2111
2313
  exports.normalizeSummaryFacts = normalizeSummaryFacts;
2314
+ exports.normalizeWql = normalizeWql;
2112
2315
  exports.parseDashboardNote = parseDashboardNote;
2113
2316
  exports.parseFlatProperties = parseFlatProperties;
2114
2317
  exports.parseFrontmatter = parseFrontmatter;