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