@bitcobblers/wod-wiki-wql 0.6.32 → 0.7.37

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
  "calc.sends"
54
54
  ];
55
55
  var WQL_INTENSITY_TIERS = ["low", "moderate", "high"];
56
- var WQL_GRAINS = ["segment", "summary", "rollup"];
56
+ var WQL_GRAINS = ["summary", "event"];
57
57
  var WQL_FIND_TARGETS = ["note", "block", "effort"];
58
58
  var WQL_SCOPES = ["journal", "collections", "feeds", "all"];
59
59
  var WQL_CONTENT_FILTER_KEYS = ["type", "text", "has", "source", "catalog", ...WQL_TAG_KEYS];
@@ -160,55 +160,62 @@ function parseWqlSuffixes(raw) {
160
160
  let groupBy;
161
161
  let last;
162
162
  let scope;
163
+ const conflicts = [];
163
164
  const isFind = text.startsWith("find:");
164
165
  const isRows = /^rows(?=[:{]|\s|$)/.test(text);
166
+ const stripRepeated = (re) => {
167
+ const matches = [];
168
+ for (let m = re.exec(text); m; m = re.exec(text)) {
169
+ matches.unshift(m);
170
+ text = text.slice(0, m.index).trim();
171
+ }
172
+ return matches;
173
+ };
174
+ const conflictFrom = (label, ms) => {
175
+ if (ms.length > 1) {
176
+ conflicts.push(
177
+ `Duplicate '${label}' clause: '${ms[0][0].trim()}' conflicts with '${ms[ms.length - 1][0].trim()}'`
178
+ );
179
+ }
180
+ };
165
181
  if (isFind || isRows) {
166
- const lastMatch = LAST_RE.exec(text);
167
- if (lastMatch) {
168
- last = {
169
- size: parseInt(lastMatch[1], 10),
170
- unit: lastMatch[2].toLowerCase(),
171
- raw: lastMatch[0].trim()
172
- };
173
- text = text.slice(0, lastMatch.index).trim();
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() };
174
187
  }
175
188
  if (isFind) {
176
- const scopeMatch = IN_SCOPE_RE.exec(text);
177
- if (scopeMatch) {
178
- scope = scopeMatch[1];
179
- text = text.slice(0, scopeMatch.index).trim();
180
- }
189
+ const scopes = stripRepeated(IN_SCOPE_RE);
190
+ conflictFrom("scope", scopes);
191
+ if (scopes.length) scope = scopes[scopes.length - 1][1];
181
192
  } else {
182
- const rollupMatch = ROLLUP_RE.exec(text);
183
- if (rollupMatch) {
184
- const rawRollup = rollupMatch[1] + rollupMatch[2];
185
- rollup = { size: rollupMatch[1] ? parseInt(rollupMatch[1], 10) : 1, unit: rollupMatch[2] || "", raw: rawRollup };
186
- text = text.slice(0, rollupMatch.index).trim();
193
+ const rollups = stripRepeated(ROLLUP_RE);
194
+ conflictFrom(".rollup", rollups);
195
+ if (rollups.length) {
196
+ const m = rollups[rollups.length - 1];
197
+ rollup = { size: m[1] ? parseInt(m[1], 10) : 1, unit: m[2] || "", raw: m[1] + m[2] };
187
198
  }
188
- const byMatch = BY_RE.exec(text);
189
- if (byMatch) {
190
- groupBy = byMatch[1].split(",").map((d) => d.trim()).filter(Boolean);
191
- text = text.slice(0, byMatch.index).trim();
199
+ const bys = stripRepeated(BY_RE);
200
+ conflictFrom("by", bys);
201
+ if (bys.length) {
202
+ groupBy = bys[bys.length - 1][1].split(",").map((d) => d.trim()).filter(Boolean);
192
203
  }
193
204
  }
194
205
  } else {
195
- const unitMatch = DISPLAY_UNIT_RE.exec(text);
196
- if (unitMatch) {
197
- displayUnit = unitMatch[1];
198
- text = text.slice(0, unitMatch.index).trim();
199
- }
200
- const rollupMatch = ROLLUP_RE.exec(text);
201
- if (rollupMatch) {
202
- const rawRollup = rollupMatch[1] + rollupMatch[2];
203
- const size = rollupMatch[1] ? parseInt(rollupMatch[1], 10) : 1;
204
- const unit = rollupMatch[2] || "";
205
- rollup = { size, unit, raw: rawRollup };
206
- text = text.slice(0, rollupMatch.index).trim();
206
+ const units = stripRepeated(DISPLAY_UNIT_RE);
207
+ conflictFrom("display-unit", units);
208
+ if (units.length) displayUnit = units[units.length - 1][1];
209
+ const rollups = stripRepeated(ROLLUP_RE);
210
+ conflictFrom(".rollup", rollups);
211
+ if (rollups.length) {
212
+ const m = rollups[rollups.length - 1];
213
+ rollup = { size: m[1] ? parseInt(m[1], 10) : 1, unit: m[2] || "", raw: m[1] + m[2] };
207
214
  }
208
- const byMatch = BY_RE.exec(text);
209
- if (byMatch) {
210
- groupBy = byMatch[1].split(",").map((d) => d.trim()).filter(Boolean);
211
- text = text.slice(0, byMatch.index).trim();
215
+ const bys = stripRepeated(BY_RE);
216
+ conflictFrom("by", bys);
217
+ if (bys.length) {
218
+ groupBy = bys[bys.length - 1][1].split(",").map((d) => d.trim()).filter(Boolean);
212
219
  }
213
220
  }
214
221
  return {
@@ -218,6 +225,7 @@ function parseWqlSuffixes(raw) {
218
225
  groupBy,
219
226
  last,
220
227
  scope,
228
+ conflicts: conflicts.length ? conflicts : void 0,
221
229
  primaryText: text
222
230
  };
223
231
  }
@@ -279,6 +287,10 @@ function parseRowsQuery(raw) {
279
287
  filters: [],
280
288
  last: last ? { size: last.size, unit: last.unit } : void 0
281
289
  };
290
+ if (suffixes.conflicts?.length) {
291
+ result.error = suffixes.conflicts.join("; ");
292
+ return result;
293
+ }
282
294
  if (whereText || groupBy || rollup) {
283
295
  result.error = `Rows queries return raw statements \u2014 no where / by / rollup. Got "${primaryText.trim()}"`;
284
296
  return result;
@@ -314,9 +326,20 @@ function parseRowsQuery(raw) {
314
326
  return result;
315
327
  }
316
328
  result.filters = extractFilters(tree.topNode, synthetic);
329
+ const grainError = retiredGrainRollup(result.filters);
330
+ if (grainError) {
331
+ result.error = grainError;
332
+ return result;
333
+ }
317
334
  }
318
335
  return result;
319
336
  }
337
+ function retiredGrainRollup(filters) {
338
+ if (filters.some((f) => f.key === "grain" && f.values.some((v) => v.value === "rollup"))) {
339
+ return "grain:rollup is retired \u2014 rollup grains are never stored; compute them with the .rollup suffix";
340
+ }
341
+ return void 0;
342
+ }
320
343
  function extractFilters(query, text) {
321
344
  const out = [];
322
345
  const filters = query.getChild(Filters);
@@ -356,6 +379,10 @@ function parseAnalyticsQuery(raw) {
356
379
  displayUnit,
357
380
  rollup: rollup ? { size: rollup.size, unit: rollup.unit } : void 0
358
381
  };
382
+ if (suffixes.conflicts?.length) {
383
+ base.error = suffixes.conflicts.join("; ");
384
+ return base;
385
+ }
359
386
  if (rollup && rollup.unit !== "d" && rollup.unit !== "w") {
360
387
  base.error = cannotParse(text);
361
388
  return base;
@@ -385,6 +412,11 @@ function parseAnalyticsQuery(raw) {
385
412
  base.agg = aggText;
386
413
  base.metric = text.slice(metricNode.from, metricNode.to);
387
414
  base.filters = extractFilters(query, text);
415
+ const grainError = retiredGrainRollup(base.filters);
416
+ if (grainError) {
417
+ base.error = grainError;
418
+ return base;
419
+ }
388
420
  if (whereText) {
389
421
  const join = parseJoinClause(whereText);
390
422
  if (join.error) {
@@ -412,6 +444,10 @@ function parseFindQuery(raw) {
412
444
  scope,
413
445
  last: last ? { size: last.size, unit: last.unit } : void 0
414
446
  };
447
+ if (suffixes.conflicts?.length) {
448
+ result.error = suffixes.conflicts.join("; ");
449
+ return result;
450
+ }
415
451
  const tree = parser.parse(text);
416
452
  let syntaxError = false;
417
453
  tree.iterate({ enter(node) {
@@ -436,6 +472,11 @@ function parseFindQuery(raw) {
436
472
  }
437
473
  result.target = text.slice(metricNode.from, metricNode.to);
438
474
  result.filters = extractFilters(query, text);
475
+ const findGrainError = retiredGrainRollup(result.filters);
476
+ if (findGrainError) {
477
+ result.error = findGrainError;
478
+ return result;
479
+ }
439
480
  if (whereText) {
440
481
  const join = parseJoinClause(whereText);
441
482
  if (join.error) {
@@ -721,9 +762,8 @@ function readGroupTags(metadata) {
721
762
  const entries = Object.entries(tags).filter((pair) => typeof pair[1] === "string" && pair[1].length > 0).sort(([a], [b]) => a < b ? -1 : 1);
722
763
  return entries.length > 0 ? Object.fromEntries(entries) : void 0;
723
764
  }
724
- function normalizeSummaryFacts(logs, identity) {
725
- const now = Date.now();
726
- const rowsByKey = /* @__PURE__ */ new Map();
765
+ function foldSummaryOutputs(logs) {
766
+ const folded = /* @__PURE__ */ new Map();
727
767
  for (const output of logs) {
728
768
  if (output.outputType !== "analytics") continue;
729
769
  const label = output.metrics.find((m) => m.type === MetricType.Label);
@@ -738,32 +778,171 @@ function normalizeSummaryFacts(logs, identity) {
738
778
  const grade = metadataString(value.metadata, "grade");
739
779
  const groupTags = readGroupTags(value.metadata) ?? (effortSlug ? { effort: effortSlug } : void 0);
740
780
  const rowKey = groupTags ? `${metricKey}:${Object.entries(groupTags).map(([k, v]) => `${k}=${v}`).join(":")}` : metricKey;
741
- rowsByKey.set(rowKey, {
742
- id: `${identity.resultId}-${rowKey}-${now}`,
743
- noteId: identity.noteId,
744
- blockContentId: identity.blockContentId,
745
- origin: identity.origin,
746
- pageId: identity.pageId,
747
- grain: "summary",
748
- segmentId: identity.segmentId ?? "",
749
- segmentVersion: identity.segmentVersion ?? 0,
750
- resultId: identity.resultId,
751
- type: metricKey,
781
+ folded.set(rowKey, {
782
+ projectionName,
783
+ metricKey,
752
784
  value: value.value,
753
785
  unit: value.unit,
754
- label: projectionName,
755
- metricKey,
756
- metricLabel: projectionName,
757
- metricUnit: value.unit,
758
- ...effortSlug ? { effortSlug } : {},
759
- ...discipline ? { discipline } : {},
760
- ...intensityTier ? { intensityTier } : {},
761
- ...grade ? { grade } : {},
762
- timestamp: identity.workoutTimestamp ?? output.timeSpan?.started ?? now,
763
- createdAt: now
786
+ effortSlug,
787
+ discipline,
788
+ intensityTier,
789
+ grade,
790
+ groupTags,
791
+ rowKey,
792
+ started: output.timeSpan?.started
764
793
  });
765
794
  }
766
- return Array.from(rowsByKey.values());
795
+ return folded;
796
+ }
797
+ function normalizeSummaryFacts(logs, identity) {
798
+ const now = Date.now();
799
+ return Array.from(foldSummaryOutputs(logs).values(), (f) => ({
800
+ id: `${identity.resultId}-${f.rowKey}-${now}`,
801
+ noteId: identity.noteId,
802
+ blockContentId: identity.blockContentId,
803
+ origin: identity.origin,
804
+ pageId: identity.pageId,
805
+ grain: "summary",
806
+ segmentId: identity.segmentId ?? "",
807
+ segmentVersion: identity.segmentVersion ?? 0,
808
+ resultId: identity.resultId,
809
+ type: f.metricKey,
810
+ value: f.value,
811
+ unit: f.unit,
812
+ label: f.projectionName,
813
+ metricKey: f.metricKey,
814
+ metricLabel: f.projectionName,
815
+ metricUnit: f.unit,
816
+ ...f.effortSlug ? { effortSlug: f.effortSlug } : {},
817
+ ...f.discipline ? { discipline: f.discipline } : {},
818
+ ...f.intensityTier ? { intensityTier: f.intensityTier } : {},
819
+ ...f.grade ? { grade: f.grade } : {},
820
+ timestamp: identity.workoutTimestamp ?? f.started ?? now,
821
+ createdAt: now
822
+ }));
823
+ }
824
+ function firstEffortSlug(metrics) {
825
+ const loose = metrics;
826
+ for (const m of loose) {
827
+ const slug = metadataString(m.metadata, "effortSlug");
828
+ if (slug) return slug;
829
+ }
830
+ return void 0;
831
+ }
832
+ function toEventRows(logs, identity) {
833
+ return logs.map((output, seq) => ({
834
+ id: `${identity.resultId}:${seq}`,
835
+ resultId: identity.resultId,
836
+ noteId: identity.noteId,
837
+ blockContentId: identity.blockContentId,
838
+ pageId: identity.pageId,
839
+ origin: identity.origin,
840
+ timestamp: identity.workoutTimestamp ?? output.timeSpan?.started ?? Date.now(),
841
+ grain: "event",
842
+ outputType: output.outputType ?? "segment",
843
+ effortSlug: firstEffortSlug(output.metrics),
844
+ metrics: output.metrics,
845
+ timeSpan: output.timeSpan?.started !== void 0 ? { started: output.timeSpan.started, ended: output.timeSpan.ended } : void 0,
846
+ sourceBlockKey: output.sourceBlockKey,
847
+ stackLevel: output.stackLevel,
848
+ completionReason: output.completionReason,
849
+ segmentId: identity.segmentId,
850
+ segmentVersion: identity.segmentVersion
851
+ }));
852
+ }
853
+ function toSummaryEventRows(logs, identity) {
854
+ const now = Date.now();
855
+ return Array.from(foldSummaryOutputs(logs).values(), (f) => ({
856
+ id: `${identity.resultId}:summary:${f.rowKey}`,
857
+ resultId: identity.resultId,
858
+ noteId: identity.noteId,
859
+ blockContentId: identity.blockContentId,
860
+ pageId: identity.pageId,
861
+ origin: identity.origin,
862
+ timestamp: identity.workoutTimestamp ?? f.started ?? now,
863
+ grain: "summary",
864
+ outputType: "analytics",
865
+ effortSlug: f.effortSlug,
866
+ metrics: [{
867
+ type: f.metricKey,
868
+ value: f.value,
869
+ ...f.unit ? { unit: f.unit } : {},
870
+ metadata: {
871
+ canonicalKey: f.metricKey,
872
+ ...f.effortSlug ? { effortSlug: f.effortSlug } : {},
873
+ ...f.discipline ? { effortDiscipline: f.discipline } : {},
874
+ ...f.intensityTier ? { effortIntensityTier: f.intensityTier } : {},
875
+ ...f.groupTags ? { groupTags: f.groupTags } : {},
876
+ ...f.grade ? { grade: f.grade } : {}
877
+ }
878
+ }],
879
+ segmentId: identity.segmentId,
880
+ segmentVersion: identity.segmentVersion
881
+ }));
882
+ }
883
+ function projectEventToFacts(record) {
884
+ const metrics = record.metrics;
885
+ if (record.grain === "summary") {
886
+ const m = metrics[0];
887
+ if (!m || typeof m.value !== "number") return [];
888
+ const metricKey = metadataString(m.metadata, "canonicalKey") ?? (m.type ?? "");
889
+ return [{
890
+ id: `${record.id}:0`,
891
+ noteId: record.noteId,
892
+ blockContentId: record.blockContentId,
893
+ origin: record.origin,
894
+ pageId: record.pageId,
895
+ grain: "summary",
896
+ segmentId: record.segmentId ?? "",
897
+ segmentVersion: record.segmentVersion ?? 0,
898
+ resultId: record.resultId,
899
+ type: metricKey,
900
+ value: m.value,
901
+ unit: m.unit,
902
+ label: metricKey,
903
+ metricKey,
904
+ metricLabel: metricKey,
905
+ metricUnit: m.unit,
906
+ effortSlug: metadataString(m.metadata, "effortSlug") ?? record.effortSlug,
907
+ discipline: metadataString(m.metadata, "effortDiscipline"),
908
+ intensityTier: metadataString(m.metadata, "effortIntensityTier"),
909
+ grade: metadataString(m.metadata, "grade"),
910
+ timestamp: record.timestamp,
911
+ createdAt: record.timestamp
912
+ }];
913
+ }
914
+ const label = metrics.find((m) => m.type === MetricType.Label);
915
+ const labelName = label ? String(label.value ?? label.image ?? "") : "";
916
+ const facts = [];
917
+ metrics.forEach((m) => {
918
+ if (m.type === MetricType.Label || typeof m.value !== "number") return;
919
+ const metricKey = metadataString(m.metadata, "canonicalKey") ?? (labelName ? resolveCanonicalMetricKey(labelName) : m.type ?? "metric");
920
+ const ordinal = facts.length;
921
+ facts.push({
922
+ id: `${record.id}:${ordinal}`,
923
+ noteId: record.noteId,
924
+ blockContentId: record.blockContentId,
925
+ origin: record.origin,
926
+ pageId: record.pageId,
927
+ grain: "event",
928
+ segmentId: record.segmentId ?? "",
929
+ segmentVersion: record.segmentVersion ?? 0,
930
+ resultId: record.resultId,
931
+ type: metricKey,
932
+ value: m.value,
933
+ unit: m.unit,
934
+ label: labelName || metricKey,
935
+ metricKey,
936
+ metricLabel: labelName || metricKey,
937
+ metricUnit: m.unit,
938
+ effortSlug: metadataString(m.metadata, "effortSlug") ?? record.effortSlug,
939
+ intensityTier: metadataString(m.metadata, "effortIntensityTier"),
940
+ grade: metadataString(m.metadata, "grade"),
941
+ timestamp: record.timestamp,
942
+ createdAt: record.timestamp
943
+ });
944
+ });
945
+ return facts;
767
946
  }
768
947
 
769
948
  // src/dashboard/frontmatter.ts
@@ -955,23 +1134,27 @@ function windowAnchor(selected, parsed, options2) {
955
1134
  if (options2.anchor === "latest-activity" && parsed.last) return latestActivity(selected);
956
1135
  return void 0;
957
1136
  }
958
- var defaultFactStore = {
959
- getFactsByMetric: async () => [],
960
- getFactsByTimeRange: async () => [],
961
- getNoteTagLabels: async () => []
1137
+ var defaultEventStore = {
1138
+ getEventsByTimeRange: async () => [],
1139
+ getEventsByResult: async () => [],
1140
+ getEventsForNote: async () => [],
1141
+ getEventsByContent: async () => [],
1142
+ scanAll: async () => [],
1143
+ appendEvents: async () => {
1144
+ },
1145
+ finalizeSummaries: async () => {
1146
+ },
1147
+ deleteEvents: async () => {
1148
+ }
962
1149
  };
963
1150
  var defaultNoteStore = {
964
1151
  getAllNotes: async () => [],
965
- getNoteIdsForTag: async () => /* @__PURE__ */ new Set()
1152
+ getNoteIdsForTag: async () => /* @__PURE__ */ new Set(),
1153
+ getNoteTagLabels: async () => []
966
1154
  };
967
1155
  var defaultBlockStore = {
968
1156
  getAllBlocks: async () => []
969
1157
  };
970
- var defaultResultStore = {
971
- getResultsByContentId: async () => [],
972
- getResultById: async () => void 0,
973
- getResultsForNote: async () => []
974
- };
975
1158
  var defaultEffortStore = {
976
1159
  getAllEfforts: async () => []
977
1160
  };
@@ -1083,32 +1266,31 @@ var QueryService = class {
1083
1266
  store;
1084
1267
  noteStore;
1085
1268
  blockStore;
1086
- resultStore;
1087
1269
  effortStore;
1088
1270
  staticNoteStore;
1089
1271
  staticBlockStore;
1090
- constructor(storesOrFactStore, noteStore, blockStore, resultStore, effortStore, staticNoteStore, staticBlockStore) {
1091
- if (storesOrFactStore && typeof storesOrFactStore === "object" && ("factStore" in storesOrFactStore || "noteStore" in storesOrFactStore || "blockStore" in storesOrFactStore || "resultStore" in storesOrFactStore || "effortStore" in storesOrFactStore || "staticNoteStore" in storesOrFactStore || "staticBlockStore" in storesOrFactStore)) {
1092
- const stores = storesOrFactStore;
1093
- this.store = stores.factStore ?? defaultFactStore;
1272
+ constructor(storesOrEventStore, noteStore, blockStore, effortStore, staticNoteStore, staticBlockStore) {
1273
+ if (storesOrEventStore && typeof storesOrEventStore === "object" && ("eventStore" in storesOrEventStore || "noteStore" in storesOrEventStore || "blockStore" in storesOrEventStore || "effortStore" in storesOrEventStore || "staticNoteStore" in storesOrEventStore || "staticBlockStore" in storesOrEventStore)) {
1274
+ const stores = storesOrEventStore;
1275
+ this.store = stores.eventStore ?? defaultEventStore;
1094
1276
  this.noteStore = stores.noteStore ?? defaultNoteStore;
1095
1277
  this.blockStore = stores.blockStore ?? defaultBlockStore;
1096
- this.resultStore = stores.resultStore ?? defaultResultStore;
1097
1278
  this.effortStore = stores.effortStore ?? defaultEffortStore;
1098
1279
  this.staticNoteStore = stores.staticNoteStore;
1099
1280
  this.staticBlockStore = stores.staticBlockStore;
1100
1281
  } else {
1101
- this.store = storesOrFactStore ?? defaultFactStore;
1282
+ this.store = storesOrEventStore ?? defaultEventStore;
1102
1283
  this.noteStore = noteStore ?? defaultNoteStore;
1103
1284
  this.blockStore = blockStore ?? defaultBlockStore;
1104
- this.resultStore = resultStore ?? defaultResultStore;
1105
1285
  this.effortStore = effortStore ?? defaultEffortStore;
1106
1286
  this.staticNoteStore = staticNoteStore;
1107
1287
  this.staticBlockStore = staticBlockStore;
1108
1288
  }
1109
1289
  }
1290
+ /** Windowed flat-fact fetch — the projected view over the event store. */
1110
1291
  async getFactsByTimeRange(start, end) {
1111
- return this.store.getFactsByTimeRange(start, end);
1292
+ const rows = await this.store.getEventsByTimeRange(start, end);
1293
+ return rows.flatMap(projectEventToFacts);
1112
1294
  }
1113
1295
  async runQuery(raw, options2 = {}) {
1114
1296
  const parsed = parseQuery(raw);
@@ -1132,8 +1314,8 @@ var QueryService = class {
1132
1314
  }
1133
1315
  /**
1134
1316
  * Execute a rows query (rows:{…}, #949) — the session results table plane.
1135
- * Re-derives output statements from raw WorkoutResult logs through the
1136
- * ResultLogStore seam ("logs win" — facts are never read for this path).
1317
+ * Reads event rows directly over the unified store (ticket 003): no
1318
+ * WorkoutResult blob parsing; outputType narrowing hits the promoted column.
1137
1319
  */
1138
1320
  async runRows(parsed, options2 = {}) {
1139
1321
  const empty = { parsed, runs: [] };
@@ -1155,28 +1337,30 @@ var QueryService = class {
1155
1337
  if (resultIds.length + blockIds.length + noteIds.length === 0) {
1156
1338
  return { ...empty, error: "Rows query needs a scope: result:, block:, or note:." };
1157
1339
  }
1158
- const byId = /* @__PURE__ */ new Map();
1159
- for (const id of resultIds) {
1160
- const r = await this.resultStore.getResultById(id);
1161
- if (r) byId.set(r.id, r);
1162
- }
1163
- for (const blockContentId of blockIds) {
1164
- for (const r of await this.resultStore.getResultsByContentId(blockContentId)) byId.set(r.id, r);
1165
- }
1166
- for (const noteId of noteIds) {
1167
- for (const r of await this.resultStore.getResultsForNote(noteId)) byId.set(r.id, r);
1168
- }
1169
- let results = [...byId.values()].filter((r) => r.data?.logs?.length);
1340
+ const byResult = /* @__PURE__ */ new Map();
1341
+ const collect = (rows) => {
1342
+ for (const row of rows) {
1343
+ const bucket = byResult.get(row.resultId);
1344
+ if (bucket) bucket.push(row);
1345
+ else byResult.set(row.resultId, [row]);
1346
+ }
1347
+ };
1348
+ for (const id of resultIds) collect(await this.store.getEventsByResult(id));
1349
+ for (const blockContentId of blockIds) collect(await this.store.getEventsByContent(blockContentId));
1350
+ for (const noteId of noteIds) collect(await this.store.getEventsForNote(noteId));
1351
+ let groups = [...byResult.entries()].filter(([, rows]) => rows.length > 0);
1170
1352
  if (parsed.last) {
1171
- results = results.filter(
1172
- (r) => effectiveTimeWindow(r.data.endTime ?? r.createdAt ?? 0, parsed.last, void 0, options2.anchorNow)
1353
+ groups = groups.filter(
1354
+ ([, rows]) => effectiveTimeWindow(rows[0].timestamp, parsed.last, void 0, options2.anchorNow)
1173
1355
  );
1174
1356
  }
1175
- results.sort((a, b) => (b.data.endTime ?? b.createdAt) - (a.data.endTime ?? a.createdAt));
1176
- const runs = results.map((result) => ({
1177
- result,
1178
- logs: parsed.outputType ? result.data.logs.filter((l) => l.outputType === parsed.outputType) : result.data.logs
1179
- })).filter((run) => run.logs.length > 0);
1357
+ groups.sort((a, b) => b[1][0].timestamp - a[1][0].timestamp);
1358
+ const runs = groups.map(([resultId, rows]) => ({
1359
+ resultId,
1360
+ noteId: rows[0].noteId,
1361
+ timestamp: rows[0].timestamp,
1362
+ events: parsed.outputType ? rows.filter((row) => row.outputType === parsed.outputType) : rows
1363
+ })).filter((run) => run.events.length > 0);
1180
1364
  return { parsed, runs };
1181
1365
  }
1182
1366
  /**
@@ -1365,14 +1549,11 @@ var QueryService = class {
1365
1549
  };
1366
1550
  if (parsed.error) return empty;
1367
1551
  if (parsed.join) return this.runJoined(parsed, options2);
1368
- const byMetric = await this.store.getFactsByMetric(parsed.metric);
1369
- let candidates = byMetric;
1370
- if (options2.rangeStart !== void 0 || options2.rangeEnd !== void 0) {
1371
- const start = options2.rangeStart ?? 0;
1372
- const end = options2.rangeEnd ?? Number.MAX_SAFE_INTEGER;
1373
- const inRange = new Set((await this.store.getFactsByTimeRange(start, end)).map((row) => row.id));
1374
- candidates = candidates.filter((row) => inRange.has(row.id));
1375
- }
1552
+ const windowed = options2.rangeStart !== void 0 || options2.rangeEnd !== void 0;
1553
+ const start = options2.rangeStart ?? 0;
1554
+ const end = options2.rangeEnd ?? Number.MAX_SAFE_INTEGER;
1555
+ const eventRows = windowed ? await this.store.getEventsByTimeRange(start, end) : await this.store.scanAll();
1556
+ const candidates = eventRows.flatMap(projectEventToFacts).filter((row) => row.metricKey === parsed.metric);
1376
1557
  const touchesTags = parsed.filters.some((f) => f.key === "tags") || parsed.groupBy.includes("tags");
1377
1558
  const noteTags = await this.loadNoteTags(candidates, touchesTags);
1378
1559
  const matched = this.applyEffortScope(
@@ -1516,29 +1697,14 @@ var QueryService = class {
1516
1697
  }
1517
1698
  return matched.some((r) => r.effortSlug === void 0) ? matched.filter((r) => r.effortSlug === void 0) : matched;
1518
1699
  }
1519
- /** Re-derive summary facts for one Canonical Metric Key from RAW WorkoutResult
1520
- * logs across the given content ids — the cross-store join source. One fact
1521
- * row per result (normalizeSummaryFacts dedupes within a result). */
1700
+ /** Summary facts for one Canonical Metric Key across the given content ids —
1701
+ * the cross-store join source. Reads finalize-written summary rows straight
1702
+ * off the content index (ticket 003: the store is authoritative; the old
1703
+ * freshness re-derivation is moot). One fact row per result × rowKey. */
1522
1704
  async deriveMetricFacts(contentIds, metricKey) {
1523
- const out = [];
1524
1705
  const ids = [...new Set(contentIds)];
1525
- await Promise.all(ids.map(async (blockContentId) => {
1526
- const results = await this.resultStore.getResultsByContentId(blockContentId);
1527
- for (const result of results) {
1528
- const facts = normalizeSummaryFacts(result.data.logs ?? [], {
1529
- noteId: result.noteId,
1530
- resultId: result.id,
1531
- segmentId: result.segmentId,
1532
- segmentVersion: result.segmentVersion,
1533
- blockContentId,
1534
- origin: result.origin,
1535
- pageId: result.pageId,
1536
- workoutTimestamp: result.createdAt
1537
- });
1538
- for (const f of facts) if (f.metricKey === metricKey) out.push(f);
1539
- }
1540
- }));
1541
- return out;
1706
+ const rows = await Promise.all(ids.map((blockContentId) => this.store.getEventsByContent(blockContentId)));
1707
+ return rows.flat().filter((row) => row.grain === "summary").flatMap(projectEventToFacts).filter((f) => f.metricKey === metricKey);
1542
1708
  }
1543
1709
  /** All content blocks across the journal + static corpus. */
1544
1710
  async allContentBlocks() {
@@ -1578,7 +1744,7 @@ var QueryService = class {
1578
1744
  const noteTags = /* @__PURE__ */ new Map();
1579
1745
  if (!touchesTags) return noteTags;
1580
1746
  const noteIds = [...new Set(rows.map((r) => r.noteId))];
1581
- await Promise.all(noteIds.map(async (id) => noteTags.set(id, await this.store.getNoteTagLabels(id))));
1747
+ await Promise.all(noteIds.map(async (id) => noteTags.set(id, await this.noteStore.getNoteTagLabels(id))));
1582
1748
  return noteTags;
1583
1749
  }
1584
1750
  };
@@ -1878,15 +2044,15 @@ sum:$metric{} rollup:1w
1878
2044
  }
1879
2045
 
1880
2046
  // src/version.ts
1881
- var VERSION = "0.6.32";
1882
- var GIT_SHA = "bc13c0f";
1883
- var BUILD_TIME = "2026-08-21T20:12:50.453Z";
2047
+ var VERSION = "0.7.37";
2048
+ var GIT_SHA = "713c593";
2049
+ var BUILD_TIME = "2026-08-25T00:18:25.714Z";
1884
2050
  var SEMVER = {
1885
- version: "0.6.32",
1886
- gitSha: "bc13c0f",
1887
- buildTime: "2026-08-21T20:12:50.453Z"
2051
+ version: "0.7.37",
2052
+ gitSha: "713c593",
2053
+ buildTime: "2026-08-25T00:18:25.714Z"
1888
2054
  };
1889
2055
 
1890
- 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, isDashboardMeta, isDashboardWidgetType, isEffortDiscipline, isFindQuery, isProposedMetric, isRowsQuery, normalizeSummaryFacts, parseDashboardNote, parseFlatProperties, parseFrontmatter, parseFrontmatterBody, parseFrontmatterCategories, parseFrontmatterProps, parseQuery, parseQueryWidgetSuffix, parseWqlSuffixes, referencedTokens, resolveCanonicalMetricKey, resolveDisplayUnit, resolveWidgetType, serializeFrontmatter, setDashboardTokenValue, splitAtWhere, splitWidgetBody, staticNotesFromBlocks, staticTagIndexFromBlocks, stripFrontmatter, substituteTokens, unknownTokensMessage, unknownWidgetTypeMessage, wql, wqlCompletion, wqlCompletionSource, wqlHighlightStyle, wqlLanguage, parser as wqlParser, wql_parser_terms_exports as wqlTerms };
2056
+ 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, 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 };
1891
2057
  //# sourceMappingURL=index.mjs.map
1892
2058
  //# sourceMappingURL=index.mjs.map