@bitcobblers/wod-wiki-wql 0.11.43 → 0.35.2116

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.d.cts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { LRParser } from '@lezer/lr';
2
2
  import { LanguageSupport, HighlightStyle, LRLanguage } from '@codemirror/language';
3
- import { CompletionContext, CompletionResult } from '@codemirror/autocomplete';
3
+ import { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete';
4
4
  import { Extension } from '@codemirror/state';
5
- import { ResultOrigin, AnalyticsDataPoint, UnifiedEventRecord, StoredOutputStatement, BlockIndexRow, Note } from '@bitcobblers/wod-wiki-core';
5
+ import { SummaryCoverage, BlockIndexRow, Note, EventRecord, PageNote, Page, AnalyticsDataPoint, ResultOrigin, StoredOutputStatement } from '@bitcobblers/wod-wiki-core';
6
+ export { SYSTEM_DEFAULT_OUTPUT_UNIT, UnitConversionError, convertViaCatalog, dimensionKeyOf, lookupUnit, systemDefaultUnit } from '@bitcobblers/wod-wiki-core';
6
7
 
7
8
  /**
8
9
  * WQL vocabulary — the single owner of the WQL dictionary: aggregators,
@@ -28,9 +29,20 @@ type WqlAggregator = (typeof WQL_AGGREGATORS)[number];
28
29
  /** Comparison operators for cross-store `where` joins (#800). */
29
30
  declare const WQL_COMPARISON_OPS: readonly [">", ">=", "<", "<=", "==", "!="];
30
31
  type WqlComparisonOp = (typeof WQL_COMPARISON_OPS)[number];
32
+ /** Frontmatter typed-tag keys (CONTEXT.md § Tags) — values are dynamic,
33
+ * backed by the host's tags store (`by-type` index), not a static enum. */
34
+ declare const WQL_TYPED_TAG_KEYS: readonly ["domain", "format", "equipment", "quality", "intent"];
35
+ type WqlTypedTagKey = (typeof WQL_TYPED_TAG_KEYS)[number];
31
36
  /** Tag keys the Query Service reads off a fact row (QueryService.factTagValue). */
32
- declare const WQL_TAG_KEYS: readonly ["effort", "discipline", "intensity", "note", "page", "origin", "grain", "metric", "block", "result", "tags"];
37
+ declare const WQL_TAG_KEYS: readonly ["domain", "format", "equipment", "quality", "intent", "effort", "discipline", "intensity", "grade", "note", "page", "origin", "grain", "metric", "block", "result", "tags"];
33
38
  type WqlTagKey = (typeof WQL_TAG_KEYS)[number];
39
+ /** Content-plane keys — meaningful only on find:/rows:. On an aggregate
40
+ * they are a category error (a fact row has no note text or source);
41
+ * every other non-structural key is a candidate custom dimension. */
42
+ declare const WQL_CONTENT_ONLY_KEYS: readonly ["type", "text", "has", "source", "catalog"];
43
+ /** Filter keys the effort registry can resolve (runFindEffort) — anything
44
+ * else on find:effort is ignored and reported as an advisory. */
45
+ declare const WQL_EFFORT_FILTER_KEYS: readonly ["effort", "discipline", "intensity", "origin", "text"];
34
46
  /** Virtual dimensions — time buckets and stream positions, not fact fields. */
35
47
  declare const WQL_VIRTUAL_DIMS: readonly ["day", "week", "session", "round"];
36
48
  type WqlVirtualDim = (typeof WQL_VIRTUAL_DIMS)[number];
@@ -56,11 +68,42 @@ type WqlIntensityTier = (typeof WQL_INTENSITY_TIERS)[number];
56
68
  declare const WQL_GRAINS: readonly ["summary", "event"];
57
69
  type WqlGrain = (typeof WQL_GRAINS)[number];
58
70
  /** Content-discovery query targets (find:<target>). */
59
- declare const WQL_FIND_TARGETS: readonly ["note", "block", "effort"];
71
+ declare const WQL_FIND_TARGETS: readonly ["note", "block", "effort", "session", "segment", "event"];
60
72
  type WqlFindTarget = (typeof WQL_FIND_TARGETS)[number];
61
- /** Canonical source filter values (C2): scope folded into the source: filter key. */
62
- declare const WQL_SOURCE_VALUES: readonly ["journal", "collections", "feeds", "all"];
73
+ /** Canonical source filter values: the allowed storage locations (the
74
+ * former `collections` spelling is retired — renamed to feeds; storage
75
+ * identities keep their `collection:` prefixes). Seed corpus locations
76
+ * joined the note plane: prebuilt dashboards and the effort library. */
77
+ declare const WQL_SOURCE_VALUES: readonly ["journal", "feeds", "guides", "playground", "dashboards", "efforts"];
63
78
  type WqlSourceValue = (typeof WQL_SOURCE_VALUES)[number];
79
+ /** Colon source heads — the unprefixed `:<head>` names. Find targets map
80
+ * 1:1 to the content planes; journal/feed(s)/catalog(s)/playground are
81
+ * source-scoped note heads (they parse to target 'note' plus an injected
82
+ * `source:` filter). */
83
+ declare const WQL_SOURCE_HEADS: readonly ["note", "block", "effort", "session", "segment", "event", "journal", "feed", "feeds", "catalog", "catalogs", "playground", "dashboard"];
84
+ type WqlSourceHead = (typeof WQL_SOURCE_HEADS)[number];
85
+ /** Storage scope a source-scoped note head binds (undefined = plain target). */
86
+ declare const WQL_SOURCE_HEAD_SCOPES: Readonly<Record<string, WqlSourceValue>>;
87
+ /** Type-alias note heads — `:<head>` parses to target 'note' plus an injected
88
+ * `type:` filter (no storage scope, so the head never narrows by source).
89
+ * `:dashboard` is the canonical member: dashboards are notes typed
90
+ * 'dashboard', findable with ordinary note tags/text/frontmatter filters. */
91
+ declare const WQL_TYPE_HEADS: Readonly<Record<string, string>>;
92
+ /** Former generic `:note` default scope — now EMPTY: the generic note head
93
+ * is the inclusive note plane (every row). Kept exported (empty) so the
94
+ * serializer and host surfaces keep a single name for "the default scope",
95
+ * which no longer narrows anything. */
96
+ declare const WQL_NOTE_DEFAULT_SOURCES: readonly WqlSourceValue[];
97
+ /** Colon function heads — the unprefixed `:<fn>{metric:…}` aggregate stage. */
98
+ declare const WQL_FUNCTION_HEADS: readonly ["sum", "avg", "min", "max", "count", "last", "delta"];
99
+ type WqlFunctionHead = WqlAggregator;
100
+ /** Colon chart heads — terminal pipeline sinks (`:timeseries`, `:bar`, …). */
101
+ declare const WQL_CHART_HEADS: readonly ["timeseries", "bar", "table", "donut", "toplist", "value"];
102
+ type WqlChartHead = (typeof WQL_CHART_HEADS)[number];
103
+ /** Standard datasets the host registers per page/run before children
104
+ * evaluate (`@session` active session, `@today` civil-day telemetry+notes). */
105
+ declare const WQL_STANDARD_DATASETS: readonly ["@session", "@today"];
106
+ type WqlStandardDataset = (typeof WQL_STANDARD_DATASETS)[number];
64
107
  /** Result planes — `rows:<plane>` narrows raw output-statement rows by the
65
108
  * unified store's promoted `outputType` column. The STORE vocabulary is
66
109
  * open (core KNOWN_OUTPUT_TYPES, ticket 002: unknowns are stored and
@@ -69,17 +112,8 @@ type WqlSourceValue = (typeof WQL_SOURCE_VALUES)[number];
69
112
  * stay addressable programmatically via hand-built ASTs. */
70
113
  declare const WQL_RESULT_PLANES: readonly ["segment", "system", "load", "event", "compiler", "completion", "analytics", "wellness"];
71
114
  type WqlResultPlane = (typeof WQL_RESULT_PLANES)[number];
72
- /** All `rows:` targets — content planes plus result planes (rows model
73
- * §1.0: every query selects rows from a target) plus `all`, the explicit
74
- * no-narrowing pseudo-target (spec v2 decision 1: the bare `rows:{…}`
75
- * alias retires). Content-plane targets scope by content, not outputType
76
- * (C4). */
77
- declare const WQL_ROWS_TARGETS: readonly ["note", "block", "effort", "segment", "system", "load", "event", "compiler", "completion", "analytics", "wellness", "all"];
78
- type WqlRowsTarget = (typeof WQL_ROWS_TARGETS)[number];
79
- /** Rows scope filter keys (C4): the exact-match keys a rows query filters by. */
80
- declare const WQL_ROWS_SCOPE_KEYS: readonly ["result", "block", "note"];
81
115
  /** Content-specific filter keys (beyond the analytics tag keys). */
82
- declare const WQL_CONTENT_FILTER_KEYS: readonly ["type", "text", "has", "source", "catalog", "effort", "discipline", "intensity", "note", "page", "origin", "grain", "metric", "block", "result", "tags"];
116
+ declare const WQL_CONTENT_FILTER_KEYS: readonly ["type", "text", "has", "source", "catalog", "domain", "format", "equipment", "quality", "intent", "effort", "discipline", "intensity", "grade", "note", "page", "origin", "grain", "metric", "block", "result", "tags"];
83
117
  type WqlContentFilterKey = (typeof WQL_CONTENT_FILTER_KEYS)[number];
84
118
  /**
85
119
  * Composer source planes (issue #838, decision #836): the single `source`
@@ -90,10 +124,10 @@ type WqlContentFilterKey = (typeof WQL_CONTENT_FILTER_KEYS)[number];
90
124
  * deliberately absent — it needs new grammar/engine semantics and is
91
125
  * deferred to its own ticket.
92
126
  */
93
- declare const WQL_SOURCES: readonly ["journal", "collections", "feeds", "notes", "blocks", "efforts", "metrics"];
127
+ declare const WQL_SOURCES: readonly ["note", "block", "effort", "session", "segment", "event"];
94
128
  type WqlSource = (typeof WQL_SOURCES)[number];
95
- /** Rollup periods the aggregate grammar accepts (wql.ts: unit d|w only). */
96
- declare const WQL_ROLLUP_PERIODS: readonly ["1d", "1w"];
129
+ /** Rollup periods the aggregate grammar accepts (wql.ts: N > 1, unit d|w). */
130
+ declare const WQL_ROLLUP_PERIODS: readonly ["2w", "4w"];
97
131
  type WqlRollupPeriod = (typeof WQL_ROLLUP_PERIODS)[number];
98
132
  /** Display units the app can render (analytics unit preference, kg/lb). The
99
133
  * `in <unit>` directive accepts any word; these are the canonical options. */
@@ -103,29 +137,54 @@ type WqlDisplayUnit = (typeof WQL_DISPLAY_UNITS)[number];
103
137
  declare const WQL_KEYWORDS: readonly ["find", "rows", "where", "group", "by", "order", "limit", "sum", "avg", "min", "max", "count", "last", "delta"];
104
138
  type WqlKeyword = (typeof WQL_KEYWORDS)[number];
105
139
 
140
+ /**
141
+ * Grouping dimensions the content consumer (stream entryGrouping) applies on
142
+ * top of find results: the note/block/effort/session executors return flat
143
+ * results and the view groups them, ordered and multi-dim. The find tables
144
+ * (segment/event) group inside the executor over selected columns instead.
145
+ */
146
+ declare const CONTENT_GROUPING_DIMENSIONS: readonly ["date", "day", "week", "month", "year", "discipline", "origin", "source", "kind", "type", "tag"];
147
+ /** Guided clause types, including structural clauses honored by their consumers. */
148
+ declare function allowedFilterTypesForTarget(target: string, family: 'find' | 'aggregate'): ReadonlySet<string>;
149
+ /** Suggested filter keys. Fact-backed queries also accept custom dimension keys. */
150
+ declare function wqlFilterKeys(target: string, family: 'find' | 'aggregate'): readonly string[];
151
+ /** Content grouping is applied by the stream consumer; table grouping reads
152
+ * selected columns. Effort/session have no executor grouping but their views
153
+ * group content (the effort view profile groups by discipline). */
154
+ declare function wqlGroupingDimensions(target: string, family: 'find' | 'aggregate'): readonly string[];
155
+ /** Fact dimensions are open, but content-only fields and plane have no fact meaning. */
156
+ declare function supportsWqlFilterKey(target: string, family: 'find' | 'aggregate', rawKey: string): boolean;
157
+
106
158
  declare const parser: LRParser;
107
159
 
108
160
  declare const Query = 1;
109
- declare const Head = 2;
110
- declare const Aggregator$1 = 3;
111
- declare const Word = 4;
112
- declare const Metric = 5;
113
- declare const Filters = 6;
114
- declare const Filter = 7;
115
- declare const Negate = 8;
116
- declare const TagKey = 9;
117
- declare const TagValue$1 = 10;
118
- declare const Value = 11;
119
- declare const Star = 12;
120
- declare const Quoted = 13;
121
- declare const GroupBy = 14;
122
- declare const By = 15;
123
- declare const Dimension = 16;
124
- declare const Rollup = 17;
125
- declare const RollupDot = 18;
126
- declare const Int = 19;
161
+ declare const Pipeline = 2;
162
+ declare const Stage = 3;
163
+ declare const Head = 4;
164
+ declare const Word = 5;
165
+ declare const Aggregator$1 = 6;
166
+ declare const Metric = 7;
167
+ declare const Filters = 8;
168
+ declare const Filter = 9;
169
+ declare const Negate = 10;
170
+ declare const TagKey = 11;
171
+ declare const TagValue$1 = 12;
172
+ declare const Value = 13;
173
+ declare const Star = 14;
174
+ declare const Quoted = 15;
175
+ declare const DatasetReference = 16;
176
+ declare const At = 17;
177
+ declare const Single = 18;
178
+ declare const GroupBy = 19;
179
+ declare const By = 20;
180
+ declare const Dimension = 21;
181
+ declare const Rollup = 22;
182
+ declare const RollupDot = 23;
183
+ declare const Int = 24;
127
184
 
185
+ declare const wql_parser_terms_At: typeof At;
128
186
  declare const wql_parser_terms_By: typeof By;
187
+ declare const wql_parser_terms_DatasetReference: typeof DatasetReference;
129
188
  declare const wql_parser_terms_Dimension: typeof Dimension;
130
189
  declare const wql_parser_terms_Filter: typeof Filter;
131
190
  declare const wql_parser_terms_Filters: typeof Filters;
@@ -134,16 +193,19 @@ declare const wql_parser_terms_Head: typeof Head;
134
193
  declare const wql_parser_terms_Int: typeof Int;
135
194
  declare const wql_parser_terms_Metric: typeof Metric;
136
195
  declare const wql_parser_terms_Negate: typeof Negate;
196
+ declare const wql_parser_terms_Pipeline: typeof Pipeline;
137
197
  declare const wql_parser_terms_Query: typeof Query;
138
198
  declare const wql_parser_terms_Quoted: typeof Quoted;
139
199
  declare const wql_parser_terms_Rollup: typeof Rollup;
140
200
  declare const wql_parser_terms_RollupDot: typeof RollupDot;
201
+ declare const wql_parser_terms_Single: typeof Single;
202
+ declare const wql_parser_terms_Stage: typeof Stage;
141
203
  declare const wql_parser_terms_Star: typeof Star;
142
204
  declare const wql_parser_terms_TagKey: typeof TagKey;
143
205
  declare const wql_parser_terms_Value: typeof Value;
144
206
  declare const wql_parser_terms_Word: typeof Word;
145
207
  declare namespace wql_parser_terms {
146
- export { Aggregator$1 as Aggregator, wql_parser_terms_By as By, wql_parser_terms_Dimension as Dimension, wql_parser_terms_Filter as Filter, wql_parser_terms_Filters as Filters, wql_parser_terms_GroupBy as GroupBy, wql_parser_terms_Head as Head, wql_parser_terms_Int as Int, wql_parser_terms_Metric as Metric, wql_parser_terms_Negate as Negate, wql_parser_terms_Query as Query, wql_parser_terms_Quoted as Quoted, wql_parser_terms_Rollup as Rollup, wql_parser_terms_RollupDot as RollupDot, wql_parser_terms_Star as Star, wql_parser_terms_TagKey as TagKey, TagValue$1 as TagValue, wql_parser_terms_Value as Value, wql_parser_terms_Word as Word };
208
+ export { Aggregator$1 as Aggregator, wql_parser_terms_At as At, wql_parser_terms_By as By, wql_parser_terms_DatasetReference as DatasetReference, wql_parser_terms_Dimension as Dimension, wql_parser_terms_Filter as Filter, wql_parser_terms_Filters as Filters, wql_parser_terms_GroupBy as GroupBy, wql_parser_terms_Head as Head, wql_parser_terms_Int as Int, wql_parser_terms_Metric as Metric, wql_parser_terms_Negate as Negate, wql_parser_terms_Pipeline as Pipeline, wql_parser_terms_Query as Query, wql_parser_terms_Quoted as Quoted, wql_parser_terms_Rollup as Rollup, wql_parser_terms_RollupDot as RollupDot, wql_parser_terms_Single as Single, wql_parser_terms_Stage as Stage, wql_parser_terms_Star as Star, wql_parser_terms_TagKey as TagKey, TagValue$1 as TagValue, wql_parser_terms_Value as Value, wql_parser_terms_Word as Word };
147
209
  }
148
210
 
149
211
  /**
@@ -180,9 +242,13 @@ type ParsedWqlWindowSuffix = {
180
242
  interface ParsedWqlSuffixes {
181
243
  /** Outer cross-store join clause text (`where ...`). */
182
244
  where?: string;
245
+ /** Stored source binding target (`=> @sourceName`). */
246
+ storedAs?: string;
247
+ /** Source dataset reference for downstream query (`@source | ...` or `in @source`). */
248
+ sourceRef?: string;
183
249
  /** Display unit directive (`in kg`). */
184
250
  displayUnit?: string;
185
- /** Rollup period (`.rollup(1w)`). */
251
+ /** Rollup period (`.rollup(2w)` — 1-unit forms are rejected in parseQuery). */
186
252
  rollup?: ParsedWqlRollupSuffix;
187
253
  /** Group-by dimensions (`by {week, effort}`). */
188
254
  groupBy?: string[];
@@ -205,7 +271,7 @@ declare function splitAtWhere(text: string): {
205
271
  };
206
272
  /**
207
273
  * Extract suffixes from a raw WQL query string.
208
- * Strips outer join (`where`), display unit (`in kg`), rollup (`.rollup(1w)`),
274
+ * Strips outer join (`where`), display unit (`in kg`), rollup (`.rollup(Nw)`),
209
275
  * group-by (`by {week}`), time window (`last 8w` / `from … [to …]`), and
210
276
  * legacy scope (`in journal`).
211
277
  */
@@ -216,7 +282,7 @@ declare function parseWqlSuffixes(raw: string): ParsedWqlSuffixes;
216
282
  *
217
283
  * <aggregator>:<metric.namespace>{<tag filters>} by {<dimensions>} .rollup(<period>)
218
284
  *
219
- * sum:totalVolume{discipline:strength} by {week}.rollup(1w)
285
+ * sum:totalVolume{discipline:strength} by {week}
220
286
  * avg:tis{effort:thruster,!discipline:recovery} by {session}
221
287
  *
222
288
  * This module holds the AST contract the Query Service executes and the
@@ -252,7 +318,7 @@ interface MetricPredicate {
252
318
  }
253
319
  /**
254
320
  * Content half of a cross-store join — the find predicate attached to an
255
- * analytics query via `where`. Example: `find:note{tags:competition} in journal`.
321
+ * analytics query via `where`. Example: `find:note{tags:competition,source:journal}`.
256
322
  * Restricts the metric computation to the blockContentIds owned by matching
257
323
  * content; the metric is recomputed from raw logs for those blocks only.
258
324
  */
@@ -291,17 +357,50 @@ interface ParsedAggregateQuery {
291
357
  advisories?: string[];
292
358
  error?: string;
293
359
  }
294
- /** Result of parsing a content-discovery query (`find:target{filters} in scope`). */
360
+ /**
361
+ * Ticket 18 — drill-down: construct a cross-workout `rows:segment` query
362
+ * inheriting an aggregate point's tag/metadata filters, the clicked bucket's
363
+ * EXACT half-open civil boundaries (structural, not display timestamps), and
364
+ * the clicked group tuple as exact filters.
365
+ */
366
+ declare function buildDrillDownQuery(options: {
367
+ filters?: Array<{
368
+ key: string;
369
+ values: readonly string[];
370
+ }>;
371
+ /** Half-open bucket bounds (ms epoch) from the clicked point's bucket. */
372
+ start?: number;
373
+ end?: number;
374
+ /** Civil dates (YYYY-MM-DD) — used when the point is a calendar bucket. */
375
+ startIso?: string;
376
+ endIso?: string;
377
+ timeZone?: string;
378
+ limit?: number;
379
+ }): string;
380
+ /** Result of parsing a content-discovery query — the colon source heads
381
+ * (`:note{…}`, `:journal{…}`); the legacy `find:` spelling is retired and
382
+ * survives only inside `where` join halves. */
295
383
  interface ParsedFindQuery {
296
384
  family: 'find';
297
385
  raw: string;
298
386
  /** Content target — a WQL_FIND_TARGETS value (C7 closed enum). */
299
387
  target: string;
300
388
  filters: TagFilter[];
389
+ /** Implicit default source scope for the generic `:note` head —
390
+ * journal | feeds | playground. NOT a filter: authored filters stay
391
+ * untouched (composer pills never see it); the executor ANDs it in only
392
+ * when no explicit `source:` filter was written. */
393
+ sourceScope?: string[];
301
394
  /** Time-selection window (C1): `last 8w` or `from … [to …]`. */
302
395
  window?: QueryWindow;
303
396
  /** Cross-store metric join (`where sum:totalVolume{} > 5000`). */
304
397
  join?: MetricPredicate;
398
+ /** Presentation pipes (`| select … | order by … | limit …`). */
399
+ pipes?: RowsPipes;
400
+ /** Group-by dimensions (`by {effort}`). */
401
+ groupBy?: string[];
402
+ /** Optional display unit directive (`in kg` / `in lb`). */
403
+ displayUnit?: string;
305
404
  /** Deprecation advisories (C2 normalizer). */
306
405
  advisories?: string[];
307
406
  error?: string;
@@ -321,16 +420,53 @@ type QueryWindow = {
321
420
  start: string;
322
421
  end?: string;
323
422
  };
324
- type AnyParsedQuery = ParsedAggregateQuery | ParsedFindQuery | ParsedRowsQuery;
423
+ type AnyParsedQuery = ParsedAggregateQuery | ParsedFindQuery | ParsedPipelineQuery;
325
424
  /** Type guard: true for content-discovery queries. */
326
425
  declare function isFindQuery(parsed: AnyParsedQuery): parsed is ParsedFindQuery;
327
- /** Type guard: true for rows queries. */
328
- declare function isRowsQuery(parsed: AnyParsedQuery): parsed is ParsedRowsQuery;
329
426
  /** Type guard: true for analytics (aggregate) queries. */
330
427
  declare function isAggregateQuery(parsed: AnyParsedQuery): parsed is ParsedAggregateQuery;
428
+ /** Type guard: true for left-to-right value pipelines (`:src | :fn | :chart`). */
429
+ declare function isPipelineQuery(parsed: AnyParsedQuery): parsed is ParsedPipelineQuery;
430
+ /** A pipeline's input: a full single query, or a named dataset
431
+ * (`@session` / `@today` / a host-registered page source — name keeps `@`). */
432
+ type PipelineSource = {
433
+ kind: 'query';
434
+ query: ParsedFindQuery | ParsedAggregateQuery;
435
+ } | {
436
+ kind: 'dataset';
437
+ name: string;
438
+ };
439
+ /** Terminal chart stage of a pipeline: the chart head plus its authored
440
+ * parameter filters (`:bar{type:session}` → `{head:'bar', filters:[…]}`). */
441
+ interface PipelineSink {
442
+ head: WqlChartHead;
443
+ filters: TagFilter[];
444
+ }
445
+ /**
446
+ * Left-to-right value pipeline (`:journal{…} last 8w | :sum{metric:tis} | :timeseries{}`).
447
+ * Transforms apply in order to the previous stage's output — the executor
448
+ * reduces in memory, it never rescans stores. A dataset alone (`@today`) is
449
+ * a pipeline with no transforms; a lone chart head starts from an implicit
450
+ * `:segment` source.
451
+ */
452
+ interface ParsedPipelineQuery {
453
+ family: 'pipeline';
454
+ raw: string;
455
+ source: PipelineSource;
456
+ /** Ordered aggregate stages, each applied to the previous stage's rows. */
457
+ transforms: ParsedAggregateQuery[];
458
+ sink?: PipelineSink;
459
+ /** Parse-time disclosures (ignored chart suffixes, …). */
460
+ advisories?: string[];
461
+ error?: string;
462
+ }
331
463
  interface SeriesPoint {
332
464
  ts: number;
333
465
  value: number;
466
+ /** True for a structurally present bucket with no recorded observations
467
+ * (ticket 12 calendar domain generation) — display zero-fill, never an
468
+ * observation. */
469
+ missing?: boolean;
334
470
  }
335
471
  /**
336
472
  * Result of parsing a rows query (`rows:{filters}` / `rows:segment{filters}`) —
@@ -338,12 +474,31 @@ interface SeriesPoint {
338
474
  * output-statement rows re-derived from WorkoutResult logs, scoped by
339
475
  * `result:` / `block:` / `note:`. Never aggregates — no by/rollup/where.
340
476
  */
477
+ /** Ticket 18 — pipe clauses: presentation only, never alter the match. */
478
+ interface RowsPipes {
479
+ error?: string;
480
+ select?: Array<{
481
+ col: string;
482
+ unit?: string;
483
+ }>;
484
+ order?: Array<{
485
+ col: string;
486
+ dir: 'asc' | 'desc';
487
+ }>;
488
+ limit?: number;
489
+ offset?: number;
490
+ }
341
491
  interface ParsedRowsQuery {
492
+ /** Ticket 18 — pipe clauses (| select / | order by / | limit). */
493
+ pipes?: RowsPipes;
342
494
  raw: string;
343
495
  /** Family discriminator shared by all three query ASTs (C5). */
344
496
  family: 'rows';
345
497
  /** Output-statement type narrowing from the optional target (`rows:segment{…}`); undefined = all types. */
346
498
  outputType?: string;
499
+ /** The rows target itself — 'all' | 'segment' | content plane (ticket 18:
500
+ * segment without scope = cross-workout form). */
501
+ target?: string;
347
502
  filters: TagFilter[];
348
503
  /** Time-selection window (C1): `last 4w` or `from … [to …]` over the
349
504
  * workout end time. */
@@ -352,11 +507,14 @@ interface ParsedRowsQuery {
352
507
  advisories?: string[];
353
508
  error?: string;
354
509
  }
510
+ /** One aggregated series. `error` carries the diagnostic when this series'
511
+ * calculation failed (ticket 13) — an errored series has no valid values. */
355
512
  interface Series {
356
513
  key: string;
357
514
  label: string;
358
515
  points: SeriesPoint[];
359
516
  unit?: string;
517
+ error?: string;
360
518
  }
361
519
  /**
362
520
  * Parse a WQL query string into one of the three query families —
@@ -364,10 +522,17 @@ interface Series {
364
522
  * (C5). Dispatch is textual: a leading `find:` routes to the content path,
365
523
  * `rows` to the rows path, everything else to analytics.
366
524
  */
525
+ declare function rowsParseError(raw: string): string;
526
+ /**
527
+ * Parse a WQL query string into one of the query families — analytics
528
+ * aggregate, content find, or pipeline — discriminated by `family` (C5).
529
+ * Dispatch: `rows` is a retired-error; `find:` is retired; a pipe-separated
530
+ * stage list (or a lone `@dataset`/chart head) is a pipeline; a colon
531
+ * source/function head or legacy `agg:metric` head parses as a single query.
532
+ */
367
533
  declare function parseQuery(raw: string): AnyParsedQuery;
368
534
  /**
369
535
  * C2 Compatibility normalizer: rewrites legacy query syntax into modern WQL.
370
- * - Bare `rows:{…}` heads rewrite to `rows:all{…}`
371
536
  * - Legacy trailing `in <scope>` rewrites into `{source:<scope>}`
372
537
  * Returns the normalized query string and any deprecation advisories.
373
538
  */
@@ -375,6 +540,14 @@ declare function normalizeWql(raw: string): {
375
540
  query: string;
376
541
  advisories: string[];
377
542
  };
543
+ /**
544
+ * Target-capability advisories for a find AST: every clause the executor will
545
+ * silently drop, apply differently than written, or group outside the
546
+ * executor. A pure function of the parsed shape — `parseFindQuery` attaches
547
+ * these at parse time and `QueryService.runFind` re-derives them, so
548
+ * hand-built ASTs get the same loud disclosure without re-serializing text.
549
+ */
550
+ declare function findTargetAdvisories(query: ParsedFindQuery): string[];
378
551
 
379
552
  /**
380
553
  * WQL serializer (C6 — one structured interface for code).
@@ -393,7 +566,10 @@ declare function normalizeWql(raw: string): {
393
566
  * to parse has no well-defined structure to re-emit, and echoing the input
394
567
  * keeps the function total.
395
568
  *
396
- * Round-trip equality holds for text-surface-representable ASTs. Three value
569
+ * Round-trip equality holds for text-surface-representable ASTs. Single
570
+ * aggregate queries serialize in the retained legacy `sum:metric{…}` form;
571
+ * find queries serialize with colon source heads (find: is retired) and
572
+ * pipelines as `source | :fn{…} | :chart{…}`. Three value
397
573
  * shapes the grammar cannot express (or parses away) sit outside that
398
574
  * contract — documented here rather than silently corrupted:
399
575
  * - a `"` inside a filter value: the quoted token has no escape form;
@@ -425,6 +601,53 @@ declare const DISCIPLINE_FACTORS: Record<EffortDiscipline, number>;
425
601
  /** Factor for an arbitrary discipline string; 1.0 when absent or unknown. */
426
602
  declare function disciplineFactorFor(discipline: string | undefined): number;
427
603
 
604
+ /**
605
+ * IFieldCatalog — the consumer interface for field discovery (wayfinder
606
+ * datadog-analytics ticket 15, per the field discovery contract: typeahead
607
+ * reads catalog indexes with bounded result counts and NEVER scans event or
608
+ * result history; the query package performs no browser-storage access — the
609
+ * app injects an implementation over the V17 stores).
610
+ */
611
+ /** One typed field variant as surfaced to authoring surfaces. */
612
+ interface CatalogFieldSuggestion {
613
+ /** Typed field identity (collision-free key). */
614
+ id: string;
615
+ /** Normalized full path. */
616
+ path: string;
617
+ kind: string;
618
+ /** Physical/named dimension of a numeric variant, when known. */
619
+ dimension?: string;
620
+ /** Observed effective units (original spellings as context). */
621
+ units: readonly string[];
622
+ /** Observed original spellings (provenance context). */
623
+ spellings: readonly string[];
624
+ }
625
+ /** One observed categorical value of a field (original spelling kept). */
626
+ interface CatalogValueSuggestion {
627
+ fieldId: string;
628
+ value: string;
629
+ }
630
+ interface IFieldCatalog {
631
+ /**
632
+ * Bounded normalized-path prefix listing for field typeahead. Implementations
633
+ * MUST serve it from the catalog index — never from event/result scans — and
634
+ * MUST cap results at `limit` (narrower prefixes reveal further suggestions
635
+ * rather than silently claiming the page is the whole vocabulary).
636
+ */
637
+ listByPrefix(prefix: string, limit?: number): Promise<CatalogFieldSuggestion[]>;
638
+ /** Typed-variant lookup by exact normalized path — all variants of the path
639
+ * (e.g. numeric and text `score`), for `field(...)` alternatives. */
640
+ lookup(path: string): Promise<CatalogFieldSuggestion[]>;
641
+ /** Bounded categorical-value prefix lookup scoped to one field path. */
642
+ listValues(path: string, valuePrefix: string, limit?: number): Promise<CatalogValueSuggestion[]>;
643
+ /**
644
+ * Change notification — catalog commits signal interested consumers so new
645
+ * fields appear without reload (invalidation wiring lands with ticket 20).
646
+ * Returns an unsubscribe function.
647
+ */
648
+ onChange?(listener: () => void): () => void;
649
+ }
650
+
428
651
  /**
429
652
  * WQL language support — CodeMirror highlighting + autocomplete over the
430
653
  * Lezer grammar (grammar/wql.grammar).
@@ -447,8 +670,28 @@ interface WqlCompletionOptions {
447
670
  * `() => resolver.list().map(e => e.slug)`.
448
671
  */
449
672
  effortNames?: () => readonly string[];
673
+ /**
674
+ * Dynamic values for the frontmatter typed-tag filters (`{domain:…}`,
675
+ * `{format:…}`, `{equipment:…}`, `{quality:…}`, `{intent:…}`) — fed from
676
+ * the tags store's `by-type` index:
677
+ * `async key => (await storage.getAllFromIndex('tags', 'by-type', key)).map(t => t.label)`.
678
+ * Absent or empty results fall through to catalog discovery.
679
+ */
680
+ tagTypeValues?: (key: string) => Promise<readonly string[]> | readonly string[];
681
+ /**
682
+ * Host-supplied values for a filter key (e.g. the composer's suggestion
683
+ * feeds). Non-empty results win over the static vocabularies below.
684
+ */
685
+ values?: (key: string) => Promise<readonly Completion[]>;
686
+ /**
687
+ * Injected field catalog (ticket 15): discovered typed variants join the
688
+ * static metric vocabulary, and categorical values back filter-value
689
+ * suggestions for discovered fields. Bounded prefix lookups only — the
690
+ * catalog never scans event/result history.
691
+ */
692
+ catalog?: IFieldCatalog;
450
693
  }
451
- declare function wqlCompletionSource(options_?: WqlCompletionOptions): (context: CompletionContext) => CompletionResult | null;
694
+ declare function wqlCompletionSource(options_?: WqlCompletionOptions): (context: CompletionContext) => CompletionResult | Promise<CompletionResult | null> | null;
452
695
  declare function wqlCompletion(options?: WqlCompletionOptions): Extension;
453
696
  /**
454
697
  * Query-field colors, mapped onto the app's theme tokens so
@@ -460,141 +703,244 @@ declare const wqlHighlightStyle: HighlightStyle;
460
703
  declare function wql(completionOptions?: WqlCompletionOptions): LanguageSupport;
461
704
 
462
705
  /**
463
- * Analytics unit conversion seam — narrow, fake-free, pure.
706
+ * WQL unit policy (wayfinder ticket 13, home per decision ticket 21).
464
707
  *
465
- * Supports exactly the unit families analytics currently needs:
466
- * - mass: kg ↔ lb (exact 0.45359237 kg per lb)
467
- * - distance: m ↔ km
708
+ * Recognition and conversion evidence live in the core catalog — this module
709
+ * adds the wql application policy: strict conversion before arithmetic, the
710
+ * system-default output table applied when the query has no `in <unit>`
711
+ * directive, and errors (never pass-through) for unknown units and
712
+ * dimension-incompatible conversions.
468
713
  *
469
- * Any unknown or incompatible units pass through unchanged.
714
+ * Cutover notes (ticket 13): the old first-record-unit fallback and the
715
+ * unknown/incompatible pass-through in `resolveDisplayUnit` are gone. The
716
+ * host `kg | lb` widget preference is no longer a tier in the default chain —
717
+ * `useAnalyticsUnitPreference` callers migrate to the system defaults under
718
+ * ticket 19; no new callers allowed.
470
719
  */
471
- declare const KG_PER_LB = 0.45359237;
720
+
721
+ /** Legacy 2-family vocabulary kept only as a type alias for existing
722
+ * callers of the display seam; arithmetic dimensions come from the catalog. */
472
723
  type UnitFamily = 'mass' | 'distance';
473
- interface UnitFamilyDef {
474
- /** Base unit for the family (used for documentation, not conversion). */
475
- base: string;
476
- /** Factor to convert FROM this unit TO the base unit. */
477
- units: Record<string, number>;
478
- }
479
- declare const UNIT_FAMILIES: Record<UnitFamily, UnitFamilyDef>;
480
- declare function getUnitFamily(unit: string | undefined): UnitFamily | undefined;
481
- /**
482
- * Convert a value between known units.
483
- *
484
- * Returns the input unchanged when:
485
- * - from === to
486
- * - either unit is unknown
487
- * - the units belong to different families
488
- */
489
- declare function convert(value: number, from: string | undefined, to: string | undefined): number;
490
- /**
491
- * Determine the target display unit for a set of matched facts and query
492
- * options. Returns the unit to render and whether values should be converted.
493
- */
494
- declare function resolveDisplayUnit(facts: ReadonlyArray<{
724
+ /** The fact-unit shape resolveOutputUnit reads. */
725
+ interface UnitBearingFact {
495
726
  unit?: string;
496
727
  metricUnit?: string;
497
- }>, { directive, preferred, }: {
498
- directive?: string;
499
- preferred?: string;
500
- }): {
728
+ }
729
+ /** Display conversion for UI surfaces (RawPointsTable etc.): converts
730
+ * within a shared dimension through the catalog, passes unknown or
731
+ * cross-dimension values through unchanged instead of throwing. Arithmetic
732
+ * must use {@link convertViaCatalog}. Ticket 19 migrates the remaining
733
+ * display callers onto renderer formatting. */
734
+ declare function convertDisplay(value: number, from: string | undefined, to: string | undefined): number;
735
+ /** Dimension compatibility probe for display surfaces. */
736
+ declare function getUnitFamily(unit: string | undefined): string | undefined;
737
+ interface OutputUnitResolution {
738
+ /** The resolved output unit — undefined means unitless / no conversion. */
501
739
  unit?: string;
740
+ /** Whether fact values must be converted into `unit` before aggregation. */
502
741
  convert: boolean;
503
- };
504
-
742
+ /** Diagnostic when the directive is unusable for this result (unknown
743
+ * unit, or dimension-incompatible with the selected observations). */
744
+ error?: string;
745
+ }
505
746
  /**
506
- * Workout derivation helpers for WQL QueryService and cross-store joins.
747
+ * Resolve the output unit for an aggregate result (arithmetic contract §5):
748
+ *
749
+ * 1. an explicit `in <unit>` directive wins when it is a known unit and
750
+ * dimensionally compatible with the observations; incompatible → error
751
+ * for the affected calculation;
752
+ * 2. otherwise the system default for the observations' dimension
753
+ * (mass `kg`, distance `m`, duration `s`, …; ratio unitless);
754
+ * 3. observations with no unit at all stay unitless — no first-record
755
+ * fallback, no reinterpretation of stored measurements.
507
756
  */
757
+ declare function resolveOutputUnit(facts: ReadonlyArray<UnitBearingFact>, { directive, preferred, }: {
758
+ directive?: string;
759
+ preferred?: string;
760
+ }): OutputUnitResolution;
508
761
 
509
762
  /**
510
- * Map a summary projection name to its Canonical Metric Key — the one key two
511
- * workouts must share for an aggregate to be compared across them. Same key
512
- * in fact rows and display. 'Total Volume' → 'totalVolume', 'TIS' → 'tis'.
513
- */
514
- declare function resolveCanonicalMetricKey(projectionName: string): string;
515
- /**
516
- * Identity of the block a result belongs to, stamped on every fact row.
763
+ * Calendar — system-local civil calendar arithmetic over captured execution
764
+ * contexts (wayfinder datadog-analytics ticket 12, per the time alignment
765
+ * contract §§ Evaluation context, Calendar and duration windows).
766
+ *
767
+ * Calendar days run from local midnight to the next local midnight in the
768
+ * context's timezone; weeks start on Monday. All arithmetic is component
769
+ * math via the timezone's civil date — never multiplication by 86 400 000
770
+ * ms, which mislabels DST-shifted days. Range endpoints are half-open:
771
+ * start included, exclusive end; an inclusive `to` civil date is implemented
772
+ * as an exclusive bound at the following local midnight.
517
773
  */
518
- interface SummaryFactIdentity {
519
- noteId: string;
520
- resultId: string;
521
- /** FK to NoteSegment.id (positional section id of the block run). */
522
- segmentId?: string;
523
- /** NoteSegment.version at record time. */
524
- segmentVersion?: number;
525
- /** Content-stable cross-note join key. */
526
- blockContentId?: string;
527
- /** Which surface produced the result; trend queries exclude 'playground' by default. */
528
- origin?: ResultOrigin;
529
- /** FK to the `page` store (copied from the parent note). */
530
- pageId?: string;
531
- /**
532
- * Canonical workout time — WorkoutResult.createdAt (true workout end).
533
- * Every fact row carries it as `timestamp` so time-range queries mean
534
- * "when the workout happened", never "when the metric was derived".
535
- */
536
- workoutTimestamp?: number;
774
+ /** One captured execution context: every relative window, bucket, and
775
+ * boundary decision in a document run resolves against this one capture. */
776
+ interface ExecutionContext {
777
+ /** Captured current instant (ms epoch). */
778
+ readonly instant: number;
779
+ /** IANA timezone name of the effective system timezone. */
780
+ readonly timeZone: string;
537
781
  }
538
- /** Logs shape read by normalizeSummaryFacts (StoredOutputStatement-compatible). */
539
- interface SummaryFactSourceOutput {
540
- outputType?: string;
541
- metrics: readonly {
542
- type?: string;
543
- value?: unknown;
544
- image?: string;
545
- unit?: string;
546
- /** Summary-processor payload (effortSlug / effortDiscipline / effortIntensityTier / …). */
547
- metadata?: Record<string, unknown>;
548
- }[];
549
- timeSpan?: {
550
- started?: number;
551
- ended?: number;
552
- };
782
+ /** Capture the execution context — once per document run (ticket 19's
783
+ * runner captures; QueryService consumes). */
784
+ declare function captureContext(instant?: number, timeZone?: string): ExecutionContext;
785
+ /** The system's IANA timezone. */
786
+ declare function systemTimeZone(): string;
787
+ /** Civil YYYY-MM-DD of `instant` interpreted in `timeZone` — the calendar
788
+ * the athlete lives in, not UTC. */
789
+ declare function civilDateOf(instant: number, timeZone: string): string;
790
+ /** Add `days` (may be negative) to a civil date — pure date math across
791
+ * month/year boundaries and leap days; no timezone involved. */
792
+ declare function civilDateAdd(iso: string, days: number): string;
793
+ /** Days between two civil dates (second minus first). */
794
+ declare function civilDateDiff(a: string, b: string): number;
795
+ /** The Monday-starting civil week of a civil date. */
796
+ declare function civilMonday(iso: string): string;
797
+ /** The instant of local midnight (00:00) starting civil date `iso` in
798
+ * `timeZone` — component math that survives DST boundaries (a 23h or 25h
799
+ * day still starts at its own local 00:00). */
800
+ declare function zonedStartOfDay(iso: string, timeZone: string): number;
801
+ /** Local noon of civil date `iso` in `timeZone` — the presentation-only
802
+ * representative instant for calendar buckets (never a join key). */
803
+ declare function zonedNoon(iso: string, timeZone: string): number;
804
+ /** A resolved range: [start, end). `endExclusive` marks the half-open
805
+ * convention; `end === Number.MAX_SAFE_INTEGER` with `endExclusive ===
806
+ * false` is the unbounded side. */
807
+ interface ResolvedRange {
808
+ readonly start: number;
809
+ readonly end: number;
810
+ /** True when `end` is an exclusive half-open bound (calendar windows and
811
+ * captured-instant relative cutoffs). */
812
+ readonly endExclusive: boolean;
553
813
  }
554
- /**
555
- * Convert Tier-2 summary outputs (outputType 'analytics') in a result's logs
556
- * into persisted fact rows — one row per result × Canonical Metric Key.
557
- */
558
- declare function normalizeSummaryFacts(logs: readonly SummaryFactSourceOutput[], identity: SummaryFactIdentity): AnalyticsDataPoint[];
559
- /** Identity for unified event-row emission — same fields as the fact identity. */
560
- type EventRowIdentity = SummaryFactIdentity;
561
- /**
562
- * Logs → event rows, 1:1 per statement (ticket 002). Deterministic ids
563
- * `${resultId}:${seq}`; canonical workout time wins over the statement's own
564
- * timeSpan; query-critical scalars promoted top-level.
565
- */
566
- declare function toEventRows(logs: readonly StoredOutputStatement[], identity: SummaryFactIdentity): UnifiedEventRecord[];
567
- /**
568
- * Tier-2 analytics outputs → deterministic summary event rows (tickets
569
- * 002/004): id `${resultId}:summary:${rowKey}` — no timestamp salt, so
570
- * re-finalize overwrites cleanly. Fold identity lives in metrics[0].metadata
571
- * (canonicalKey, effort metadata, groupTags) — shape-uniform with events.
814
+ /** Resolve a parsed query window against the captured context — the single
815
+ * window→range mapping every execution path shares (time contract):
816
+ *
817
+ * - `last Nd`: N local dates including today — midnight N−1 dates back
818
+ * through the captured current instant (inclusive cutoff, exclusive
819
+ * encoding `instant + 1`). Never N×86 400 000 ms.
820
+ * - `last Nw`: current Monday-start week plus N−1 preceding complete
821
+ * weeks, through the captured instant. Never a rolling 7N-date window.
822
+ * - explicit `from`/`to`: both named dates included in full — implemented
823
+ * as an exclusive bound at the following local midnight. An open `to`
824
+ * stays unbounded. Future ranges are preserved, never replaced.
572
825
  */
573
- declare function toSummaryEventRows(logs: readonly StoredOutputStatement[], identity: SummaryFactIdentity): UnifiedEventRecord[];
826
+ declare function resolveWindowRange(w: {
827
+ kind: 'relative';
828
+ size: number;
829
+ unit: 'd' | 'w';
830
+ } | {
831
+ kind: 'range';
832
+ start: string;
833
+ end?: string;
834
+ } | undefined, ctx: ExecutionContext): ResolvedRange | undefined;
835
+ /** Half-open membership predicate: start included, exclusive end excluded
836
+ * (a metric exactly at Monday midnight belongs only to the new week).
837
+ * Unbounded ends admit everything. */
838
+ declare function inRange(instant: number, range: ResolvedRange | undefined): boolean;
839
+
574
840
  /**
575
- * Event row → flat fact currency for the four-stage plan (ticket 003):
576
- * one AnalyticsDataPoint per numeric metric. Canonical key resolution
577
- * mirrors the summary fold — metadata.canonicalKey first, name-derived
578
- * from the row's label metric as fallback. Deterministic fact ids:
579
- * `${record.id}:${factOrdinal}`.
841
+ * Query documents (wayfinder datadog-analytics ticket 17, per the query
842
+ * documents contract).
843
+ *
844
+ * A query block is a self-contained document:
845
+ *
846
+ * defaults [by {dim, …}] [window]
847
+ * <ident> = <query | expression> [(normalize <bucket>)] [-> <unit>]
848
+ * show <ident>, …
849
+ *
850
+ * A body that carries no document constructs parses as a DEGENERATE document
851
+ * wrapping the single query — zero behavior change for legacy bodies
852
+ * (decision ticket 22: every widget body is a document; single-line bodies
853
+ * are degenerate documents).
854
+ *
855
+ * Evaluation order follows the reference graph; forward references are
856
+ * allowed; cycles are a document-level diagnostic.
580
857
  */
581
- declare function projectEventToFacts(record: UnifiedEventRecord): AnalyticsDataPoint[];
582
858
 
859
+ interface DocumentDefaults {
860
+ groupBy?: string[];
861
+ window?: QueryWindow;
862
+ }
863
+ interface DocumentFormula {
864
+ expression: string;
865
+ /** Referenced assignment names, in appearance order. */
866
+ references: string[];
867
+ /** Persisted normalization intent (user-selected target bucket). */
868
+ normalize?: string;
869
+ /** Explicit output unit directive. */
870
+ unit?: string;
871
+ }
872
+ interface DocumentAssignment {
873
+ name: string;
874
+ kind: 'query' | 'formula';
875
+ /** For kind 'query' — the parsed query of any family (defaults merged at run). */
876
+ queryText?: string;
877
+ parsed?: AnyParsedQuery;
878
+ formula?: DocumentFormula;
879
+ raw: string;
880
+ }
881
+ interface QueryDocument {
882
+ defaults?: DocumentDefaults;
883
+ assignments: DocumentAssignment[];
884
+ show: string[];
885
+ /** True when the body carried no document constructs (legacy single query). */
886
+ degenerate: boolean;
887
+ }
888
+ interface ParsedDocument {
889
+ doc: QueryDocument;
890
+ diagnostics: string[];
891
+ }
583
892
  /**
584
- * Pure projection of a block_index into static Notes — one Note per distinct
585
- * noteId, with a `catalog` field set to `noteId.split('/')[0]`. The catalog is
586
- * the directory the file lives under (e.g. `crossfit-girls` for collections,
587
- * `crossfit-programming` for feeds) and is what the Library's panel uses to
588
- * target the `+ Filter → Catalog` menu.
893
+ * Parse a query-document body. Never throws: syntax problems surface as
894
+ * diagnostics so one broken block never takes down a surface.
589
895
  */
590
- declare function staticNotesFromBlocks(blocks: BlockIndexRow[]): Note[];
896
+ declare function parseDocument(text: string): ParsedDocument;
897
+ /** Lossless serialization: round-trips parse(serialize(parse(text))) to the
898
+ * same document (used by dashboards and note rendering to persist intent). */
899
+ declare function serializeDocument(doc: QueryDocument): string;
900
+
591
901
  /**
592
- * Tag → noteIds index over a block index's frontmatter rows. Static-corpus
593
- * notes declare tags only in frontmatter `tags:`; this recovers the mapping
594
- * the note store needs to answer `tags:` clauses (issue #853 — the store
595
- * previously returned an empty set for every tag).
902
+ * Contribution selection — grain integrity and coverage (wayfinder
903
+ * datadog-analytics ticket 16, per the automatic grain contract).
904
+ *
905
+ * Coverage is chosen within each workout/result and typed metric — never by
906
+ * an `any attributed record exists` check over the whole query:
907
+ *
908
+ * - Detail observations and substitute summaries of the same population are
909
+ * duplicate representations: only one is aggregated (detail wins).
910
+ * - A summary with a coverage partition (effort / group tags) disjoint from
911
+ * the present detail substitutes exactly for its uncovered partition —
912
+ * provided its retained statistics answer the requested operation.
913
+ * - A retained population represented only by an insufficient summary
914
+ * reports insufficient evidence — never an invented observation.
915
+ * - Explicit grain filters are hard source constraints and report
916
+ * limitations rather than silently switching representation.
596
917
  */
597
- declare function staticTagIndexFromBlocks(blocks: BlockIndexRow[]): Map<string, Set<string>>;
918
+
919
+ /** Compact coverage references carried on aggregate results (ticket 16
920
+ * explainability; ticket 18's drill-down consumes the on-demand detail). */
921
+ interface CoverageReport {
922
+ /** (resultId, metric) populations selected as detail observations. */
923
+ detailPopulations: number;
924
+ /** Substitute summaries that answered the requested operation. */
925
+ selectedSummaries: Array<{
926
+ resultId: string;
927
+ metricKey: string;
928
+ coverage?: SummaryCoverage;
929
+ }>;
930
+ /** Duplicate representations dropped because detail covered them. */
931
+ ignoredDuplicates: Array<{
932
+ resultId: string;
933
+ metricKey: string;
934
+ id: string;
935
+ }>;
936
+ /** Populations that could not answer the operation — reported, never
937
+ * silently omitted and never fabricated. */
938
+ insufficientScopes: Array<{
939
+ resultId: string;
940
+ metricKey: string;
941
+ reason: string;
942
+ }>;
943
+ }
598
944
 
599
945
  /**
600
946
  * Injected store interfaces for WQL QueryService.
@@ -610,37 +956,125 @@ declare function staticTagIndexFromBlocks(blocks: BlockIndexRow[]): Map<string,
610
956
  * Query Service needs, plus the write contract owned by the write-path
611
957
  * lifecycle (ticket 005).
612
958
  */
613
- interface UnifiedEventStore {
959
+ interface EventStore {
960
+ /** Ticket 12/14 complete-fetch seam: candidates whose METRIC date
961
+ * (civil, multiEntry index) falls in the requested dates — rows whose
962
+ * own date differs from their fetch-hint timestamp are reachable. */
963
+ getEventsByMetricDates?(dates: readonly string[]): Promise<EventRecord[]>;
614
964
  /** Windowed fetch — the one proven culling index (ticket 001). */
615
- getEventsByTimeRange(start: number, end: number): Promise<UnifiedEventRecord[]>;
965
+ getEventsByTimeRange(start: number, end: number): Promise<EventRecord[]>;
616
966
  /** Per-result fetch (rows:{result:…}, re-finalize, orphan inspection). */
617
- getEventsByResult(resultId: string): Promise<UnifiedEventRecord[]>;
967
+ getEventsByResult(resultId: string): Promise<EventRecord[]>;
618
968
  /** Note-scoped fetch (rows:{note:…}). */
619
- getEventsForNote(noteId: string): Promise<UnifiedEventRecord[]>;
969
+ getEventsForNote(noteId: string): Promise<EventRecord[]>;
620
970
  /** Content-scoped fetch — the cross-store join hot path (indexed). */
621
- getEventsByContent(blockContentId: string): Promise<UnifiedEventRecord[]>;
971
+ getEventsByContent(blockContentId: string): Promise<EventRecord[]>;
622
972
  /** Full scan — all-time SELECT leg (ticket 001: scan beats non-selective indexes). */
623
- scanAll(): Promise<UnifiedEventRecord[]>;
973
+ scanAll(): Promise<EventRecord[]>;
624
974
  /** Append event rows (per-statement flush; same-tick coalescing allowed). */
625
- appendEvents(rows: UnifiedEventRecord[]): Promise<void>;
975
+ appendEvents(rows: EventRecord[]): Promise<void>;
626
976
  /** Atomic finalize: clear the result's engine-authored summaries, write finals. */
627
- finalizeSummaries(resultId: string, rows: UnifiedEventRecord[]): Promise<void>;
977
+ finalizeSummaries(resultId: string, rows: EventRecord[]): Promise<void>;
628
978
  /** Reconcile deletes (wellness note-save) + GC sweeps. */
629
979
  deleteEvents(ids: string[]): Promise<void>;
630
980
  }
631
- /** Store surface for content queries (`find:note`). */
981
+ /** Store surface for content queries (`:note`). */
632
982
  interface NoteQueryStore {
633
983
  getAllNotes(): Promise<Note[]>;
634
984
  getNoteIdsForTag(label: string): Promise<Set<string>>;
635
985
  /** Note-tags label set of one note (moved here from FactQueryStore — it
636
986
  * reads note tags, not facts). */
637
987
  getNoteTagLabels(noteId: string): Promise<string[]>;
988
+ /** Indexed/domain candidate read for :note — `selection` carries only
989
+ * exact-semantics predicates, so rows are a SUPERSET of the matches and
990
+ * QueryService re-applies every residual filter in JS. `undefined` =
991
+ * capability unavailable (no domain projection / scoped transaction) →
992
+ * the caller falls back to getAllNotes(). Server order/offset/limit ride
993
+ * only when no residual filter, window, or join precedes the slice. */
994
+ queryDomain?(request: {
995
+ plan: 'notes';
996
+ selection?: WqlDomainPredicate[];
997
+ filters?: WqlDomainPredicate[];
998
+ order?: WqlDomainOrder[];
999
+ offset?: number;
1000
+ limit?: number;
1001
+ }): Promise<WqlDomainReadResult | undefined>;
638
1002
  }
639
- /** Store surface for block-index queries (`find:block`). */
1003
+ /**
1004
+ * Candidate/domain read wire — a structural mirror of packages/storage/src/
1005
+ * domain.ts (root-owned). WQL stays DB-independent: no storage-package
1006
+ * dependency; the app adapter bridges these shapes 1:1 to
1007
+ * IStorage.queryDomain. Keep field-by-field compatible with that file.
1008
+ *
1009
+ * Clause algebra (identical to the wire): clauses AND; a clause's value list
1010
+ * ORs; `negate` excludes any row matching any value.
1011
+ */
1012
+ type WqlDomainPredicate = {
1013
+ field: 'id' | 'noteId' | 'type' | 'catalog' | 'source' | 'tags' | 'effort';
1014
+ values: string[];
1015
+ negate?: boolean;
1016
+ } | {
1017
+ field: 'text';
1018
+ value: string;
1019
+ } | {
1020
+ field: 'date';
1021
+ start: number;
1022
+ end: number;
1023
+ endExclusive?: boolean;
1024
+ } | {
1025
+ field: 'page';
1026
+ value: boolean;
1027
+ }
1028
+ /** The :note default page-exclusion stage (notes plan only). `feeds`
1029
+ * = an authored feed source clause rides along: true drops literal
1030
+ * type 'page' rows; false drops isPage rows (page-like kinds or page:/
1031
+ * guides: source prefixes on non-note kinds). */
1032
+ | {
1033
+ field: 'defaultNotes';
1034
+ feeds: boolean;
1035
+ } | {
1036
+ field: 'sourceFence';
1037
+ };
1038
+ /** Server order clause — structural mirror of the storage wire's
1039
+ * DomainOrder. Only exact-parity orderings are ever sent (natural id order
1040
+ * matches the historical whole-store iteration order). */
1041
+ type WqlDomainOrder = {
1042
+ field: 'id' | 'createdAt' | 'date' | 'title' | 'noteTitle' | 'dataType' | 'position';
1043
+ direction: 'asc' | 'desc';
1044
+ };
1045
+ /** Candidate read result for the plans WQL consumes. `rows` are original
1046
+ * entity payloads; the counts follow the wire contract (selected before
1047
+ * filters, matched before limit) — selectedCount baselines the historical
1048
+ * pre-filter `stages.selected`, matchedCount is informational (WQL
1049
+ * recounts after its own residual pipeline and slice). */
1050
+ type WqlDomainReadResult = {
1051
+ plan: 'notes';
1052
+ projectionVersion: number;
1053
+ selectedCount: number;
1054
+ matchedCount: number;
1055
+ rows: Note[];
1056
+ } | {
1057
+ plan: 'blocks';
1058
+ projectionVersion: number;
1059
+ selectedCount: number;
1060
+ matchedCount: number;
1061
+ rows: BlockIndexRow[];
1062
+ };
1063
+ /** Store surface for block-index queries (`:block`). */
640
1064
  interface BlockQueryStore {
641
1065
  getAllBlocks(): Promise<BlockIndexRow[]>;
1066
+ /** Indexed/domain candidate read for :block — same superset contract as
1067
+ * the note seam; `undefined` falls back to getAllBlocks(). */
1068
+ queryDomain?(request: {
1069
+ plan: 'blocks';
1070
+ selection?: WqlDomainPredicate[];
1071
+ filters?: WqlDomainPredicate[];
1072
+ order?: WqlDomainOrder[];
1073
+ offset?: number;
1074
+ limit?: number;
1075
+ }): Promise<WqlDomainReadResult | undefined>;
642
1076
  }
643
- /** Pure effort model interface for `find:effort` queries. */
1077
+ /** Pure effort model interface for `:effort` queries. */
644
1078
  interface IEffort {
645
1079
  id: string;
646
1080
  slug: string;
@@ -655,18 +1089,69 @@ interface IEffort {
655
1089
  registrySource?: 'bundled' | 'user' | string;
656
1090
  [key: string]: unknown;
657
1091
  }
658
- /** Store surface for effort queries (`find:effort`). */
659
1092
  interface EffortQueryStore {
660
1093
  getAllEfforts(): Promise<IEffort[]>;
661
1094
  }
662
1095
  /** Optional bundle of stores for QueryService initialization. */
1096
+ /** One note's hydration relations — structurally the storage entries
1097
+ * bundle's relation slice, so a host can bridge its entry batch reads
1098
+ * (local: one transaction; remote: entries plan with ids chunked at the
1099
+ * domain parser's 64-value limit) straight through without mapping. WQL
1100
+ * reads only `label` off each tag record and never imports storage. */
1101
+ interface NoteRelationBundle {
1102
+ /** The note's junction tag records (label plane). */
1103
+ tags: ReadonlyArray<{
1104
+ label: string;
1105
+ }>;
1106
+ /** page_notes junction rows for the note; first by position is the
1107
+ * parent page. */
1108
+ links: PageNote[];
1109
+ /** Resolved junction pages by page id. */
1110
+ pages: Page[];
1111
+ }
663
1112
  interface QueryServiceStores {
664
- eventStore?: UnifiedEventStore;
1113
+ eventStore?: EventStore;
665
1114
  noteStore?: NoteQueryStore;
1115
+ /** Serves every block row — user journal and seeded corpus alike (the
1116
+ * seed importer materializes corpus rows into the same store). */
666
1117
  blockStore?: BlockQueryStore;
667
1118
  effortStore?: EffortQueryStore;
1119
+ /** Derived corpus-note projections (catalog/tag planes). */
668
1120
  staticNoteStore?: NoteQueryStore;
669
- staticBlockStore?: BlockQueryStore;
1121
+ /** Serves exercise containment rows (`block_efforts` store). */
1122
+ blockEffortsStore?: {
1123
+ getAllFromIndex(index: string, key: string): Promise<any[]>;
1124
+ };
1125
+ /** Serves tag records (`tags` store). */
1126
+ tagsStore?: {
1127
+ getAllFromIndex(index: string, key: string): Promise<any[]>;
1128
+ };
1129
+ /** Serves note-tag junction rows (`note_tags` store). */
1130
+ noteTagsStore?: {
1131
+ getAllFromIndex(index: string, key: string): Promise<any[]>;
1132
+ };
1133
+ /** Canonical page_notes junction reads for container resolution — a
1134
+ * note's parent Page wins over a standalone Note container. */
1135
+ pageStore?: {
1136
+ getNotePages(noteId: string): Promise<PageNote[]>;
1137
+ getPage(pageId: string): Promise<Page | undefined>;
1138
+ };
1139
+ /** Batched relation reads for the hydration paths (containers, run
1140
+ * tags) — one bundle per distinct note id instead of a page/tag read
1141
+ * per matched result. Optional: absent → the per-note pageStore /
1142
+ * noteStore seams above serve each request. */
1143
+ relationStore?: {
1144
+ getBatches(noteIds: readonly string[]): Promise<Map<string, NoteRelationBundle>>;
1145
+ };
1146
+ /** Synchronous named datasets (`@session`, `@today`, user-stored sources)
1147
+ * populated by the host before queries run — pipeline dataset stages
1148
+ * never touch the database. */
1149
+ datasetStore?: {
1150
+ getDataset(name: string): {
1151
+ events: EventRecord[];
1152
+ notes: Note[];
1153
+ } | undefined;
1154
+ };
670
1155
  }
671
1156
 
672
1157
  /**
@@ -690,37 +1175,124 @@ interface QueryServiceStores {
690
1175
  * Fully inverted dependencies: zero IndexedDB/storage module-level imports.
691
1176
  */
692
1177
 
1178
+ /** Extract the catalog directory id from a Note or BlockIndexRow.
1179
+ * Uses explicit `catalog` when present; falls back to parsing `sourceId`
1180
+ * (stripping `collection:`/`feed:` prefixes and `feeds/` path components) or `noteId`.
1181
+ * Exported so host mappers (Entry derivation) carry the exact field the
1182
+ * `catalog:` filter matches — presentation catalogs must not diverge. */
1183
+ declare function catalogOfItem(item: {
1184
+ id?: string;
1185
+ noteId?: string;
1186
+ sourceId?: string;
1187
+ catalog?: string;
1188
+ }): string | undefined;
1189
+ /** Match a single row against one source filter value. The `journal` kind matches
1190
+ * rows with no sourceId, the literal 'journal', or UUID / journal/date lineage
1191
+ * pointing to the original note. Lineage also passes corpus exclusions below.
1192
+ * The feed / guide kinds match
1193
+ * rows whose sourceId starts with the kind (storage identities keep their
1194
+ * `collection:`/`page:collection:` prefixes — only the query vocabulary
1195
+ * was renamed). A `kind:id` literal matches the
1196
+ * exact id. `playground` matches the playground intake's sourceId convention
1197
+ * and, on the note plane, legacy rows typed 'playground' (playground pages
1198
+ * saved before the sourceId convention existed — their sourceId is absent).
1199
+ * `all` spans exactly the WQL source domain plus sourceless rows — identical
1200
+ * to the source fence. `journal` rejects the rest of the seed corpus: rows
1201
+ * with seed provenance (importer stamps, or surviving seedChunkId /
1202
+ * markdown/ sourcePath / catalog on legacy user-edited rows whose
1203
+ * seedOrigin flipped to 'user') and typed dashboards never journal — a user
1204
+ * journal note carries none of those markers. Exported so host mappers
1205
+ * classify result rows with the exact predicate the `source:` filter
1206
+ * applies. */
1207
+ declare function sourceMatches(item: {
1208
+ id?: string;
1209
+ noteId?: string;
1210
+ sourceId?: string;
1211
+ type?: string;
1212
+ seedOrigin?: string;
1213
+ seedChunkId?: string;
1214
+ sourcePath?: string;
1215
+ catalog?: string;
1216
+ }, kind: string): boolean;
693
1217
  interface FindQueryResult {
694
1218
  parsed: ParsedFindQuery;
695
1219
  notes: Note[];
696
1220
  blocks: BlockIndexRow[];
697
- /** Registry rows for find:effort queries. */
1221
+ /** Registry rows for :effort queries. */
698
1222
  efforts?: IEffort[];
1223
+ /** Session run cards for :session queries (#1041). */
1224
+ runs?: RowsRun[];
1225
+ /** Tabular output for :segment and :event queries (#1042). */
1226
+ table?: TabularResult;
1227
+ /** Raw event rows behind the result — session run statements and table
1228
+ * observations. Pipeline stages aggregate these in memory (no re-read). */
1229
+ events?: EventRecord[];
1230
+ /** Highest container per note id: the parent Page when the canonical
1231
+ * page_notes junction links one, else the standalone Note. Present only
1232
+ * when a pageStore is injected and notes were matched. */
1233
+ containers?: Record<string, NoteContainer>;
699
1234
  stages: {
700
1235
  selected: number;
701
1236
  matched: number;
702
1237
  };
703
1238
  }
1239
+ /** Highest-container link for a matched note (parent Page wins). `slug` and
1240
+ * `date` route app URLs (/p/:slug, /journal/:date). Core Page has no type —
1241
+ * the parent is never classified by the note's kind. */
1242
+ type NoteContainer = {
1243
+ kind: 'page';
1244
+ id: string;
1245
+ slug?: string;
1246
+ date?: string;
1247
+ } | {
1248
+ kind: 'note';
1249
+ id: string;
1250
+ };
1251
+ /** Options for runPipeline: query options plus host-populated datasets.
1252
+ * `pageSources` wins over an injected `datasetStore`; both are synchronous
1253
+ * in-memory lookups — dataset stages never roundtrip the database. */
1254
+ interface PipelineOptions extends QueryOptions {
1255
+ /** Host-populated datasets keyed by name (names include `@`). */
1256
+ pageSources?: ReadonlyMap<string, {
1257
+ events: EventRecord[];
1258
+ notes: Note[];
1259
+ }>;
1260
+ }
1261
+ /** Result of one pipeline run: the final stage's data under its existing
1262
+ * result type, plus the chart sink marker for the renderer. */
1263
+ interface PipelineResult {
1264
+ parsed: ParsedPipelineQuery;
1265
+ series?: Series[];
1266
+ table?: TabularResult;
1267
+ notes?: Note[];
1268
+ blocks?: BlockIndexRow[];
1269
+ efforts?: IEffort[];
1270
+ runs?: RowsRun[];
1271
+ /** Highest container per note id (see FindQueryResult.containers). */
1272
+ containers?: Record<string, NoteContainer>;
1273
+ unit?: string;
1274
+ chart?: PipelineSink;
1275
+ error?: string;
1276
+ }
704
1277
  interface QueryOptions {
705
1278
  rangeStart?: number;
706
1279
  rangeEnd?: number;
707
1280
  /** App-level unit preference ('kg' | 'lb'). Used when query has no `in <unit>` directive. */
708
1281
  preferredUnit?: string;
1282
+ /** Captured execution context (ticket 12) — one `{instant, timeZone}`
1283
+ * capture per document run; defaults to a fresh system capture. */
1284
+ context?: ExecutionContext;
709
1285
  }
710
1286
  /** Options for `runFind` / `runFindBlock` — overrides for the parsed WQL. */
711
1287
  interface FindOptions {
712
- /** Explicit timestamp range; overrides parsed WQL's `last` clause when set. */
713
- range?: {
714
- start: number;
715
- end: number;
716
- };
717
- /** Time-window anchoring mode. 'wall-clock' anchors to Date.now();
718
- * 'latest-activity' anchors to the newest row in the scope selection
719
- * (#857). Defaults to 'wall-clock'. */
720
- anchor?: 'wall-clock' | 'latest-activity';
721
- /** Explicit reference time for the window (useful for tests/replay).
722
- * Overrides `anchor`. */
1288
+ /** Host-supplied timestamp range (half-open [start, end)) — the DEFAULT
1289
+ * when the parsed WQL has no window; an explicit query window wins. */
1290
+ range?: ResolvedRange;
1291
+ /** Explicit reference time for the window (tests/replay). Superseded by
1292
+ * `context` when both are given. */
723
1293
  anchorNow?: number;
1294
+ /** Captured execution context (ticket 12). */
1295
+ context?: ExecutionContext;
724
1296
  }
725
1297
  interface QueryResult {
726
1298
  parsed: ParsedAggregateQuery;
@@ -736,6 +1308,10 @@ interface QueryResult {
736
1308
  scalar?: number;
737
1309
  /** Result display unit, if determined by directive, preference, or fact metadata. */
738
1310
  unit?: string;
1311
+ /** First diagnostic across series (ticket 13) — per-widget badges in 19. */
1312
+ error?: string;
1313
+ /** Compact coverage references (ticket 16 explainability). */
1314
+ coverage?: CoverageReport;
739
1315
  }
740
1316
  /** One run in a rows result: the canonical result identity plus the event
741
1317
  * rows that survived the optional output-type narrowing (`rows:segment{…}`). */
@@ -744,48 +1320,125 @@ interface RowsRun {
744
1320
  noteId: string;
745
1321
  /** Canonical workout time (result.createdAt under the unified model). */
746
1322
  timestamp: number;
747
- events: UnifiedEventRecord[];
1323
+ events: EventRecord[];
748
1324
  }
749
1325
  interface RowsQueryResult {
750
1326
  parsed: ParsedRowsQuery;
751
1327
  runs: RowsRun[];
752
1328
  error?: string;
1329
+ /** Ticket 18 — cross-workout tabular result (cross-workout rows only). */
1330
+ table?: TabularResult;
1331
+ }
1332
+ interface TabularColumn {
1333
+ name: string;
1334
+ type: 'date' | 'string' | 'number';
1335
+ unit?: string;
1336
+ }
1337
+ /** Civil dates (YYYY-MM-DD, `timeZone`-local) whose [local midnight,
1338
+ * next local midnight) windows intersect `[start, end)` — the by-metric-date
1339
+ * candidate set for the complete fetch (ticket 12/14). Callers pass bounded
1340
+ * ranges only: an unbounded side uses the all-store scan as the complete
1341
+ * fetch instead of enumerating an open horizon. */
1342
+ declare function civilDatesCoveredByRange(start: number, end: number, timeZone: string): string[];
1343
+ /** The stable tabular shape consumed by table widgets (ticket 19 wires
1344
+ * widgets): bounded page + full match count. Missing column values are
1345
+ * ABSENT (undefined) — strictly distinct from a recorded 0. */
1346
+ interface TabularGroup {
1347
+ key: string;
1348
+ label: string;
1349
+ rows: Array<Record<string, unknown>>;
1350
+ }
1351
+ interface TabularResult {
1352
+ columns: TabularColumn[];
1353
+ rows: Array<Record<string, unknown>>;
1354
+ groups?: TabularGroup[];
1355
+ totalCount: number;
1356
+ limit?: number;
1357
+ offset?: number;
753
1358
  }
1359
+ /** Structural missing-group sentinel — distinct from any literal text
1360
+ * (group identity is the JSON tuple, so it cannot collide). */
1361
+ declare const UNASSIGNED = "\0unassigned";
754
1362
  declare class QueryService {
755
1363
  private readonly store;
756
1364
  private readonly noteStore;
757
1365
  private readonly blockStore;
758
1366
  private readonly effortStore;
759
1367
  private readonly staticNoteStore?;
760
- private readonly staticBlockStore?;
761
- constructor(storesOrEventStore?: QueryServiceStores | UnifiedEventStore, noteStore?: NoteQueryStore, blockStore?: BlockQueryStore, effortStore?: EffortQueryStore, staticNoteStore?: NoteQueryStore, staticBlockStore?: BlockQueryStore);
1368
+ private readonly pageStore?;
1369
+ private readonly relationStore?;
1370
+ private readonly datasetStore?;
1371
+ private blockEffortsStore;
1372
+ private tagsStore;
1373
+ private noteTagsStore;
1374
+ constructor(storesOrEventStore?: QueryServiceStores | EventStore, noteStore?: NoteQueryStore, blockStore?: BlockQueryStore, effortStore?: EffortQueryStore, staticNoteStore?: NoteQueryStore);
762
1375
  /** Windowed flat-fact fetch — the projected view over the event store. */
763
1376
  getFactsByTimeRange(start: number, end: number): Promise<AnalyticsDataPoint[]>;
764
1377
  runQuery(raw: string, options?: QueryOptions): Promise<QueryResult>;
765
1378
  /**
766
- * Execute a rows query (rows:<target>{…}, #949/C4) — the session results
767
- * table plane. Filter rules are validated at parse; this executes only.
768
- * Reads event rows directly over the unified store: outputType narrowing
769
- * hits the promoted column; content-plane targets scope by content.
1379
+ * Legacy rows-family executor kept for hand-built ASTs (RowsTable, CLI).
1380
+ * Text `rows:…` no longer parses (#1044); this delegates to the find paths
1381
+ * so a programmatic ParsedRowsQuery still resolves.
770
1382
  */
771
1383
  runRows(parsed: ParsedRowsQuery, options?: {
772
1384
  anchorNow?: number;
1385
+ context?: ExecutionContext;
773
1386
  }): Promise<RowsQueryResult>;
774
1387
  /**
775
- * Execute a content-discovery query (find:note). Naive in-memory filtering
1388
+ * Execute a :session query (#1041/#1042) — grouped run cards per completed session.
1389
+ */
1390
+ runFindSession(parsed: ParsedFindQuery, options?: FindOptions): Promise<FindQueryResult>;
1391
+ /**
1392
+ * Execute a :segment or :event query (#1042/#1048) — flat cross-workout table.
1393
+ */
1394
+ runFindTable(parsed: ParsedFindQuery, options?: FindOptions): Promise<FindQueryResult>;
1395
+ /**
1396
+ * Execute a content-discovery query (:note). Naive in-memory filtering
776
1397
  * per the tracer-bullet scope (#797): load all notes, then apply tag/text/
777
1398
  * time filters.
778
1399
  */
779
1400
  runFind(parsed: ParsedFindQuery, options?: FindOptions): Promise<FindQueryResult>;
1401
+ /** Highest container per note id: the parent Page from the canonical
1402
+ * page_notes junction (first link by position) wins; a note with no page
1403
+ * link stays a standalone Note container. Batched when a relationStore
1404
+ * is injected — one bundle read per run instead of a link/page read per
1405
+ * matched note. */
1406
+ private containersForNotes;
780
1407
  /**
781
- * Execute a find:block query against the derived block_index store.
1408
+ * Execute a :block query against the derived block_index store.
782
1409
  */
783
1410
  runFindBlock(parsed: ParsedFindQuery, options?: FindOptions): Promise<FindQueryResult>;
784
1411
  /**
785
- * Execute a find:effort query against the effort registry.
1412
+ * Execute a :effort query against the effort registry.
786
1413
  */
787
1414
  runFindEffort(parsed: ParsedFindQuery): Promise<FindQueryResult>;
788
- run(parsed: ParsedAggregateQuery, options?: QueryOptions): Promise<QueryResult>;
1415
+ run(parsed: AnyParsedQuery, options?: QueryOptions): Promise<QueryResult>;
1416
+ /**
1417
+ * In-memory aggregation over ALREADY-FETCHED event rows — the pipeline
1418
+ * dataset/transform path. Identical metric projection, selection,
1419
+ * bucketing, unit, and aggregation stages as `run`; no store reads (a
1420
+ * content join on carried data is REJECTED by runAggregateEvents — it
1421
+ * would abandon the carried population).
1422
+ */
1423
+ runAggregateEvents(query: ParsedAggregateQuery, events: readonly EventRecord[], options?: QueryOptions): Promise<QueryResult>;
1424
+ /** Public pipeline executor — parse the text, then run. */
1425
+ runPipeline(queryText: string, options?: PipelineOptions): Promise<PipelineResult>;
1426
+ /** Execute an already-parsed pipeline: resolve the source once, then flow
1427
+ * the carried value left to right through the transforms — each function
1428
+ * reduces the PRECEDING stage, never an independent re-query. Dataset
1429
+ * stages look up `pageSources` (then an injected datasetStore)
1430
+ * synchronously; no database roundtrip. */
1431
+ runPipelineParsed(parsed: AnyParsedQuery, options?: PipelineOptions): Promise<PipelineResult>;
1432
+ /** Events behind a content source — resolved ONLY when a transform needs
1433
+ * them: session/table sources already carry their rows; note sources read
1434
+ * the per-note index; block sources read the content index. */
1435
+ private sourceEventsFromFind;
1436
+ /** QueryExecutor adapter: a pipeline run's final stage rendered as the
1437
+ * aggregate QueryResult shape chart consumers already read. */
1438
+ private pipelineResultToQueryResult;
1439
+ /** Stages 2–4 over raw event rows: project → metric filter → range filter
1440
+ * → tag filters → coverage selection → BUCKET/GROUP/AGGREGATE. */
1441
+ private aggregateEventRows;
789
1442
  /**
790
1443
  * Stages 2–4 — BUCKET → GROUP → AGGREGATE over an already-selected +
791
1444
  * filtered `matched` set.
@@ -795,30 +1448,388 @@ declare class QueryService {
795
1448
  * blockContentIds owned by the find predicate's content matches. */
796
1449
  private runJoined;
797
1450
  /** Direction 1 — keep only content owning a wod block whose raw-log metric
798
- * aggregate satisfies the predicate. `notes` for find:note, `blocks` for
799
- * find:block. */
1451
+ * aggregate satisfies the predicate. `notes` for :note, `blocks` for
1452
+ * :block. */
800
1453
  private applyMetricJoin;
801
1454
  /** Re-derive the metric from raw logs for each content id, aggregate per id,
802
1455
  * and return the ids whose aggregate satisfies the join predicate. */
803
1456
  private contentIdsSatisfying;
804
- /** Filter per-effort vs un-attributed overall summary rows to avoid
805
- * double-counting. */
806
- private applyEffortScope;
807
1457
  /** Summary facts for one Canonical Metric Key across the given content ids —
808
1458
  * the cross-store join source. Reads finalize-written summary rows straight
809
1459
  * off the content index (ticket 003: the store is authoritative; the old
810
1460
  * freshness re-derivation is moot). One fact row per result × rowKey. */
811
1461
  private deriveMetricFacts;
812
- /** All content blocks across the journal + static corpus. */
1462
+ /** All content blocks — journal and seeded corpus in one store. */
813
1463
  private allContentBlocks;
814
1464
  /** Map each note id to the wod blockContentIds it owns. */
815
1465
  private noteContentMap;
816
1466
  /** Collect wod blockContentIds owned by a find query's content matches. */
817
1467
  private contentIdsFromFindResult;
818
- /** Load note tag labels only when the query touches 'tags'. */
1468
+ /** Load note tag labels only when the query touches 'tags'. Batched when
1469
+ * a relationStore is injected — one bundle read per run instead of a
1470
+ * junction read per row's note. */
819
1471
  private loadNoteTags;
1472
+ /** Resolve matching note IDs for an exercise slug via the block_efforts containment index. */
1473
+ private getNoteIdsForEffort;
1474
+ /** Resolve matching note IDs for a canonical typed tag (e.g. equipment:kettlebell). */
1475
+ private getNoteIdsForTypedTag;
1476
+ /**
1477
+ * Find catalogs: identifies catalogs whose member notes (or catalog landing
1478
+ * notes) match the authored criteria (effort, tags, text, typed tags, etc.).
1479
+ */
1480
+ private runFindCatalog;
1481
+ }
1482
+
1483
+ /**
1484
+ * Document runner + formula evaluation (wayfinder datadog-analytics
1485
+ * ticket 17).
1486
+ *
1487
+ * - `FormulaEvaluator` is an INJECTED capability: the engine/app composition
1488
+ * adapts lang's calc Pratt evaluator behind it. `wql` ships a small
1489
+ * built-in pointwise arithmetic evaluator so documents evaluate without
1490
+ * the injection (add/sub/mul/div, corr) — never a dependency on lang.
1491
+ * - Post-aggregate formulas evaluate pointwise over the ALIGNMENT DOMAIN:
1492
+ * the intersection of input requested ranges and the union of observed
1493
+ * group tuples. Missing operands substitute zero, flagged as
1494
+ * missing-derived; missing-derived zeros are excluded from downstream
1495
+ * ordinary averages; division by zero propagates an error.
1496
+ * - `corr(a, b)` is paired-only over in-range positions; the pair count is
1497
+ * reported.
1498
+ */
1499
+
1500
+ /** Presence provenance of one operand value at one aligned position. */
1501
+ interface OperandValue {
1502
+ value: number;
1503
+ /** True when the value was substituted for a missing input. */
1504
+ missingDerived: boolean;
1505
+ }
1506
+ type PointwiseInputs = Record<string, (position: number) => OperandValue | undefined>;
1507
+ interface FormulaEvaluation {
1508
+ value: number;
1509
+ missingDerived: boolean;
1510
+ error?: string;
1511
+ }
1512
+ interface FormulaEvaluator {
1513
+ /**
1514
+ * Evaluate `expression` at one aligned position. `inputs` maps referenced
1515
+ * assignment names to position accessors; a missing operand is `undefined`
1516
+ * (the evaluator substitutes zero and flags the result).
1517
+ */
1518
+ evaluate(expression: string, inputs: PointwiseInputs, position: number): FormulaEvaluation;
1519
+ }
1520
+ /** Built-in pointwise arithmetic evaluator (+ - * /, unary minus, corr-free). */
1521
+ declare function createBuiltinFormulaEvaluator(): FormulaEvaluator;
1522
+ /** Append document-default window/groupBy as textual suffixes before family
1523
+ * dispatch — defaults apply to find and pipeline documents too (a bare
1524
+ * `last 4w` block default must bound a `:journal{…}` content run). */
1525
+ declare function appendSuffixes(queryText: string, window: QueryWindow, groupBy?: string[]): string;
1526
+ /** One ordered `show` output — widget-independent (ticket 19 consumes). */
1527
+ interface DocumentOutput {
1528
+ name: string;
1529
+ kind: 'aggregate' | 'formula' | 'find' | 'pipeline';
1530
+ /** Ticket 19 — TabularResult for cross-workout rows outputs. */
1531
+ table?: TabularResult;
1532
+ unit?: string;
1533
+ groupBy?: string[];
1534
+ window?: QueryWindow;
1535
+ /** Series for aggregate and formula outputs. */
1536
+ series?: Array<{
1537
+ key: string;
1538
+ label: string;
1539
+ points: Array<{
1540
+ ts: number;
1541
+ value: number;
1542
+ missing?: boolean;
1543
+ missingDerived?: boolean;
1544
+ }>;
1545
+ unit?: string;
1546
+ }>;
1547
+ /** Formula provenance: pair counts for corr, coverage notes. */
1548
+ pairs?: number;
1549
+ /** Content-plane payloads (find / find-source pipeline outputs). */
1550
+ notes?: Note[];
1551
+ blocks?: BlockIndexRow[];
1552
+ efforts?: unknown[];
1553
+ /** Highest-container page/note links keyed by note id. */
1554
+ containers?: Record<string, NoteContainer>;
1555
+ /** Session run cards (find:session / session-source pipelines). */
1556
+ runs?: RowsRun[];
1557
+ /** Terminal chart sink of a pipeline output (renderer dispatch). */
1558
+ chart?: PipelineSink;
1559
+ error?: string;
1560
+ }
1561
+ interface DocumentResult {
1562
+ outputs: DocumentOutput[];
1563
+ diagnostics: string[];
1564
+ }
1565
+ /** Payload surfaces the ticket-19 family dispatch may hand back. */
1566
+ interface FamilyRunPayload {
1567
+ series?: DocumentOutput['series'];
1568
+ table?: TabularResult;
1569
+ notes?: Note[];
1570
+ blocks?: BlockIndexRow[];
1571
+ efforts?: unknown[];
1572
+ containers?: Record<string, NoteContainer>;
1573
+ runs?: RowsRun[];
1574
+ unit?: string;
1575
+ chart?: PipelineSink;
1576
+ error?: string;
1577
+ }
1578
+ interface QueryDocumentRunnerOptions {
1579
+ /** Injected formula evaluator (lang's calc evaluator via the engine). */
1580
+ formulaEvaluator?: FormulaEvaluator;
1581
+ /** Host default window — used only when the document has none. */
1582
+ rangeStart?: number;
1583
+ rangeEnd?: number;
1584
+ /** Ticket 19 family dispatch: find queries surface FindQueryResult. */
1585
+ runFind?: (queryText: string) => Promise<FamilyRunPayload>;
1586
+ /** Pipeline queries (`:source | :fn | :chart`, `@dataset | …`) surface
1587
+ * the pipeline result — the host's QueryService.runPipeline. */
1588
+ runPipeline?: (queryText: string) => Promise<FamilyRunPayload>;
1589
+ }
1590
+ declare class QueryDocumentRunner$1 {
1591
+ private readonly runAggregate;
1592
+ private readonly evaluator;
1593
+ constructor(runAggregate: (queryText: string, overrides: {
1594
+ groupBy?: string[];
1595
+ window?: QueryWindow;
1596
+ }) => Promise<{
1597
+ series?: DocumentOutput['series'];
1598
+ unit?: string;
1599
+ error?: string;
1600
+ window?: QueryWindow;
1601
+ }>, options?: QueryDocumentRunnerOptions);
1602
+ private readonly options;
1603
+ /** Parse → validate → evaluate in reference order → ordered outputs. */
1604
+ run(text: string): Promise<DocumentResult>;
1605
+ /** Defaults precedence (ticket 17): explicit per-query range/grouping >
1606
+ * block defaults > host. Explicit per-query by{} fully replaces block
1607
+ * grouping — never merges. */
1608
+ private mergeDefaults;
820
1609
  }
821
1610
 
1611
+ /**
1612
+ * QueryDocumentRunner — the ONE execution path for query documents
1613
+ * (wayfinder datadog-analytics ticket 19, per the shared query execution
1614
+ * contract). Dashboard surfaces and embedded note query documents evaluate
1615
+ * through this runner via a host adapter; no surface runs queries itself.
1616
+ *
1617
+ * Per document run:
1618
+ * 1. token substitution (`$name` → active parameter values, raw text);
1619
+ * 2. parse as a QueryDocument (degenerate single query = legacy body);
1620
+ * 3. AST rollup inspection — `calc.*`-class targets (including pipeline
1621
+ * transform stages) trigger the host's rollup-ensure ONCE per run
1622
+ * (never string matching over query text);
1623
+ * 4. one captured execution context threads every window resolution;
1624
+ * 5. family dispatch: aggregate → aggregate run, find → runFind,
1625
+ * rows → runRows, pipeline → runPipeline.
1626
+ *
1627
+ * Zero browser dependencies — stores, catalog, and evaluator are injected.
1628
+ * Hosts that lack a family capability surface a diagnostic on the output —
1629
+ * never a silent empty result.
1630
+ */
1631
+
1632
+ /** Host-populated page source dataset: named standard sources (`@session`,
1633
+ * `@today`, user names include `@`) resolved in memory before queries run. */
1634
+ interface PageSourceEntry {
1635
+ events: EventRecord[];
1636
+ notes: Note[];
1637
+ }
1638
+ /**
1639
+ * PageSourceRegistry — the host's named page-source store (ticket 5).
1640
+ * Constructed/populated ONCE per page load / run context, BEFORE child
1641
+ * queries evaluate. Dataset pipeline stages (`@today | :sum{…}`) read it
1642
+ * synchronously through `QueryServiceStores.datasetStore` — no database
1643
+ * roundtrip downstream.
1644
+ */
1645
+ declare class PageSourceRegistry extends Map<string, PageSourceEntry> {
1646
+ /** QueryService datasetStore seam alias — names include the `@`. */
1647
+ getDataset(name: string): PageSourceEntry | undefined;
1648
+ }
1649
+ interface QueryDocumentRunnerHost {
1650
+ runAggregate(queryText: string, overrides: {
1651
+ groupBy?: string[];
1652
+ window?: QueryWindow;
1653
+ context?: ExecutionContext;
1654
+ }): Promise<{
1655
+ series?: DocumentOutput['series'];
1656
+ unit?: string;
1657
+ error?: string;
1658
+ }>;
1659
+ runFind?(queryText: string, context?: ExecutionContext): Promise<{
1660
+ kind: 'find';
1661
+ notes?: Note[];
1662
+ blocks?: BlockIndexRow[];
1663
+ efforts?: unknown[];
1664
+ containers?: Record<string, NoteContainer>;
1665
+ runs?: RowsRun[];
1666
+ table?: TabularResult;
1667
+ unit?: string;
1668
+ error?: string;
1669
+ }>;
1670
+ /** Pipeline execution (`:source | :fn | :chart`, `@dataset | …`) — the
1671
+ * host's QueryService.runPipeline. */
1672
+ runPipeline?(queryText: string, context?: ExecutionContext): Promise<FamilyRunPayload>;
1673
+ /** Injected formula evaluator override (engine/app composition adapts
1674
+ * lang's calc evaluator); the built-in pointwise evaluator otherwise. */
1675
+ formulaEvaluator?: FormulaEvaluator;
1676
+ /** Host rollup driver: recompute-on-open for `calc.*` facts. Must skip
1677
+ * already-up-to-date prerequisites (no ensure→refresh loop). */
1678
+ rollupEnsure?: () => Promise<void>;
1679
+ }
1680
+ interface SharedRunnerOptions {
1681
+ formulaEvaluator?: FormulaEvaluator;
1682
+ /** One captured execution context per document run (ticket 12). */
1683
+ context?: ExecutionContext;
1684
+ /** Host default range — applies only when the document has no window. */
1685
+ rangeStart?: number;
1686
+ rangeEnd?: number;
1687
+ /** Active token values (frontmatter controls, dashboard controls). */
1688
+ tokens?: Record<string, string>;
1689
+ }
1690
+ declare class QueryDocumentRunner {
1691
+ private readonly host;
1692
+ private readonly initialOptions;
1693
+ constructor(host: QueryDocumentRunnerHost, options?: SharedRunnerOptions);
1694
+ /**
1695
+ * Run one document. `runOptions` override construction-time captures
1696
+ * (tokens, context) — one capture per run. Each run gets its OWN
1697
+ * evaluation snapshot: overlapping runs (one awaiting rollupEnsure while
1698
+ * another executes) can never borrow each other's context.
1699
+ */
1700
+ run(text: string, runOptions?: SharedRunnerOptions): Promise<DocumentResult>;
1701
+ }
1702
+ /** Substitute `$name` tokens with active parameter values (raw text). */
1703
+ declare function substituteTokens$1(text: string, tokens: Record<string, string>): string;
1704
+ /**
1705
+ * AST rollup inspection (replaces `.includes('calc.')` string sniffing):
1706
+ * true when the parsed head's metric — a pipeline's source aggregate metric
1707
+ * or any transform stage's — is a calc target whose derivation depends on
1708
+ * workload rollup facts.
1709
+ */
1710
+ declare function consumesRollupFacts(parsed: AnyParsedQuery | undefined): boolean;
1711
+
1712
+ /**
1713
+ * Workout derivation helpers for WQL QueryService and cross-store joins.
1714
+ */
1715
+
1716
+ /**
1717
+ * Map a summary projection name to its Canonical Metric Key — the one key two
1718
+ * workouts must share for an aggregate to be compared across them. Same key
1719
+ * in fact rows and display. 'Total Volume' → 'totalVolume', 'TIS' → 'tis'.
1720
+ */
1721
+ declare function resolveCanonicalMetricKey(projectionName: string): string;
1722
+ /**
1723
+ * Identity of the block a result belongs to, stamped on every fact row.
1724
+ */
1725
+ interface SummaryFactIdentity {
1726
+ noteId: string;
1727
+ resultId: string;
1728
+ /** FK to NoteSegment.id (positional section id of the block run). */
1729
+ segmentId?: string;
1730
+ /** NoteSegment.version at record time. */
1731
+ segmentVersion?: number;
1732
+ /** Content-stable cross-note join key. */
1733
+ blockContentId?: string;
1734
+ /** Which surface produced the result; trend queries exclude 'playground' by default. */
1735
+ origin?: ResultOrigin;
1736
+ /** FK to the `page` store (copied from the parent note). */
1737
+ pageId?: string;
1738
+ /**
1739
+ * Canonical workout time — WorkoutResult.createdAt (true workout end).
1740
+ * Every fact row carries it as `timestamp` so time-range queries mean
1741
+ * "when the workout happened", never "when the metric was derived".
1742
+ */
1743
+ workoutTimestamp?: number;
1744
+ }
1745
+ /** Logs shape read by normalizeSummaryFacts (StoredOutputStatement-compatible). */
1746
+ interface SummaryFactSourceOutput {
1747
+ outputType?: string;
1748
+ metrics: readonly {
1749
+ type?: string;
1750
+ value?: unknown;
1751
+ image?: string;
1752
+ unit?: string;
1753
+ /** Summary-processor payload (effortSlug / effortDiscipline / effortIntensityTier / …). */
1754
+ metadata?: Record<string, unknown>;
1755
+ }[];
1756
+ timeSpan?: {
1757
+ started?: number;
1758
+ ended?: number;
1759
+ };
1760
+ }
1761
+ /**
1762
+ * Convert Tier-2 summary outputs (outputType 'analytics') in a result's logs
1763
+ * into persisted fact rows — one row per result × Canonical Metric Key.
1764
+ */
1765
+ declare function normalizeSummaryFacts(logs: readonly SummaryFactSourceOutput[], identity: SummaryFactIdentity): AnalyticsDataPoint[];
1766
+ /** Identity for unified event-row emission — same fields as the fact identity. */
1767
+ type EventRowIdentity = SummaryFactIdentity;
1768
+ /**
1769
+ * Logs → event rows, 1:1 per statement (ticket 002). Deterministic ids
1770
+ * `${resultId}:${seq}`; query-critical scalars promoted top-level.
1771
+ *
1772
+ * Ticket 12 temporal anchoring: the row timestamp is the statement's OWN
1773
+ * timeSpan start when it has one — the workout-start timestamp never
1774
+ * relocates a metric observation whose own instant differs. Statements with
1775
+ * a timeSpan carry per-metric temporal anchors ('instant') so projection
1776
+ * groups each observation under its own date.
1777
+ */
1778
+ declare function toEventRows(logs: readonly StoredOutputStatement[], identity: SummaryFactIdentity): EventRecord[];
1779
+ /**
1780
+ * Tier-2 analytics outputs → deterministic summary event rows (tickets
1781
+ * 002/004): id `${resultId}:summary:${rowKey}` — no timestamp salt, so
1782
+ * re-finalize overwrites cleanly. Fold identity lives in metrics[0].metadata
1783
+ * (canonicalKey, effort metadata, groupTags) — shape-uniform with events.
1784
+ */
1785
+ declare function toSummaryEventRows(logs: readonly StoredOutputStatement[], identity: SummaryFactIdentity): EventRecord[];
1786
+ /**
1787
+ * Event row → flat fact currency for the four-stage plan (ticket 003):
1788
+ * one AnalyticsDataPoint per numeric metric. Canonical key resolution
1789
+ * mirrors the summary fold — metadata.canonicalKey first, name-derived
1790
+ * from the row's label metric as fallback. Deterministic fact ids:
1791
+ * `${record.id}:${factOrdinal}`.
1792
+ */
1793
+ declare function projectEventToFacts(record: EventRecord): AnalyticsDataPoint[];
1794
+ /**
1795
+ * Inverse of {@link toEventRows}/{@link toSummaryEventRows}: rebuild the
1796
+ * statement stream a session's event rows represent. Event rows keep their
1797
+ * recorded order (id `${resultId}:${seq}`); summary rows follow in fold
1798
+ * order. Consumers that need the statement shape (replay, review display)
1799
+ * read events and call this — the store keeps only event rows.
1800
+ */
1801
+ declare function eventsToStoredLogs(events: readonly EventRecord[]): StoredOutputStatement[];
1802
+
1803
+ /**
1804
+ * Landing/catalog projection for a block row, derived from its sourceId route
1805
+ * (`collection:<catalog>/<item>`, `page:collection:<catalog>`,
1806
+ * `feed:feeds/<catalog>/…`) — never from `noteId`, which is a UUID for
1807
+ * imported notes. Legacy path-keyed rows keep deriving from the noteId stem
1808
+ * through `catalogOfItem` (undefined for UUID or no path); a bare
1809
+ * `collection:<catalog>` route is the legacy landing and gets its canonical
1810
+ * `page:collection:` sourceId.
1811
+ */
1812
+ declare function staticCatalogRoute(block: Pick<BlockIndexRow, 'noteId' | 'sourceId'>): {
1813
+ landing: boolean;
1814
+ catalog?: string;
1815
+ sourceId?: string;
1816
+ };
1817
+ /**
1818
+ * Pure projection of a block_index into static Notes — one Note per distinct
1819
+ * noteId. `id` stays the canonical storage identity (UUID for imported rows);
1820
+ * `catalog` and feed-landing status come from the sourceId route, not
1821
+ * the noteId path. The catalog (e.g. `crossfit-girls`) is what the Library's
1822
+ * panel uses to target the `+ Filter → Catalog` menu.
1823
+ */
1824
+ declare function staticNotesFromBlocks(blocks: BlockIndexRow[]): Note[];
1825
+ /**
1826
+ * Tag → noteIds index over a block index's frontmatter rows. Static-corpus
1827
+ * notes declare tags only in frontmatter `tags:`; this recovers the mapping
1828
+ * the note store needs to answer `tags:` clauses (issue #853 — the store
1829
+ * previously returned an empty set for every tag).
1830
+ */
1831
+ declare function staticTagIndexFromBlocks(blocks: BlockIndexRow[]): Map<string, Set<string>>;
1832
+
822
1833
  /**
823
1834
  * Dashboard Note model (#898 map, format locked in #899).
824
1835
  *
@@ -827,12 +1838,13 @@ declare class QueryService {
827
1838
  * ```query[:<type>][-<N>|-full] blocks whose title/question associate from the
828
1839
  * markdown heading/paragraph directly above each block. Queries reference
829
1840
  * tokens as `$name`, substituted as raw text at execution time. A block body
830
- * is one line: WQL query + optional `/`-separated positional params.
1841
+ * body is a Query Document (decision 22); presentation params ride the
1842
+ * fence tag as `key=value` attributes.
831
1843
  *
832
1844
  * This module is pure — no React, no editor, no query service — so the editor
833
1845
  * extensions, the inline block renderer, and the dashboard route all share it.
834
1846
  */
835
- declare const DASHBOARD_WIDGET_TYPES: readonly ["table", "value", "timeseries", "bar", "toplist", "stacked-bar", "goal-rings", "zone-distribution"];
1847
+ declare const DASHBOARD_WIDGET_TYPES: readonly ["table", "list", "value", "timeseries", "bar", "donut", "toplist", "stacked-bar", "goal-rings", "zone-distribution"];
836
1848
  type DashboardWidgetType = (typeof DASHBOARD_WIDGET_TYPES)[number];
837
1849
  declare function isDashboardWidgetType(type: string): type is DashboardWidgetType;
838
1850
  /** The widget type a block renders as: a bare ```query fence defaults to `table`. */
@@ -856,6 +1868,11 @@ interface QueryWidgetSuffix {
856
1868
  spanCols?: number;
857
1869
  /** Full-row flag (`-full`). Mutually exclusive with spanCols. */
858
1870
  spanFull?: boolean;
1871
+ /** Presentation attributes: `key=value` tokens after the type/span
1872
+ * (decision 22 — positional params ride the fence tag, not the body). */
1873
+ attributes: Record<string, string>;
1874
+ /** Attribute order for stable re-serialization. */
1875
+ attributeOrder: string[];
859
1876
  /** Set when the suffix is malformed (the type is still reported raw). */
860
1877
  error?: string;
861
1878
  }
@@ -899,16 +1916,6 @@ declare function substituteTokens(query: string, values: Record<string, string>)
899
1916
  };
900
1917
  /** Token names a query references, in order of first appearance. */
901
1918
  declare function referencedTokens(query: string): string[];
902
- /**
903
- * Split a block body into the WQL query and its trailing `/`-separated
904
- * positional parameters (decision #899-7). Splits at the FIRST ` / ` so
905
- * params stay trailing; a literal ` / ` inside a tag value is pathological
906
- * and unsupported. Params are whitespace-split literals or `$token` refs.
907
- */
908
- declare function splitWidgetBody(body: string): {
909
- query: string;
910
- params: string[];
911
- };
912
1919
  /** Structural input for the builder — satisfied by EditorSection + doc text. */
913
1920
  interface DashboardSectionInput {
914
1921
  /** Section type ('query', 'markdown', 'frontmatter', …). */
@@ -925,6 +1932,9 @@ interface DashboardSectionInput {
925
1932
  spanFull?: boolean;
926
1933
  /** Malformed fence-suffix reason — carried so route mode badges it too. */
927
1934
  widgetError?: string;
1935
+ /** Fence-tag presentation attributes (decision 22). */
1936
+ attributes?: Record<string, string>;
1937
+ attributeOrder?: string[];
928
1938
  }
929
1939
  interface DashboardWidget {
930
1940
  /** Stable key within the document (section index). */
@@ -933,14 +1943,15 @@ interface DashboardWidget {
933
1943
  type: string;
934
1944
  spanCols?: number;
935
1945
  spanFull?: boolean;
936
- /** Raw block body (query + params). */
1946
+ /** Full fenced body — a Query Document (decision 22), comment lines
1947
+ * stripped, surrounding whitespace trimmed, line structure preserved. */
937
1948
  body: string;
938
1949
  /** Malformed fence-suffix reason — renderers badge instead of executing. */
939
1950
  widgetError?: string;
940
- /** WQL query after parameter split. */
941
- query: string;
942
- /** Positional parameters (literals or `$token` refs). */
943
- params: string[];
1951
+ /** Fence-tag presentation attributes (decision 22) — positional params
1952
+ * ride the tag, never the body. */
1953
+ attributes: Record<string, string>;
1954
+ attributeOrder: string[];
944
1955
  /** Title from the markdown heading directly above (or above the question). */
945
1956
  title?: string;
946
1957
  /** Coaching question from the paragraph directly above the block. */
@@ -1035,17 +2046,109 @@ declare function parseFlatProperties(innerContent: string): Record<string, strin
1035
2046
  */
1036
2047
  declare function parseFrontmatterProps(lines: string[]): Record<string, string>;
1037
2048
 
2049
+ /**
2050
+ * Dashboard note operations — pure markdown↔markdown transforms over the
2051
+ * locked dashboard-note format (#899). Every operation:
2052
+ *
2053
+ * - takes the note's raw content and returns the NEW raw content (the caller
2054
+ * persists through journalNotes, the only write path);
2055
+ * - is identity-guarded: the target widget is located by its positional key
2056
+ * (`w${index}`, matching buildDashboardDocument) AND its expected body
2057
+ * content. A guard mismatch returns null and writes NOTHING — a concurrent
2058
+ * edit or reorder between read and write can never retarget the splice to
2059
+ * the wrong widget.
2060
+ *
2061
+ * A widget's editable unit is its "group": the title heading / coaching
2062
+ * question paragraph the document builder associates with the block (strict
2063
+ * adjacency, same rule as buildDashboardDocument) plus the ```query fence
2064
+ * itself. Move/duplicate/remove operate on the whole group so the rendered
2065
+ * card — title, question, chart — travels together.
2066
+ */
2067
+ interface WidgetSpec {
2068
+ /** Card title — renders as a `## ` heading above the block. */
2069
+ title?: string;
2070
+ /** Coaching question — the paragraph between the heading and the block. */
2071
+ question?: string;
2072
+ /** Raw widget type from the fence suffix ('' → the `table` default). */
2073
+ type: string;
2074
+ spanCols?: number;
2075
+ spanFull?: boolean;
2076
+ /** The composed Query Document text — stored verbatim as the block body
2077
+ * (single-line or multi-line; decision 22). */
2078
+ wql: string;
2079
+ /** Fence-tag presentation attributes (goal targets, …). */
2080
+ attributes?: Record<string, string>;
2081
+ /** Attribute emission order (defaults to Object key order). */
2082
+ attributeOrder?: string[];
2083
+ }
2084
+ type WidgetSpan = Pick<WidgetSpec, 'spanCols' | 'spanFull'>;
2085
+ /** Opening fence line for a widget spec — type/span plus `key=value`
2086
+ * attributes (decision 22: presentation params ride the fence tag). */
2087
+ declare function widgetFenceTag(spec: Pick<WidgetSpec, 'type' | 'spanCols' | 'spanFull' | 'attributes' | 'attributeOrder'>): string;
2088
+ /** Block body: the Query Document text verbatim (trimmed). */
2089
+ declare function widgetBodyLine(spec: Pick<WidgetSpec, 'wql'>): string;
2090
+ /** The markdown lines of a whole widget group (heading? / question? / fence block). */
2091
+ declare function renderWidgetGroup(spec: WidgetSpec): string[];
2092
+ /**
2093
+ * Append a new widget group at the end of the note body. Frontmatter and all
2094
+ * existing content are preserved verbatim.
2095
+ */
2096
+ declare function appendWidget(raw: string, spec: WidgetSpec): string;
2097
+ /** Structured operation result (decision 22): a missing widget is distinct
2098
+ * from a concurrent edit, and neither silently no-ops. */
2099
+ type WidgetOpResult = {
2100
+ ok: true;
2101
+ note: string;
2102
+ } | {
2103
+ ok: false;
2104
+ reason: 'not-found' | 'stale-body';
2105
+ };
2106
+ /**
2107
+ * Replace a widget's whole group (title/question/type/body) in place.
2108
+ * Identity-guarded against the widget's exact full body text.
2109
+ */
2110
+ declare function updateWidget(raw: string, key: string, expectedBody: string, spec: WidgetSpec): WidgetOpResult;
2111
+ /** Duplicate a widget's group directly below itself. Identity-guarded. */
2112
+ declare function duplicateWidget(raw: string, key: string, expectedBody: string): WidgetOpResult;
2113
+ /**
2114
+ * Remove a widget's group (title/question/fence). Identity-guarded. Collapses
2115
+ * the blank-line gap the removal leaves behind.
2116
+ */
2117
+ declare function removeWidget(raw: string, key: string, expectedBody: string): WidgetOpResult;
2118
+ /**
2119
+ * Reorder a widget among the query blocks — the whole group (title, question,
2120
+ * block) moves past its neighbor. `delta` is -1 (up) or +1 (down); out-of-range
2121
+ * moves are no-ops that return the content unchanged. Identity-guarded.
2122
+ */
2123
+ declare function moveWidget(raw: string, key: string, expectedBody: string, delta: -1 | 1): WidgetOpResult;
2124
+ /**
2125
+ * Resize (or re-span) a widget: rewrites only the opening fence line, leaving
2126
+ * the body, params, and surrounding markdown untouched. `spanFull` wins over
2127
+ * `spanCols`. Identity-guarded.
2128
+ */
2129
+ declare function resizeWidget(raw: string, key: string, expectedBody: string, span: WidgetSpan): WidgetOpResult;
2130
+ /**
2131
+ * Rewrite one dashboard note's legacy widget bodies to Query Documents
2132
+ * (decision 22, answer 1): the first-` / ` positional split is retired.
2133
+ * For each query fence whose body carries ` / ` positional params, the
2134
+ * query text stays the body and the params move onto the fence tag as
2135
+ * numbered attributes (`param1=…`) — values preserved verbatim, losslessly
2136
+ * editable. A body already document-shaped is untouched; the note that
2137
+ * cannot be rewritten safely returns unchanged (the caller badges it).
2138
+ */
2139
+ declare function migrateLegacyDashboardBodies(raw: string): string;
2140
+
1038
2141
  /**
1039
2142
  * Auto-generated build version metadata.
1040
2143
  * Do not edit directly; updated by scripts/stamp-version.ts
1041
2144
  */
1042
- declare const VERSION = "0.11.43";
1043
- declare const GIT_SHA = "ed28149";
1044
- declare const BUILD_TIME = "2026-08-26T16:34:15.927Z";
2145
+ declare const VERSION = "0.35.2116";
2146
+ declare const GIT_SHA = "a4d411f";
2147
+ declare const BUILD_TIME = "2026-10-12T00:14:24.795Z";
1045
2148
  declare const SEMVER: {
1046
- readonly version: "0.11.43";
1047
- readonly gitSha: "ed28149";
1048
- readonly buildTime: "2026-08-26T16:34:15.927Z";
2149
+ readonly version: "0.35.2116";
2150
+ readonly gitSha: "a4d411f";
2151
+ readonly buildTime: "2026-10-12T00:14:24.795Z";
1049
2152
  };
1050
2153
 
1051
- export { type Aggregator, type AnyParsedQuery, BUILD_TIME, type BlockQueryStore, type ComparisonOp, DASHBOARD_GRID_MAX_COLS, DASHBOARD_WIDGET_TYPES, DEFAULT_DASHBOARD_TITLE, DISCIPLINE_FACTORS, type DashboardDocument, type DashboardMeta, type DashboardParsedSection, type DashboardSectionInput, type DashboardToken, type DashboardWidget, type DashboardWidgetType, EFFORT_DISCIPLINES, type EffortDiscipline, type EffortQueryStore, type EventRowIdentity, type FindOptions, type FindPredicate, type FindQueryResult, GIT_SHA, type IEffort, KG_PER_LB, type LinkWidget, type MetricPredicate, type NoteQueryStore, PLANNED_WIDGET_TYPES, type ParsedAggregateQuery, type ParsedFindQuery, type ParsedFrontmatter, type ParsedRowsQuery, type ParsedWqlRollupSuffix, type ParsedWqlSuffixes, type ParsedWqlWindowSuffix, type QueryOptions, type QueryResult, QueryService, type QueryServiceStores, type QueryWidgetSuffix, type QueryWindow, type RowsQueryResult, type RowsRun, SEMVER, type Series, type SeriesPoint, type SummaryFactIdentity, type SummaryFactSourceOutput, type TagFilter, type TagValue, UNIT_FAMILIES, type UnifiedEventStore, type UnitFamily, type UnitFamilyDef, VERSION, WQL_AGGREGATORS, WQL_CALC_TARGETS, WQL_COMPARISON_OPS, WQL_CONTENT_FILTER_KEYS, WQL_DISPLAY_UNITS, WQL_FIND_TARGETS, WQL_GRAINS, WQL_INTENSITY_TIERS, WQL_KEYWORDS, WQL_METRIC_AGGREGATES, WQL_METRIC_FAMILIES, WQL_RESULT_PLANES, WQL_ROLLUP_PERIODS, WQL_ROWS_SCOPE_KEYS, WQL_ROWS_TARGETS, WQL_SOURCES, WQL_SOURCE_VALUES, WQL_TAG_KEYS, WQL_VIRTUAL_DIMS, type WqlAggregator, type WqlCalcTarget, type WqlComparisonOp, type WqlCompletionOptions, type WqlContentFilterKey, type WqlDisplayUnit, type WqlFindTarget, type WqlGrain, type WqlIntensityTier, type WqlKeyword, type WqlMetricAggregate, type WqlMetricFamily, type WqlResultPlane, type WqlRollupPeriod, type WqlRowsTarget, type WqlSource, type WqlSourceValue, type WqlTagKey, type WqlVirtualDim, buildDashboardDocument, buildDashboardScaffold, convert, dashboardSlug, defaultTokenValues, disciplineFactorFor, extractDashboardTokens, extractFrontmatterTags, getList, getScalar, getUnitFamily, isAggregateQuery, isDashboardMeta, isDashboardWidgetType, isEffortDiscipline, isFindQuery, isProposedMetric, isRowsQuery, normalizeSummaryFacts, normalizeWql, parseDashboardNote, parseFlatProperties, parseFrontmatter, parseFrontmatterBody, parseFrontmatterCategories, parseFrontmatterProps, parseQuery, parseQueryWidgetSuffix, parseWqlSuffixes, projectEventToFacts, referencedTokens, resolveCanonicalMetricKey, resolveDisplayUnit, resolveWidgetType, serialize, serializeFrontmatter, setDashboardTokenValue, splitAtWhere, splitWidgetBody, staticNotesFromBlocks, staticTagIndexFromBlocks, stripFrontmatter, substituteTokens, toEventRows, toSummaryEventRows, unknownTokensMessage, unknownWidgetTypeMessage, wql, wqlCompletion, wqlCompletionSource, wqlHighlightStyle, wqlLanguage, parser as wqlParser, wql_parser_terms as wqlTerms };
2154
+ export { type Aggregator, type AnyParsedQuery, BUILD_TIME, type BlockQueryStore, CONTENT_GROUPING_DIMENSIONS, type CatalogFieldSuggestion, type CatalogValueSuggestion, type ComparisonOp, DASHBOARD_GRID_MAX_COLS, DASHBOARD_WIDGET_TYPES, DEFAULT_DASHBOARD_TITLE, DISCIPLINE_FACTORS, type DashboardDocument, type DashboardMeta, type DashboardParsedSection, type DashboardSectionInput, type DashboardToken, type DashboardWidget, type DashboardWidgetType, type DocumentAssignment, type DocumentDefaults, type DocumentFormula, type DocumentOutput, type DocumentResult, EFFORT_DISCIPLINES, type EffortDiscipline, type EffortQueryStore, type EventRowIdentity, type EventStore, type ExecutionContext, type FamilyRunPayload, type FindOptions, type FindPredicate, type FindQueryResult, type FormulaEvaluation, type FormulaEvaluator, GIT_SHA, type IEffort, type IFieldCatalog, type LinkWidget, type MetricPredicate, type NoteContainer, type NoteQueryStore, type NoteRelationBundle, type OperandValue, type OutputUnitResolution, PLANNED_WIDGET_TYPES, type PageSourceEntry, PageSourceRegistry, type ParsedAggregateQuery, type ParsedDocument, type ParsedFindQuery, type ParsedFrontmatter, type ParsedPipelineQuery, type ParsedRowsQuery, type ParsedWqlRollupSuffix, type ParsedWqlSuffixes, type ParsedWqlWindowSuffix, type PipelineOptions, type PipelineResult, type PipelineSink, type PipelineSource, type PointwiseInputs, type QueryDocument, QueryDocumentRunner$1 as QueryDocumentRunner, type QueryDocumentRunnerHost, type QueryDocumentRunnerOptions, type QueryOptions, type QueryResult, QueryService, type QueryServiceStores, type QueryWidgetSuffix, type QueryWindow, type ResolvedRange, type RowsPipes, type RowsQueryResult, type RowsRun, SEMVER, type Series, type SeriesPoint, QueryDocumentRunner as SharedQueryDocumentRunner, type SharedRunnerOptions, type SummaryFactIdentity, type SummaryFactSourceOutput, type TabularColumn, type TabularGroup, type TabularResult, type TagFilter, type TagValue, UNASSIGNED, type UnitBearingFact, type UnitFamily, VERSION, WQL_AGGREGATORS, WQL_CALC_TARGETS, WQL_CHART_HEADS, WQL_COMPARISON_OPS, WQL_CONTENT_FILTER_KEYS, WQL_CONTENT_ONLY_KEYS, WQL_DISPLAY_UNITS, WQL_EFFORT_FILTER_KEYS, WQL_FIND_TARGETS, WQL_FUNCTION_HEADS, WQL_GRAINS, WQL_INTENSITY_TIERS, WQL_KEYWORDS, WQL_METRIC_AGGREGATES, WQL_METRIC_FAMILIES, WQL_NOTE_DEFAULT_SOURCES, WQL_RESULT_PLANES, WQL_ROLLUP_PERIODS, WQL_SOURCES, WQL_SOURCE_HEADS, WQL_SOURCE_HEAD_SCOPES, WQL_SOURCE_VALUES, WQL_STANDARD_DATASETS, WQL_TAG_KEYS, WQL_TYPED_TAG_KEYS, WQL_TYPE_HEADS, WQL_VIRTUAL_DIMS, type WidgetOpResult, type WidgetSpan, type WidgetSpec, type WqlAggregator, type WqlCalcTarget, type WqlChartHead, type WqlComparisonOp, type WqlCompletionOptions, type WqlContentFilterKey, type WqlDisplayUnit, type WqlDomainOrder, type WqlDomainPredicate, type WqlDomainReadResult, type WqlFindTarget, type WqlFunctionHead, type WqlGrain, type WqlIntensityTier, type WqlKeyword, type WqlMetricAggregate, type WqlMetricFamily, type WqlResultPlane, type WqlRollupPeriod, type WqlSource, type WqlSourceHead, type WqlSourceValue, type WqlStandardDataset, type WqlTagKey, type WqlTypedTagKey, type WqlVirtualDim, allowedFilterTypesForTarget, appendSuffixes, appendWidget, buildDashboardDocument, buildDashboardScaffold, buildDrillDownQuery, captureContext, catalogOfItem, civilDateAdd, civilDateDiff, civilDateOf, civilDatesCoveredByRange, civilMonday, consumesRollupFacts, convertDisplay, createBuiltinFormulaEvaluator, dashboardSlug, defaultTokenValues, disciplineFactorFor, duplicateWidget, eventsToStoredLogs, extractDashboardTokens, extractFrontmatterTags, findTargetAdvisories, getList, getScalar, getUnitFamily, inRange, isAggregateQuery, isDashboardMeta, isDashboardWidgetType, isEffortDiscipline, isFindQuery, isPipelineQuery, isProposedMetric, migrateLegacyDashboardBodies, moveWidget, normalizeSummaryFacts, normalizeWql, parseDashboardNote, parseDocument, parseFlatProperties, parseFrontmatter, parseFrontmatterBody, parseFrontmatterCategories, parseFrontmatterProps, parseQuery, parseQueryWidgetSuffix, parseWqlSuffixes, projectEventToFacts, referencedTokens, removeWidget, renderWidgetGroup, resizeWidget, resolveCanonicalMetricKey, resolveOutputUnit, resolveWidgetType, resolveWindowRange, rowsParseError, serialize, serializeDocument, serializeFrontmatter, setDashboardTokenValue, sourceMatches, splitAtWhere, staticCatalogRoute, staticNotesFromBlocks, staticTagIndexFromBlocks, stripFrontmatter, substituteTokens, substituteTokens$1 as substituteTokensRaw, supportsWqlFilterKey, systemTimeZone, toEventRows, toSummaryEventRows, unknownTokensMessage, unknownWidgetTypeMessage, updateWidget, widgetBodyLine, widgetFenceTag, wql, wqlCompletion, wqlCompletionSource, wqlFilterKeys, wqlGroupingDimensions, wqlHighlightStyle, wqlLanguage, parser as wqlParser, wql_parser_terms as wqlTerms, zonedNoon, zonedStartOfDay };