@bitcobblers/wod-wiki-wql 0.44.2159 → 0.45.2166

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -31,7 +31,7 @@ var WQL_TAG_KEYS = [
31
31
  "result",
32
32
  "tags"
33
33
  ];
34
- var WQL_CONTENT_ONLY_KEYS = ["type", "text", "has", "source", "catalog", "notebook"];
34
+ var WQL_CONTENT_ONLY_KEYS = ["type", "text", "source", "notebook", "contributes"];
35
35
  var WQL_EFFORT_FILTER_KEYS = ["effort", "discipline", "intensity", "origin", "text"];
36
36
  var WQL_VIRTUAL_DIMS = ["day", "week", "session", "round"];
37
37
  var WQL_CALC_TARGETS = [
@@ -59,8 +59,8 @@ var WQL_CALC_TARGETS = [
59
59
  ];
60
60
  var WQL_INTENSITY_TIERS = ["low", "moderate", "high"];
61
61
  var WQL_GRAINS = ["summary", "event"];
62
- var WQL_FIND_TARGETS = ["note", "block", "effort", "session", "segment", "event"];
63
- var WQL_SOURCE_VALUES = ["journal", "feeds", "guides", "playground", "dashboards", "efforts", "equipment"];
62
+ var WQL_FIND_TARGETS = ["note", "block", "effort", "session", "segment", "event", "notebook", "page", "tag", "equipment"];
63
+ var WQL_SOURCE_VALUES = ["journal", "notebooks", "guides", "playground", "dashboards", "efforts", "equipment"];
64
64
  var WQL_SOURCE_HEADS = [
65
65
  "note",
66
66
  "notes",
@@ -72,23 +72,18 @@ var WQL_SOURCE_HEADS = [
72
72
  "journal",
73
73
  "notebook",
74
74
  "notebooks",
75
- "feed",
76
- "feeds",
77
- "catalog",
78
- "catalogs",
79
75
  "playground",
76
+ "page",
77
+ "tag",
78
+ "equipment",
80
79
  "dashboard"
81
80
  ];
82
81
  var WQL_SOURCE_HEAD_SCOPES = {
83
82
  journal: "journal",
84
- notebook: "feeds",
85
- notebooks: "feeds",
86
- feed: "feeds",
87
- feeds: "feeds",
88
- catalog: "feeds",
89
- catalogs: "feeds",
83
+ notebooks: "notebooks",
90
84
  playground: "playground"
91
85
  };
86
+ var WQL_NOTE_HEAD_ALIASES = ["notes"];
92
87
  var WQL_TYPE_HEADS = {
93
88
  dashboard: "dashboard"
94
89
  };
@@ -97,8 +92,8 @@ var WQL_FUNCTION_HEADS = WQL_AGGREGATORS;
97
92
  var WQL_CHART_HEADS = ["timeseries", "bar", "table", "donut", "toplist", "value"];
98
93
  var WQL_STANDARD_DATASETS = ["@session", "@today"];
99
94
  var WQL_RESULT_PLANES = wodWikiCore.KNOWN_OUTPUT_TYPES;
100
- var WQL_CONTENT_FILTER_KEYS = ["type", "text", "has", "source", "catalog", "notebook", ...WQL_TAG_KEYS];
101
- var WQL_SOURCES = ["note", "block", "effort", "session", "segment", "event"];
95
+ var WQL_CONTENT_FILTER_KEYS = ["type", "text", "source", "notebook", "page", "contributes", ...WQL_TAG_KEYS];
96
+ var WQL_SOURCES = WQL_FIND_TARGETS;
102
97
  var WQL_ROLLUP_PERIODS = ["2w", "4w"];
103
98
  var WQL_DISPLAY_UNITS = ["kg", "lb"];
104
99
  var WQL_KEYWORDS = [
@@ -134,12 +129,25 @@ var AGGREGATE_TYPES = /* @__PURE__ */ new Set([
134
129
  "metric"
135
130
  ]);
136
131
  var TARGET_TYPES = {
137
- note: /* @__PURE__ */ new Set(["text", "catalog", "notebook", "tag", "effort", ...WQL_TYPED_TAG_KEYS, "type", "page", "source", "note", "time", "groupby", "where"]),
138
- block: /* @__PURE__ */ new Set(["text", "catalog", "notebook", "tag", "effort", "type", "source", "note", "time", "groupby", "where"]),
132
+ // The notebook plane IS the note plane (its executor delegates), so both
133
+ // advertise the same member-criteria keys — container criteria (tags:,
134
+ // effort:, typed tags, text:) narrow notebook results, never ignored.
135
+ notebook: /* @__PURE__ */ new Set(["text", "notebook", "tag", "effort", ...WQL_TYPED_TAG_KEYS, "type", "page", "source", "note", "time", "groupby", "where"]),
136
+ note: /* @__PURE__ */ new Set(["text", "notebook", "tag", "effort", ...WQL_TYPED_TAG_KEYS, "type", "page", "source", "note", "time", "groupby", "where"]),
137
+ block: /* @__PURE__ */ new Set(["text", "notebook", "tag", "effort", "type", "source", "note", "time", "groupby", "where"]),
139
138
  effort: /* @__PURE__ */ new Set([...WQL_EFFORT_FILTER_KEYS, "groupby"]),
140
139
  session: /* @__PURE__ */ new Set(["result", "block", "note", "plane", "time", "groupby"]),
141
140
  segment: /* @__PURE__ */ new Set([...TABLE_KEYS, "time", "groupby", "unit"]),
142
- event: /* @__PURE__ */ new Set([...TABLE_KEYS, "time", "groupby", "unit"])
141
+ event: /* @__PURE__ */ new Set([...TABLE_KEYS, "time", "groupby", "unit"]),
142
+ // Container planes (ticket 08). `:notebook{}` delegates to the note plane
143
+ // (see the notebook entry above); `:page{}` lists the page placements;
144
+ // `:tag{}` the tags (results are notes); `:equipment{}` the equipment
145
+ // notes. The tag plane's `contributes:` filter matches what a tag
146
+ // contributes — never offered as a `tags:` label filter. These executors
147
+ // return flat rows (no `by {}` grouping), so groupby stays unadvertised.
148
+ page: /* @__PURE__ */ new Set(["page", "notebook", "text", "note", "time"]),
149
+ tag: /* @__PURE__ */ new Set(["tag", "type", "text", "contributes", "time"]),
150
+ equipment: /* @__PURE__ */ new Set(["equipment", "tags", "text", "notebook", "time"])
143
151
  };
144
152
  var EMPTY_TYPES = /* @__PURE__ */ new Set();
145
153
  var AGGREGATE_KEYS = WQL_TAG_KEYS;
@@ -654,18 +662,24 @@ function validateSourceFilter(filters) {
654
662
  for (const v of f.values) {
655
663
  const val = v.value;
656
664
  if (val === "page" || val === "pages") {
657
- return `source:${val} is retired \u2014 page-ness is handled by type: (e.g. type:feed or source:guides).`;
665
+ return `source:${val} is retired \u2014 page-ness is handled by type:page or source:guides.`;
658
666
  }
659
667
  if (val === "all") {
660
668
  return `source:all is retired \u2014 omit the source: filter to query all sources.`;
661
669
  }
662
- if (val === "collection" || val === "collections") {
663
- return `source:${val} is retired \u2014 collections were renamed to feeds; use source:feeds or :feed{}.`;
670
+ if (val === "notebook") {
671
+ return `source:notebook is retired \u2014 :notebook{} is the notebook plane (every notebook); narrow it with the notebook: filter, e.g. :notebook{notebook:journal}.`;
672
+ }
673
+ if (val === "feed" || val === "feeds" || val.startsWith("feed:")) {
674
+ return `source:${val} is retired \u2014 the feed corpus is the notebook plane; use source:notebooks or notebook:<slug>.`;
675
+ }
676
+ if (val === "collection" || val === "collections" || val.startsWith("collection:")) {
677
+ return `source:${val} is retired \u2014 collections are notebooks; use source:notebooks or notebook:<slug>.`;
664
678
  }
665
- if (WQL_SOURCE_VALUES.includes(val) || val === "feed" || val === "notebook" || val === "notebooks" || val.startsWith("collection:") || val.startsWith("feed:") || val.startsWith("notebook:") || val.startsWith("notebook/")) {
679
+ if (WQL_SOURCE_VALUES.includes(val) || val.startsWith("notebook:") || val.startsWith("notebook/")) {
666
680
  continue;
667
681
  }
668
- return `Unknown source "${val}". Try: ${WQL_SOURCE_VALUES.join(", ")} (or collection:<id>, feed:<id>)`;
682
+ return `Unknown source "${val}". Try: ${WQL_SOURCE_VALUES.join(", ")} (or notebook:<slug>)`;
669
683
  }
670
684
  }
671
685
  return void 0;
@@ -948,11 +962,7 @@ function parseFindQuery(raw, opts) {
948
962
  if (scope || typeScope) {
949
963
  result.target = "note";
950
964
  } else {
951
- result.target = headName === "notes" ? "note" : headName;
952
- if (result.target === "page") {
953
- result.error = "find:page is retired \u2014 page-ness is handled by type: (e.g. :note{source:guides} or type:feed).";
954
- return result;
955
- }
965
+ result.target = WQL_NOTE_HEAD_ALIASES.includes(headName) ? "note" : headName;
956
966
  if (!WQL_FIND_TARGETS.includes(result.target)) {
957
967
  result.error = `Unknown find target "${result.target}". Try: ${WQL_FIND_TARGETS.join(", ")}`;
958
968
  return result;
@@ -972,13 +982,6 @@ function parseFindQuery(raw, opts) {
972
982
  negate: false,
973
983
  values: [{ value: scope, wildcard: false }]
974
984
  });
975
- if (headName === "catalog" || headName === "catalogs" || headName === "notebook" || headName === "notebooks") {
976
- result.filters.push({
977
- key: "type",
978
- negate: false,
979
- values: [{ value: "feed", wildcard: false }]
980
- });
981
- }
982
985
  }
983
986
  const inScope = opts?.colon && result.displayUnit && result.displayUnit !== "kg" && result.displayUnit !== "lb" ? result.displayUnit : legacyScope;
984
987
  if (opts?.colon && inScope) {
@@ -1487,10 +1490,12 @@ function chained(list, suffix) {
1487
1490
  ...c,
1488
1491
  apply: (view, _c, from, to) => {
1489
1492
  const text = typeof c.apply === "string" ? c.apply : c.label;
1493
+ const selfTerminated = /\W$/.test(text);
1490
1494
  const present = view.state.sliceDoc(to, to + suffix.length) === suffix;
1495
+ const insert = selfTerminated || present ? text : text + suffix;
1491
1496
  view.dispatch({
1492
- changes: { from, to, insert: present ? text : text + suffix },
1493
- selection: { anchor: from + text.length + suffix.length },
1497
+ changes: { from, to: to + (selfTerminated && present ? suffix.length : 0), insert },
1498
+ selection: { anchor: from + insert.length },
1494
1499
  userEvent: "input.complete"
1495
1500
  });
1496
1501
  autocomplete.startCompletion(view);
@@ -1508,7 +1513,7 @@ function headApply(o, colonTyped) {
1508
1513
  return { ...o, apply: `${colonTyped ? "" : ":"}${o.label}{` };
1509
1514
  }
1510
1515
  var LEGACY_HEAD_OPTIONS = WQL_AGGREGATORS.map((label) => ({ label, type: "keyword" }));
1511
- var NOTE_COLUMNS = ["title", "date", "createdAt", "type", "sourceId", "catalog", "notebookId"];
1516
+ var NOTE_COLUMNS = ["title", "date", "createdAt", "type", "sourceId", "notebookId"];
1512
1517
  var BLOCK_COLUMNS = ["noteTitle", "dataType", "position", "createdAt", "noteId", "sourceId"];
1513
1518
  var EFFORT_COLUMNS = ["slug", "label", "registrySource"];
1514
1519
  var SESSION_COLUMNS = ["duration", "startTime", "origin"];
@@ -1536,7 +1541,7 @@ function resolveColonHead(name) {
1536
1541
  if (WQL_FUNCTION_HEADS.includes(n)) return { family: "aggregate" };
1537
1542
  if (WQL_CHART_HEADS.includes(n)) return { family: "find" };
1538
1543
  const headTarget = (head) => {
1539
- if (head === "notes") return "note";
1544
+ if (WQL_NOTE_HEAD_ALIASES.includes(head)) return "note";
1540
1545
  const scoped = WQL_SOURCE_HEAD_SCOPES[head] || WQL_TYPE_HEADS[head];
1541
1546
  return scoped ? "note" : head;
1542
1547
  };
@@ -1705,12 +1710,17 @@ function wqlCompletionSource(options_ = {}) {
1705
1710
  if (/\s+to\s+$/i.test(tail)) {
1706
1711
  return finish(context, context.pos, [{ label: "2026-03-31", detail: "end date (YYYY-MM-DD)", type: "constant" }], "");
1707
1712
  }
1713
+ const hasBy = /\bby\s*\{[^{}]*\}/.test(tail);
1714
+ const hasRollup = /\.rollup\([^()]*\)/.test(tail);
1715
+ const hasWindow = /\s+last\s+\d+[dw]\b/i.test(tail) || /\s+from\s+\d{4}-\d{2}-\d{2}(?:\s+to\s+\d{4}-\d{2}-\d{2})?\b/i.test(tail);
1708
1716
  const dims = wqlGroupingDimensions(head.target ?? "", head.family);
1709
1717
  const keywords = [
1710
- { label: "by {}", apply: "by ", detail: dims.length ? "group by dimensions" : `advisory: no grouping dimensions for find:${head.target}`, type: "keyword" },
1711
- ...head.family === "aggregate" ? [{ label: ".rollup()", apply: ".rollup(", detail: "bucket period", type: "keyword" }] : [],
1712
- { label: "last", apply: "last ", detail: "relative window: last <n>d|w", type: "keyword" },
1713
- { label: "from", apply: "from ", detail: "date range: from YYYY-MM-DD [to YYYY-MM-DD]", type: "keyword" },
1718
+ ...hasBy ? [] : [{ label: "by {}", apply: "by ", detail: dims.length ? "group by dimensions" : `advisory: no grouping dimensions for find:${head.target}`, type: "keyword" }],
1719
+ ...head.family === "aggregate" && !hasRollup ? [{ label: ".rollup()", apply: ".rollup(", detail: "bucket period", type: "keyword" }] : [],
1720
+ ...hasWindow ? [] : [
1721
+ { label: "last", apply: "last ", detail: "relative window: last <n>d|w", type: "keyword" },
1722
+ { label: "from", apply: "from ", detail: "date range: from YYYY-MM-DD [to YYYY-MM-DD]", type: "keyword" }
1723
+ ],
1714
1724
  ...head.family === "find" ? [{ label: "|", apply: "| ", detail: "presentation pipes: order by / select / limit", type: "keyword" }] : []
1715
1725
  ];
1716
1726
  return finish(context, from, chained(keywords, " "), typed);
@@ -2276,6 +2286,7 @@ var QueryDocumentRunner = class {
2276
2286
  ...findRun.blocks ? { blocks: findRun.blocks } : {},
2277
2287
  ...findRun.efforts ? { efforts: findRun.efforts } : {},
2278
2288
  ...findRun.containers ? { containers: findRun.containers } : {},
2289
+ ...findRun.placements ? { placements: findRun.placements } : {},
2279
2290
  ...findRun.runs ? { runs: findRun.runs } : {},
2280
2291
  ...findRun.table ? { table: findRun.table } : {},
2281
2292
  ...findRun.unit ? { unit: findRun.unit } : {},
@@ -2306,6 +2317,7 @@ var QueryDocumentRunner = class {
2306
2317
  ...pipelineRun.blocks ? { blocks: pipelineRun.blocks } : {},
2307
2318
  ...pipelineRun.efforts ? { efforts: pipelineRun.efforts } : {},
2308
2319
  ...pipelineRun.containers ? { containers: pipelineRun.containers } : {},
2320
+ ...pipelineRun.placements ? { placements: pipelineRun.placements } : {},
2309
2321
  ...pipelineRun.runs ? { runs: pipelineRun.runs } : {},
2310
2322
  ...pipelineRun.unit ? { unit: pipelineRun.unit } : {},
2311
2323
  ...pipelineRun.chart ? { chart: pipelineRun.chart } : {},
@@ -2912,16 +2924,18 @@ function dedupeById(facts) {
2912
2924
  }
2913
2925
  return out;
2914
2926
  }
2927
+
2928
+ // src/QueryService.ts
2915
2929
  var NOTE_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2916
- function catalogOfItem(item) {
2917
- if (item.catalog) return item.catalog;
2918
- const isCollectionOrFeed = !!item.sourceId && /^(collection|feed):/.test(item.sourceId);
2919
- const raw = isCollectionOrFeed ? item.sourceId.replace(/^(collection|feed):/, "") : item.noteId || item.id || "";
2920
- if (!raw) return void 0;
2921
- if (NOTE_UUID_RE.test(raw)) return void 0;
2922
- const clean = raw.startsWith("feeds/") ? raw.slice("feeds/".length) : raw;
2923
- if (!isCollectionOrFeed && !clean.includes("/")) return void 0;
2924
- return clean.split("/")[0];
2930
+ function notebookSlugOfSourceId(sourceId) {
2931
+ if (!sourceId?.startsWith("notebook:")) return void 0;
2932
+ const rest = sourceId.slice("notebook:".length);
2933
+ if (!rest || rest === "_root") return void 0;
2934
+ return rest.split("/")[0];
2935
+ }
2936
+ function feedOfItem(item) {
2937
+ const id = item.feedSourceId;
2938
+ return typeof id === "string" && id.trim() ? id : void 0;
2925
2939
  }
2926
2940
  function notebookOfItem(item) {
2927
2941
  if (item.notebookId && item.notebookId.trim()) {
@@ -2930,46 +2944,75 @@ function notebookOfItem(item) {
2930
2944
  if (item.tags) {
2931
2945
  for (const tag of item.tags) {
2932
2946
  if (tag.startsWith("notebook:") || tag.startsWith("notebook/")) {
2933
- const slug = wodWikiCore.notebookSlugFromId(tag);
2934
- if (slug) return wodWikiCore.toNotebookId(slug);
2947
+ const slug2 = wodWikiCore.notebookSlugFromId(tag);
2948
+ if (slug2) return wodWikiCore.toNotebookId(slug2);
2935
2949
  }
2936
2950
  }
2937
2951
  }
2952
+ const corpus = /^markdown\/(notebooks|canvas|efforts|dashboards|equipment)\/(.+)$/.exec(item.sourcePath ?? "");
2953
+ if (corpus) {
2954
+ const [, family, rest] = corpus;
2955
+ if (family === "canvas") return "notebook/guides";
2956
+ if (family !== "notebooks") return wodWikiCore.toNotebookId(family);
2957
+ const segs = rest.split("/");
2958
+ if (segs.length > 1) return wodWikiCore.toNotebookId(segs[0]);
2959
+ }
2938
2960
  if (sourceMatches(item, "playground")) return "notebook/playground";
2939
2961
  if (sourceMatches(item, "guides")) return "notebook/guides";
2940
- const cat = catalogOfItem(item);
2941
- if (cat) return wodWikiCore.toNotebookId(cat);
2942
- if (sourceMatches(item, "journal")) return "notebook/journal";
2962
+ if (item.sourceId === "efforts") return "notebook/efforts";
2963
+ if (item.sourceId === "equipment") return "notebook/equipment";
2964
+ if (item.sourceId === "dashboards" || item.type === "dashboard") return "notebook/dashboards";
2965
+ const slug = notebookSlugOfSourceId(item.sourceId);
2966
+ if (slug) return wodWikiCore.toNotebookId(slug);
2967
+ if (isJournalRow(item)) return "notebook/journal";
2943
2968
  return void 0;
2944
2969
  }
2970
+ function notebookOriginOfItem(item) {
2971
+ const notebook = notebookOfItem(item);
2972
+ if (!notebook) return void 0;
2973
+ if (item.notebookId?.trim()) return "user";
2974
+ if (item.tags?.some((t2) => t2.startsWith("notebook:") || t2.startsWith("notebook/"))) return "user";
2975
+ if (notebook === "notebook/journal" || notebook === "notebook/playground") return "default";
2976
+ return "feed";
2977
+ }
2978
+ function isJournalRow(item) {
2979
+ const sourceId = item.sourceId;
2980
+ if (item.type === "playground" || item.id?.startsWith("playground/") || item.noteId?.startsWith("playground/")) return false;
2981
+ if (sourceId && sourceId !== "journal" && !NOTE_UUID_RE.test(sourceId) && !/^journal\/\d{4}-\d{2}-\d{2}$/.test(sourceId)) return false;
2982
+ return item.type !== "dashboard" && item.type !== "effort" && item.type !== "equipment" && item.seedOrigin !== "seed" && item.feedOrigin !== "feed" && item.seedChunkId === void 0 && !(!!item.sourcePath && item.sourcePath.startsWith("markdown/"));
2983
+ }
2984
+ var SOURCE_ID_SHAPES = {
2985
+ notebooks: { prefixes: ["notebook:"] },
2986
+ pages: { exact: ["page"], prefixes: ["page:", "guides:"] },
2987
+ guides: { prefixes: ["guides:"] },
2988
+ playground: { exact: ["playground"], legacyType: "playground" },
2989
+ efforts: { exact: ["efforts"] },
2990
+ equipment: { exact: ["equipment"] },
2991
+ dashboards: { exact: ["dashboards"] }
2992
+ };
2993
+ var SOURCE_SHAPE_ALIASES = {
2994
+ page: "pages",
2995
+ guide: "guides"
2996
+ };
2997
+ function sourceShape(kind) {
2998
+ return SOURCE_ID_SHAPES[SOURCE_SHAPE_ALIASES[kind] ?? kind];
2999
+ }
2945
3000
  function sourceMatches(item, kind) {
2946
3001
  const sourceId = item.sourceId;
2947
3002
  if (kind === "all") return !sourceId || WQL_SOURCE_VALUES.some((k) => sourceMatches(item, k));
2948
3003
  if (kind === "journal") {
2949
- if (item.type === "playground" || item.id?.startsWith("playground/") || item.noteId?.startsWith("playground/")) return false;
2950
- if (sourceId && sourceId !== "journal" && !NOTE_UUID_RE.test(sourceId) && !/^journal\/\d{4}-\d{2}-\d{2}$/.test(sourceId)) return false;
2951
- return item.type !== "dashboard" && item.type !== "effort" && item.type !== "equipment" && item.seedOrigin !== "seed" && item.seedChunkId === void 0 && !item.catalog && !(!!item.sourcePath && item.sourcePath.startsWith("markdown/"));
3004
+ return notebookOfItem(item) === "notebook/journal";
2952
3005
  }
2953
- if (kind === "feed" || kind === "feeds" || kind === "notebook" || kind === "notebooks") {
2954
- return !!sourceId && (sourceId.startsWith("collection:") || sourceId.startsWith("page:collection:"));
2955
- }
2956
- if (kind === "page" || kind === "pages") {
2957
- return !!sourceId && (sourceId === "page" || sourceId.startsWith("page:") || sourceId.startsWith("guides:"));
2958
- }
2959
- if (kind === "guide" || kind === "guides") {
2960
- return !!sourceId && sourceId.startsWith("guides:");
2961
- }
2962
- if (kind === "playground") {
2963
- return sourceId === "playground" || item.type === "playground";
2964
- }
2965
- if (kind.startsWith("feed:")) {
2966
- const rest = kind.slice("feed:".length).replace(/^feeds\//, "");
2967
- if (!rest || !sourceId?.startsWith("feed:")) return false;
2968
- const idRest = sourceId.slice("feed:".length).replace(/^feeds\//, "");
2969
- return idRest === rest || idRest.startsWith(`${rest}/`);
3006
+ const shape = sourceShape(kind);
3007
+ if (shape) {
3008
+ const idHit = !!sourceId && (shape.exact?.includes(sourceId) || shape.prefixes?.some((p) => sourceId.startsWith(p)) === true);
3009
+ return idHit || !!shape.legacyType && item.type === shape.legacyType;
2970
3010
  }
2971
3011
  if (kind.startsWith("notebook:") || kind.startsWith("notebook/")) {
2972
- return notebookOfItem(item) === wodWikiCore.toNotebookId(kind);
3012
+ const rest = wodWikiCore.notebookSlugFromId(kind);
3013
+ const idRest = sourceId?.startsWith("notebook:") ? sourceId.slice("notebook:".length) : void 0;
3014
+ if (!idRest) return notebookOfItem(item) === wodWikiCore.toNotebookId(kind);
3015
+ return idRest === rest || idRest.startsWith(`${rest}/`);
2973
3016
  }
2974
3017
  return sourceId === kind;
2975
3018
  }
@@ -2998,8 +3041,7 @@ function applyNotebookFilter(items, filters) {
2998
3041
  return items;
2999
3042
  }
3000
3043
  var DOMAIN_SOURCE_KINDS = {
3001
- feed: true,
3002
- feeds: true,
3044
+ notebooks: true,
3003
3045
  page: true,
3004
3046
  pages: true,
3005
3047
  guide: true,
@@ -3025,6 +3067,10 @@ function compileContentRead(filters, plane, sourceScope) {
3025
3067
  residual = true;
3026
3068
  continue;
3027
3069
  }
3070
+ if (plane === "note" && WQL_TYPED_TAG_KEYS.includes(filter.key) && literal) {
3071
+ compiled.push({ field: "typedTag", tagType: filter.key, values });
3072
+ continue;
3073
+ }
3028
3074
  if (filter.key === "text" && literal) {
3029
3075
  compiled.push({ field: "text", value: values.join(" ") });
3030
3076
  continue;
@@ -3687,9 +3733,17 @@ var QueryService = class {
3687
3733
  if (parsed.target === "segment" || parsed.target === "event") {
3688
3734
  return this.runFindTable(parsed, options2);
3689
3735
  }
3690
- const isCatalogHead = parsed.target === "note" && parsed.filters.some((f) => f.key === "type" && !f.negate && f.values.some((v) => v.value === "feed")) && (parsed.filters.some((f) => f.key === "source" && !f.negate && f.values.some((v) => v.value === "feed" || v.value === "feeds" || v.value === "notebook" || v.value === "notebooks")) || parsed.sourceScope?.includes("feeds") || parsed.sourceScope?.includes("notebooks"));
3691
- if (isCatalogHead) {
3692
- return this.runFindCatalog(parsed, options2, await this.noteStore.getAllNotes());
3736
+ if (parsed.target === "notebook") {
3737
+ return this.runFindNotebook(parsed, options2);
3738
+ }
3739
+ if (parsed.target === "page") {
3740
+ return this.runFindPage(parsed, options2);
3741
+ }
3742
+ if (parsed.target === "tag") {
3743
+ return this.runFindTag(parsed, options2);
3744
+ }
3745
+ if (parsed.target === "equipment") {
3746
+ return this.runFindEquipment(parsed, options2);
3693
3747
  }
3694
3748
  const read = compileContentRead(parsed.filters, "note", parsed.sourceScope);
3695
3749
  const paging = domainPaging(parsed, options2, read.residual);
@@ -3709,14 +3763,14 @@ var QueryService = class {
3709
3763
  const selectedCount = selectedFromDomain ?? notes.length;
3710
3764
  const ctx = runContext(options2);
3711
3765
  for (const filter of parsed.filters) {
3712
- if (filter.key === "tags" && !filter.negate) {
3766
+ if (isTagLabelKey(filter.key) && !filter.negate) {
3713
3767
  const matchingIds = /* @__PURE__ */ new Set();
3714
3768
  for (const v of filter.values) {
3715
3769
  const ids = await this.noteStore.getNoteIdsForTag(v.value);
3716
3770
  ids.forEach((id) => matchingIds.add(id));
3717
3771
  }
3718
3772
  notes = notes.filter((n) => matchingIds.has(n.id));
3719
- } else if (filter.key === "tags" && filter.negate) {
3773
+ } else if (isTagLabelKey(filter.key) && filter.negate) {
3720
3774
  for (const v of filter.values) {
3721
3775
  const ids = await this.noteStore.getNoteIdsForTag(v.value);
3722
3776
  notes = notes.filter((n) => !ids.has(n.id));
@@ -3759,17 +3813,6 @@ var QueryService = class {
3759
3813
  notes = notes.filter((n) => wantsPage ? n.type === "page" : n.type !== "page");
3760
3814
  }
3761
3815
  }
3762
- for (const filter of parsed.filters) {
3763
- if (filter.key === "catalog") {
3764
- const wanted = new Set(filter.values.map((v) => v.value));
3765
- notes = notes.filter((n) => {
3766
- const cat = catalogOfItem(n);
3767
- if (!cat) return filter.negate;
3768
- const hit = wanted.has(cat);
3769
- return filter.negate ? !hit : hit;
3770
- });
3771
- }
3772
- }
3773
3816
  notes = applyNotebookFilter(notes, parsed.filters);
3774
3817
  for (const filter of parsed.filters) {
3775
3818
  if (filter.key === "note") {
@@ -3800,46 +3843,56 @@ var QueryService = class {
3800
3843
  const offset = pipes.offset ?? 0;
3801
3844
  notes = pipes.limit !== void 0 ? notes.slice(offset, offset + pipes.limit) : notes.slice(offset);
3802
3845
  }
3846
+ const relations = notes.length && (this.pageStore || this.relationStore) ? await this.containersAndPlacements(notes) : void 0;
3803
3847
  return {
3804
3848
  parsed,
3805
3849
  notes,
3806
3850
  blocks: [],
3807
- ...notes.length && (this.pageStore || this.relationStore) ? { containers: await this.containersForNotes(notes) } : {},
3851
+ ...relations ? { containers: relations.containers, placements: relations.placements } : {},
3808
3852
  stages: { selected: selectedCount, matched: notes.length }
3809
3853
  };
3810
3854
  }
3811
- /** Highest container per note id: the parent Page from the canonical
3812
- * page_notes junction (first link by position) wins; a note with no page
3813
- * link stays a standalone Note container. Batched when a relationStore
3814
- * is injected — one bundle read per run instead of a link/page read per
3815
- * matched note. */
3816
- async containersForNotes(notes) {
3855
+ /** The container projection and the full placement set per note id.
3856
+ *
3857
+ * The container is the parent Page from the canonical page_notes junction
3858
+ * (the primary placement wins); a note with no page link stays a
3859
+ * standalone Note container. Batched when a relationStore is injected —
3860
+ * one bundle read per run instead of a link/page read per matched note.
3861
+ *
3862
+ * The same read resolves EVERY page the note is placed on: the junction
3863
+ * is many-to-many, so the container is a projection of that set, not the
3864
+ * set itself. */
3865
+ async containersAndPlacements(notes) {
3817
3866
  const containers = {};
3867
+ const placements = {};
3818
3868
  const bundles = this.relationStore ? await this.relationStore.getBatches([...new Set(notes.map((n) => n.id))]) : void 0;
3819
3869
  const pageCache = /* @__PURE__ */ new Map();
3820
3870
  for (const note of notes) {
3821
- let page;
3822
3871
  let bundle;
3872
+ let resolved;
3823
3873
  if (bundles) {
3824
3874
  bundle = bundles.get(note.id);
3825
- const [first] = [...bundle?.links ?? []].sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
3826
- page = first ? bundle.pages.find((p) => p.id === first.pageId) : void 0;
3875
+ resolved = wodWikiCore.resolvePlacements(bundle?.links ?? [], bundle?.pages ?? []);
3827
3876
  if (bundle && bundle.tags.length > 0 && (!note.tags || note.tags.length === 0)) {
3828
3877
  note.tags = bundle.tags.map((t2) => t2.label);
3829
3878
  }
3830
3879
  } else {
3831
3880
  const links = await this.pageStore.getNotePages(note.id);
3832
- if (links.length > 0) {
3833
- const [first] = [...links].sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
3834
- if (!pageCache.has(first.pageId)) {
3835
- pageCache.set(first.pageId, await this.pageStore.getPage(first.pageId));
3881
+ const pages = [];
3882
+ for (const link of links) {
3883
+ if (!pageCache.has(link.pageId)) {
3884
+ pageCache.set(link.pageId, await this.pageStore.getPage(link.pageId));
3836
3885
  }
3837
- page = pageCache.get(first.pageId);
3886
+ const page = pageCache.get(link.pageId);
3887
+ if (page) pages.push(page);
3838
3888
  }
3889
+ resolved = wodWikiCore.resolvePlacements(links, pages);
3839
3890
  }
3840
- containers[note.id] = page ? { kind: "page", id: page.id, ...page.slug ? { slug: page.slug } : {}, ...page.date ? { date: page.date } : {} } : { kind: "note", id: note.id };
3891
+ placements[note.id] = resolved;
3892
+ const primary = resolved[0];
3893
+ containers[note.id] = primary ? { kind: "page", id: primary.pageId, ...primary.slug ? { slug: primary.slug } : {}, ...primary.date ? { date: primary.date } : {} } : { kind: "note", id: note.id };
3841
3894
  }
3842
- return containers;
3895
+ return { containers, placements };
3843
3896
  }
3844
3897
  async hydrateBlocks(rows) {
3845
3898
  const ids = [...new Set(rows.map((row) => row.noteId))];
@@ -3898,20 +3951,9 @@ var QueryService = class {
3898
3951
  blocks = blocks.filter((b) => wanted.has(b.dataType));
3899
3952
  }
3900
3953
  }
3901
- for (const filter of parsed.filters) {
3902
- if (filter.key === "catalog") {
3903
- const wanted = new Set(filter.values.map((v) => v.value));
3904
- blocks = blocks.filter((b) => {
3905
- const cat = catalogOfItem(b);
3906
- if (!cat) return filter.negate;
3907
- const hit = wanted.has(cat);
3908
- return filter.negate ? !hit : hit;
3909
- });
3910
- }
3911
- }
3912
3954
  blocks = applyNotebookFilter(blocks, parsed.filters);
3913
3955
  for (const filter of parsed.filters) {
3914
- if (filter.key === "tags" && !filter.negate) {
3956
+ if (isTagLabelKey(filter.key) && !filter.negate) {
3915
3957
  const matchingNoteIds = /* @__PURE__ */ new Set();
3916
3958
  for (const v of filter.values) {
3917
3959
  const ids = await this.noteStore.getNoteIdsForTag(v.value);
@@ -3961,7 +4003,9 @@ var QueryService = class {
3961
4003
  return { parsed, notes: [], blocks, stages: { selected: selectedCount, matched: blocks.length } };
3962
4004
  }
3963
4005
  /**
3964
- * Execute a :effort query against the effort registry.
4006
+ * Execute a :effort query: the registry is the filter plane (discipline,
4007
+ * intensity, origin, text), the note plane is the result plane — matched
4008
+ * projections resolve to their owning definition notes.
3965
4009
  */
3966
4010
  async runFindEffort(parsed) {
3967
4011
  const all = await this.effortStore.getAllEfforts();
@@ -4010,7 +4054,17 @@ var QueryService = class {
4010
4054
  const offset = pipes.offset ?? 0;
4011
4055
  efforts = pipes.limit !== void 0 ? efforts.slice(offset, offset + pipes.limit) : efforts.slice(offset);
4012
4056
  }
4013
- return { parsed, notes: [], blocks: [], efforts, stages: { selected: selectedCount, matched: efforts.length } };
4057
+ const ownerIds = [...new Set(efforts.map((e) => e.ownerNoteId).filter((id) => typeof id === "string" && id.length > 0))];
4058
+ let notes = [];
4059
+ if (ownerIds.length > 0) {
4060
+ const fetched = this.noteStore.getNotesByIds ? await this.noteStore.getNotesByIds(ownerIds) : (await this.noteStore.getAllNotes()).filter((n) => ownerIds.includes(n.id));
4061
+ const byId = new Map(fetched.map((n) => [n.id, n]));
4062
+ notes = ownerIds.flatMap((id) => {
4063
+ const note = byId.get(id);
4064
+ return note ? [note] : [];
4065
+ });
4066
+ }
4067
+ return { parsed, notes, blocks: [], efforts, stages: { selected: selectedCount, matched: efforts.length } };
4014
4068
  }
4015
4069
  async run(parsed, options2 = {}) {
4016
4070
  if (isPipelineQuery(parsed)) {
@@ -4122,6 +4176,7 @@ var QueryService = class {
4122
4176
  ...content.runs?.length ? { runs: content.runs } : {},
4123
4177
  ...content.table ? { table: content.table } : {},
4124
4178
  ...content.containers ? { containers: content.containers } : {},
4179
+ ...content.placements ? { placements: content.placements } : {},
4125
4180
  ...parsed.sink ? { chart: parsed.sink } : {}
4126
4181
  };
4127
4182
  }
@@ -4494,139 +4549,293 @@ var QueryService = class {
4494
4549
  return new Set(links.map((l) => l.noteId));
4495
4550
  }
4496
4551
  /**
4497
- * Find catalogs: identifies catalogs whose member notes (or catalog landing
4498
- * notes) match the authored criteria (effort, tags, text, typed tags, etc.).
4552
+ * Execute a :notebook query (ticket 08) — the notebook plane is a NOTE
4553
+ * plane (ticket 05 semantics preserved): bare `:notebook{}` returns the
4554
+ * notes of every notebook — journal, playground, defaults, user-created,
4555
+ * feed-delivered. `notebook:{slug}` narrows to members of those
4556
+ * notebooks and `!notebook:` excludes them; the result shape never
4557
+ * changes with polarity, and container criteria (tags:, effort:, typed
4558
+ * tags, text:) narrow the members exactly like the note plane — this
4559
+ * delegates to it. The notebook CONTAINER listing (the /notebooks
4560
+ * management surface) is the app's NotebookService model, not WQL rows.
4499
4561
  */
4500
- async runFindCatalog(parsed, options2, allNotes) {
4562
+ async runFindNotebook(parsed, options2 = {}) {
4563
+ return this.runFind({ ...parsed, target: "note" }, options2);
4564
+ }
4565
+ /**
4566
+ * Execute a :page query (ticket 08) — the placement plane. Results are
4567
+ * the placed notes; `placements` carries every page each note sits on
4568
+ * (many-to-many) and `containers` the primary projection. `page:{slug}`
4569
+ * (or a `YYYY-MM-DD` date, or a page id) narrows the placements.
4570
+ */
4571
+ async runFindPage(parsed, options2 = {}) {
4572
+ const empty = { parsed, notes: [], blocks: [], stages: { selected: 0, matched: 0 } };
4573
+ if (!this.pageStore?.getAllPages || !this.pageStore?.getAllLinks) return empty;
4501
4574
  const ctx = runContext(options2);
4502
- const feedNotes = allNotes.filter((n) => sourceMatches(n, "feeds") || n.catalog && !n.sourceId);
4503
- let catalogNotes = feedNotes.filter((n) => n.type === "feed" || n.sourceId?.startsWith("page:collection:"));
4504
- const existingCatIds = new Set(catalogNotes.map((n) => catalogOfItem(n) ?? n.id));
4505
- for (const n of feedNotes) {
4506
- const cat = catalogOfItem(n);
4507
- if (cat && !existingCatIds.has(cat)) {
4508
- catalogNotes.push({
4509
- id: cat,
4510
- title: cat,
4511
- type: "feed",
4512
- sourceId: `page:collection:${cat}`,
4513
- catalog: cat,
4514
- createdAt: n.createdAt ?? 0,
4515
- rawContent: "",
4516
- sourceRevision: 1,
4517
- blocks: {}
4518
- });
4519
- existingCatIds.add(cat);
4575
+ const [pages, links] = await Promise.all([this.pageStore.getAllPages(), this.pageStore.getAllLinks()]);
4576
+ let placements = wodWikiCore.resolvePlacements(links, pages);
4577
+ const selected = new Set(placements.map((p) => p.noteId)).size;
4578
+ for (const filter of parsed.filters) {
4579
+ if (filter.key !== "page") continue;
4580
+ const wanted = new Set(filter.values.map((v) => v.value));
4581
+ placements = placements.filter((p) => {
4582
+ const hit = wanted.has(p.pageId) || p.slug !== void 0 && wanted.has(p.slug) || p.date !== void 0 && wanted.has(p.date);
4583
+ return filter.negate ? !hit : hit;
4584
+ });
4585
+ }
4586
+ const placedIds = [...new Set(placements.map((p) => p.noteId))];
4587
+ let notes = this.noteStore.getNotesByIds ? await this.noteStore.getNotesByIds(placedIds) : (await this.noteStore.getAllNotes()).filter((n) => placedIds.includes(n.id));
4588
+ for (const filter of parsed.filters) {
4589
+ if (filter.key === "note") {
4590
+ const wanted = new Set(filter.values.map((v) => v.value));
4591
+ notes = notes.filter((n) => filter.negate ? !wanted.has(n.id) : wanted.has(n.id));
4520
4592
  }
4521
4593
  }
4522
- const membersByCat = /* @__PURE__ */ new Map();
4523
- for (const catNote of catalogNotes) {
4524
- const catId = catalogOfItem(catNote) ?? catNote.id;
4525
- membersByCat.set(catId, [catNote]);
4526
- }
4527
- for (const n of feedNotes) {
4528
- const catId = catalogOfItem(n);
4529
- if (catId) {
4530
- const list = membersByCat.get(catId);
4531
- if (list) {
4532
- if (!list.includes(n)) list.push(n);
4533
- } else {
4534
- membersByCat.set(catId, [n]);
4535
- }
4594
+ for (const filter of parsed.filters) {
4595
+ if (filter.key === "text") {
4596
+ const search = filter.values.map((v) => v.value).join(" ").toLowerCase();
4597
+ notes = notes.filter((n) => {
4598
+ const hit = n.title.toLowerCase().includes(search) || n.rawContent.toLowerCase().includes(search);
4599
+ return filter.negate ? !hit : hit;
4600
+ });
4536
4601
  }
4537
4602
  }
4538
- const criteriaFilters = parsed.filters.filter(
4539
- (f) => !(f.key === "source" && !f.negate && f.values.some((v) => v.value === "feeds" || v.value === "feed" || v.value === "notebooks" || v.value === "notebook")) && !(f.key === "type" && !f.negate && f.values.some((v) => v.value === "feed"))
4540
- );
4541
- let matchingCatalogs = [...catalogNotes];
4542
- for (const filter of criteriaFilters) {
4543
- if (filter.key === "catalog" || filter.key === "notebook") {
4544
- const wanted = new Set(filter.values.map((v) => wodWikiCore.notebookSlugFromId(v.value)));
4545
- matchingCatalogs = matchingCatalogs.filter((c) => {
4546
- const catId = wodWikiCore.notebookSlugFromId(catalogOfItem(c) ?? c.id);
4547
- const hit = wanted.has(catId);
4603
+ notes = applyNotebookFilter(notes, parsed.filters);
4604
+ if (parsed.window || options2.range) {
4605
+ notes = notes.filter((n) => effectiveTimeWindow(n.date ?? n.createdAt, parsed.window, options2.range, ctx));
4606
+ }
4607
+ notes = applyOrderAndPage(notes, parsed);
4608
+ const survived = new Set(notes.map((n) => n.id));
4609
+ const byNote = /* @__PURE__ */ new Map();
4610
+ for (const p of placements) {
4611
+ if (!survived.has(p.noteId)) continue;
4612
+ const list = byNote.get(p.noteId);
4613
+ if (list) list.push(p);
4614
+ else byNote.set(p.noteId, [p]);
4615
+ }
4616
+ const containers = {};
4617
+ for (const note of notes) {
4618
+ const primary = byNote.get(note.id)?.[0];
4619
+ containers[note.id] = primary ? { kind: "page", id: primary.pageId, ...primary.slug ? { slug: primary.slug } : {}, ...primary.date ? { date: primary.date } : {} } : { kind: "note", id: note.id };
4620
+ }
4621
+ return {
4622
+ parsed,
4623
+ notes,
4624
+ blocks: [],
4625
+ containers,
4626
+ placements: Object.fromEntries(byNote),
4627
+ stages: { selected, matched: notes.length }
4628
+ };
4629
+ }
4630
+ /**
4631
+ * Execute a :tag query (ticket 08) — the tag plane. Every tag record
4632
+ * surfaces as one synthetic note row, so results are notes end to end.
4633
+ * `tag:{label}` narrows; `type:` narrows by the tag's configured type;
4634
+ * `contributes:` narrows by what a tag contributes (`metric`,
4635
+ * `metric:<type>`, `tag:<slug>`, `function:<name>`) — a contribution is
4636
+ * exposed here, never as a `tags:` label filter. The synthetic row's body
4637
+ * summarizes the contributions so `text:` reaches them too.
4638
+ */
4639
+ async runFindTag(parsed, options2 = {}) {
4640
+ const ctx = runContext(options2);
4641
+ const tags = this.tagsStore.getAll ? await this.tagsStore.getAll() : [];
4642
+ const selected = tags.length;
4643
+ let matched = tags;
4644
+ for (const filter of parsed.filters) {
4645
+ if (isTagLabelKey(filter.key)) {
4646
+ const wanted = new Set(filter.values.map((v) => v.value.toLowerCase()));
4647
+ matched = matched.filter((t2) => {
4648
+ const hit = typeof t2.label === "string" && wanted.has(t2.label.toLowerCase());
4548
4649
  return filter.negate ? !hit : hit;
4549
4650
  });
4550
- } else if (filter.key === "effort") {
4551
- const matchingIds = /* @__PURE__ */ new Set();
4552
- for (const v of filter.values) {
4553
- const ids = await this.getNoteIdsForEffort(v.value);
4554
- ids.forEach((id) => matchingIds.add(id));
4555
- }
4556
- matchingCatalogs = matchingCatalogs.filter((c) => {
4557
- const catId = catalogOfItem(c) ?? c.id;
4558
- const members = membersByCat.get(catId) ?? [];
4559
- const hit = members.some((m) => matchingIds.has(m.id));
4651
+ } else if (filter.key === "type") {
4652
+ const wanted = new Set(filter.values.map((v) => v.value));
4653
+ matched = matched.filter((t2) => {
4654
+ const hit = t2.type !== void 0 && wanted.has(t2.type);
4560
4655
  return filter.negate ? !hit : hit;
4561
4656
  });
4562
- } else if (filter.key === "tags") {
4657
+ } else if (filter.key === "contributes") {
4658
+ matched = matched.filter((t2) => {
4659
+ const hit = (t2.contributions ?? []).some((c) => filter.values.some((v) => tagContributionMatches(c, v.value)));
4660
+ return filter.negate ? !hit : hit;
4661
+ });
4662
+ } else if (filter.key === "text") {
4663
+ const search = filter.values.map((v) => v.value).join(" ").toLowerCase();
4664
+ matched = matched.filter((t2) => filter.negate !== `${t2.label ?? ""} ${tagContributionSummary(t2)}`.toLowerCase().includes(search));
4665
+ }
4666
+ }
4667
+ let rows = matched.map((t2) => ({
4668
+ id: `tag/${typeof t2.type === "string" && t2.type ? t2.type : "general"}/${t2.label}`,
4669
+ title: t2.label,
4670
+ type: "tag",
4671
+ tagType: typeof t2.type === "string" ? t2.type : void 0,
4672
+ contributions: t2.contributions ?? [],
4673
+ createdAt: typeof t2.createdAt === "number" ? t2.createdAt : 0,
4674
+ content: tagContributionSummary(t2),
4675
+ rawContent: tagContributionSummary(t2),
4676
+ sourceRevision: 1,
4677
+ blocks: {}
4678
+ }));
4679
+ if (parsed.window || options2.range) {
4680
+ rows = rows.filter((r) => effectiveTimeWindow(r.createdAt, parsed.window, options2.range, ctx));
4681
+ }
4682
+ rows = applyOrderAndPage(rows, parsed);
4683
+ return { parsed, notes: rows, blocks: [], stages: { selected, matched: rows.length } };
4684
+ }
4685
+ /**
4686
+ * Execute an :equipment query (ticket 08) — the equipment plane. Results
4687
+ * are the equipment notes (the equipment notebook's members, typed
4688
+ * 'equipment' rows included). `equipment:{label}` narrows by the
4689
+ * equipment typed tag ("every equipment note tagged kettlebell" is
4690
+ * `:equipment{equipment:kettlebell}`); `tags:` and `text:` narrow like
4691
+ * the note plane.
4692
+ */
4693
+ async runFindEquipment(parsed, options2 = {}) {
4694
+ const allNotes = await this.noteStore.getAllNotes();
4695
+ const ctx = runContext(options2);
4696
+ let notes = allNotes.filter((n) => notebookOfItem(n) === "notebook/equipment" || n.type === "equipment" || sourceMatches(n, "equipment"));
4697
+ const selected = notes.length;
4698
+ for (const filter of parsed.filters) {
4699
+ if (filter.key === "equipment") {
4563
4700
  const matchingIds = /* @__PURE__ */ new Set();
4564
4701
  for (const v of filter.values) {
4565
- const ids = await this.noteStore.getNoteIdsForTag(v.value);
4566
- ids.forEach((id) => matchingIds.add(id));
4702
+ (await this.getNoteIdsForTypedTag("equipment", v.value)).forEach((id) => matchingIds.add(id));
4567
4703
  }
4568
- matchingCatalogs = matchingCatalogs.filter((c) => {
4569
- const catId = catalogOfItem(c) ?? c.id;
4570
- const members = membersByCat.get(catId) ?? [];
4571
- const hit = members.some((m) => matchingIds.has(m.id) || m.tags && filter.values.some((v) => m.tags.includes(v.value)));
4572
- return filter.negate ? !hit : hit;
4573
- });
4574
- } else if (WQL_TYPED_TAG_KEYS.includes(filter.key)) {
4704
+ notes = notes.filter((n) => filter.negate ? !matchingIds.has(n.id) : matchingIds.has(n.id));
4705
+ } else if (isTagLabelKey(filter.key) && !filter.negate) {
4575
4706
  const matchingIds = /* @__PURE__ */ new Set();
4576
4707
  for (const v of filter.values) {
4577
- const ids = await this.getNoteIdsForTypedTag(filter.key, v.value);
4578
- ids.forEach((id) => matchingIds.add(id));
4708
+ (await this.noteStore.getNoteIdsForTag(v.value)).forEach((id) => matchingIds.add(id));
4579
4709
  }
4580
- matchingCatalogs = matchingCatalogs.filter((c) => {
4581
- const catId = catalogOfItem(c) ?? c.id;
4582
- const members = membersByCat.get(catId) ?? [];
4583
- const hit = members.some((m) => matchingIds.has(m.id));
4584
- return filter.negate ? !hit : hit;
4585
- });
4586
- } else if (filter.key === "text") {
4710
+ notes = notes.filter((n) => matchingIds.has(n.id));
4711
+ } else if (isTagLabelKey(filter.key) && filter.negate) {
4712
+ for (const v of filter.values) {
4713
+ const ids = await this.noteStore.getNoteIdsForTag(v.value);
4714
+ notes = notes.filter((n) => !ids.has(n.id));
4715
+ }
4716
+ }
4717
+ }
4718
+ for (const filter of parsed.filters) {
4719
+ if (filter.key === "text") {
4587
4720
  const search = filter.values.map((v) => v.value).join(" ").toLowerCase();
4588
- matchingCatalogs = matchingCatalogs.filter((c) => {
4589
- const catId = catalogOfItem(c) ?? c.id;
4590
- const members = membersByCat.get(catId) ?? [];
4591
- const hit = members.some((m) => m.title.toLowerCase().includes(search) || m.rawContent.toLowerCase().includes(search));
4592
- return filter.negate ? !hit : hit;
4593
- });
4594
- } else if (filter.key === "note") {
4595
- const wanted = new Set(filter.values.map((v) => v.value));
4596
- matchingCatalogs = matchingCatalogs.filter((c) => {
4597
- const catId = catalogOfItem(c) ?? c.id;
4598
- const members = membersByCat.get(catId) ?? [];
4599
- const hit = members.some((m) => wanted.has(m.id));
4721
+ notes = notes.filter((n) => {
4722
+ const hit = n.title.toLowerCase().includes(search) || n.rawContent.toLowerCase().includes(search);
4600
4723
  return filter.negate ? !hit : hit;
4601
4724
  });
4602
4725
  }
4603
4726
  }
4727
+ notes = applyNotebookFilter(notes, parsed.filters);
4604
4728
  if (parsed.window || options2.range) {
4605
- matchingCatalogs = matchingCatalogs.filter((c) => {
4606
- const catId = catalogOfItem(c) ?? c.id;
4607
- const members = membersByCat.get(catId) ?? [];
4608
- return members.some((m) => effectiveTimeWindow(m.date ?? m.createdAt, parsed.window, options2.range, ctx));
4609
- });
4610
- }
4611
- if (parsed.pipes?.order) {
4612
- for (const order of [...parsed.pipes.order].reverse()) {
4613
- const dir = order.dir === "desc" ? -1 : 1;
4614
- matchingCatalogs = [...matchingCatalogs].sort((a, b) => {
4615
- const av = a[order.col] ?? "";
4616
- const bv = b[order.col] ?? "";
4617
- if (av === bv) return 0;
4618
- return (av > bv ? 1 : -1) * dir;
4619
- });
4620
- }
4729
+ notes = notes.filter((n) => effectiveTimeWindow(n.date ?? n.createdAt, parsed.window, options2.range, ctx));
4621
4730
  }
4622
- return {
4623
- parsed,
4624
- notes: matchingCatalogs,
4625
- blocks: [],
4626
- stages: { selected: catalogNotes.length, matched: matchingCatalogs.length }
4627
- };
4731
+ notes = applyOrderAndPage(notes, parsed);
4732
+ return { parsed, notes, blocks: [], stages: { selected, matched: notes.length } };
4628
4733
  }
4629
4734
  };
4735
+ function applyOrderAndPage(rows, parsed) {
4736
+ let out = rows;
4737
+ if (parsed.pipes?.order) {
4738
+ for (const order of [...parsed.pipes.order].reverse()) {
4739
+ const dir = order.dir === "desc" ? -1 : 1;
4740
+ out = [...out].sort((a, b) => {
4741
+ const av = a[order.col] ?? "";
4742
+ const bv = b[order.col] ?? "";
4743
+ if (av === bv) return 0;
4744
+ return (av > bv ? 1 : -1) * dir;
4745
+ });
4746
+ }
4747
+ }
4748
+ const pipes = parsed.pipes;
4749
+ if (pipes && (pipes.limit !== void 0 || (pipes.offset ?? 0) > 0)) {
4750
+ const offset = pipes.offset ?? 0;
4751
+ out = pipes.limit !== void 0 ? out.slice(offset, offset + pipes.limit) : out.slice(offset);
4752
+ }
4753
+ return out;
4754
+ }
4755
+ function isTagLabelKey(key) {
4756
+ return key === "tags" || key === "tag";
4757
+ }
4758
+ function tagContributionMatches(contribution, value) {
4759
+ const sep = value.indexOf(":");
4760
+ const kind = sep === -1 ? value : value.slice(0, sep);
4761
+ const qualifier = sep === -1 ? "" : value.slice(sep + 1);
4762
+ if (contribution.kind !== kind) return false;
4763
+ if (!qualifier) return true;
4764
+ if (contribution.kind === "metric") return contribution.type === qualifier;
4765
+ if (contribution.kind === "tag") return contribution.slug === qualifier;
4766
+ return contribution.name === qualifier;
4767
+ }
4768
+ function tagContributionSummary(tag) {
4769
+ const parts = (tag.contributions ?? []).map((c) => c.kind === "metric" ? `metric ${c.type}` : c.kind === "tag" ? `tag ${c.slug}` : `function ${c.name}`);
4770
+ return parts.length ? `contributes ${parts.join(", ")}` : "";
4771
+ }
4772
+
4773
+ // src/notebookVocabulary.ts
4774
+ function migrateLegacyChunkId(chunkId) {
4775
+ if (chunkId.startsWith("collection.")) return `notebook.${chunkId.slice("collection.".length)}`;
4776
+ if (chunkId.startsWith("feed.")) return `notebook.${chunkId.slice("feed.".length)}`;
4777
+ return chunkId;
4778
+ }
4779
+ function migrateLegacySourcePath(path) {
4780
+ if (path.startsWith("markdown/collections/")) return `markdown/notebooks/${path.slice("markdown/collections/".length)}`;
4781
+ if (path.startsWith("markdown/feeds/")) return `markdown/notebooks/${path.slice("markdown/feeds/".length)}`;
4782
+ return path;
4783
+ }
4784
+ function migrateLegacySourceId(sourceId) {
4785
+ if (sourceId.startsWith("page:collection:")) return `page:${sourceId.slice("page:collection:".length)}`;
4786
+ if (sourceId.startsWith("collection:")) return `notebook:${sourceId.slice("collection:".length)}`;
4787
+ if (sourceId.startsWith("feed:")) {
4788
+ return `notebook:${sourceId.slice("feed:".length).replace(/^feeds\//, "")}`;
4789
+ }
4790
+ return sourceId;
4791
+ }
4792
+ function migrateLegacyRouteId(routeId) {
4793
+ return routeId.startsWith("feeds/") ? routeId.slice("feeds/".length) : routeId;
4794
+ }
4795
+ function migrateLegacyRow(row) {
4796
+ let next = row;
4797
+ const apply = (patch) => {
4798
+ next = { ...next, ...patch };
4799
+ };
4800
+ const sourceId = row["sourceId"];
4801
+ if (typeof sourceId === "string") {
4802
+ const migrated = migrateLegacySourceId(sourceId);
4803
+ if (migrated !== sourceId) apply({ sourceId: migrated });
4804
+ }
4805
+ const sourcePath = row["sourcePath"];
4806
+ if (typeof sourcePath === "string") {
4807
+ const migrated = migrateLegacySourcePath(sourcePath);
4808
+ if (migrated !== sourcePath) apply({ sourcePath: migrated });
4809
+ }
4810
+ const seedChunkId = row["seedChunkId"];
4811
+ if (typeof seedChunkId === "string") {
4812
+ const migrated = migrateLegacyChunkId(seedChunkId);
4813
+ if (migrated !== seedChunkId) apply({ seedChunkId: migrated });
4814
+ }
4815
+ const routeId = row["routeId"];
4816
+ if (typeof routeId === "string") {
4817
+ const migrated = migrateLegacyRouteId(routeId);
4818
+ if (migrated !== routeId) apply({ routeId: migrated });
4819
+ }
4820
+ if ("catalog" in next) {
4821
+ next = { ...next };
4822
+ delete next["catalog"];
4823
+ }
4824
+ if (next["type"] === "feed") apply({ type: "note" });
4825
+ return next;
4826
+ }
4827
+ function hasLegacyIdentity(row) {
4828
+ const sourceId = row["sourceId"];
4829
+ if (typeof sourceId === "string" && (/^(collection|feed):/.test(sourceId) || sourceId.startsWith("page:collection:"))) return true;
4830
+ const sourcePath = row["sourcePath"];
4831
+ if (typeof sourcePath === "string" && (sourcePath.startsWith("markdown/collections/") || sourcePath.startsWith("markdown/feeds/"))) return true;
4832
+ const seedChunkId = row["seedChunkId"];
4833
+ if (typeof seedChunkId === "string" && /^(collection|feed)\./.test(seedChunkId)) return true;
4834
+ const routeId = row["routeId"];
4835
+ if (typeof routeId === "string" && routeId.startsWith("feeds/")) return true;
4836
+ if ("catalog" in row || row["type"] === "feed") return true;
4837
+ return false;
4838
+ }
4630
4839
 
4631
4840
  // src/dashboard/model.ts
4632
4841
  var DASHBOARD_WIDGET_TYPES = [
@@ -5269,13 +5478,13 @@ ${query}
5269
5478
  }
5270
5479
 
5271
5480
  // src/version.ts
5272
- var VERSION = "0.44.2159";
5273
- var GIT_SHA = "30e43c7";
5274
- var BUILD_TIME = "2026-10-24T23:07:22.311Z";
5481
+ var VERSION = "0.45.2166";
5482
+ var GIT_SHA = "32bda10";
5483
+ var BUILD_TIME = "2026-10-26T20:01:09.868Z";
5275
5484
  var SEMVER = {
5276
- version: "0.44.2159",
5277
- gitSha: "30e43c7",
5278
- buildTime: "2026-10-24T23:07:22.311Z"
5485
+ version: "0.45.2166",
5486
+ gitSha: "32bda10",
5487
+ buildTime: "2026-10-26T20:01:09.868Z"
5279
5488
  };
5280
5489
 
5281
5490
  Object.defineProperty(exports, "DEFAULT_NOTEBOOK_SLUGS", {
@@ -5331,6 +5540,7 @@ exports.PageSourceRegistry = PageSourceRegistry;
5331
5540
  exports.QueryDocumentRunner = QueryDocumentRunner;
5332
5541
  exports.QueryService = QueryService;
5333
5542
  exports.SEMVER = SEMVER;
5543
+ exports.SOURCE_ID_SHAPES = SOURCE_ID_SHAPES;
5334
5544
  exports.SharedQueryDocumentRunner = QueryDocumentRunner2;
5335
5545
  exports.UNASSIGNED = UNASSIGNED;
5336
5546
  exports.VERSION = VERSION;
@@ -5350,6 +5560,7 @@ exports.WQL_KEYWORDS = WQL_KEYWORDS;
5350
5560
  exports.WQL_METRIC_AGGREGATES = WQL_METRIC_AGGREGATES;
5351
5561
  exports.WQL_METRIC_FAMILIES = WQL_METRIC_FAMILIES;
5352
5562
  exports.WQL_NOTE_DEFAULT_SOURCES = WQL_NOTE_DEFAULT_SOURCES;
5563
+ exports.WQL_NOTE_HEAD_ALIASES = WQL_NOTE_HEAD_ALIASES;
5353
5564
  exports.WQL_RESULT_PLANES = WQL_RESULT_PLANES;
5354
5565
  exports.WQL_ROLLUP_PERIODS = WQL_ROLLUP_PERIODS;
5355
5566
  exports.WQL_SOURCES = WQL_SOURCES;
@@ -5368,7 +5579,6 @@ exports.buildDashboardDocument = buildDashboardDocument;
5368
5579
  exports.buildDashboardScaffold = buildDashboardScaffold;
5369
5580
  exports.buildDrillDownQuery = buildDrillDownQuery;
5370
5581
  exports.captureContext = captureContext;
5371
- exports.catalogOfItem = catalogOfItem;
5372
5582
  exports.civilDateAdd = civilDateAdd;
5373
5583
  exports.civilDateDiff = civilDateDiff;
5374
5584
  exports.civilDateOf = civilDateOf;
@@ -5384,10 +5594,12 @@ exports.duplicateWidget = duplicateWidget;
5384
5594
  exports.eventsToStoredLogs = eventsToStoredLogs;
5385
5595
  exports.extractDashboardTokens = extractDashboardTokens;
5386
5596
  exports.extractFrontmatterTags = extractFrontmatterTags;
5597
+ exports.feedOfItem = feedOfItem;
5387
5598
  exports.findTargetAdvisories = findTargetAdvisories;
5388
5599
  exports.getList = getList;
5389
5600
  exports.getScalar = getScalar;
5390
5601
  exports.getUnitFamily = getUnitFamily;
5602
+ exports.hasLegacyIdentity = hasLegacyIdentity;
5391
5603
  exports.inRange = inRange;
5392
5604
  exports.isAggregateQuery = isAggregateQuery;
5393
5605
  exports.isDashboardMeta = isDashboardMeta;
@@ -5396,11 +5608,18 @@ exports.isEffortDiscipline = isEffortDiscipline;
5396
5608
  exports.isFindQuery = isFindQuery;
5397
5609
  exports.isPipelineQuery = isPipelineQuery;
5398
5610
  exports.isProposedMetric = isProposedMetric;
5611
+ exports.migrateLegacyChunkId = migrateLegacyChunkId;
5399
5612
  exports.migrateLegacyDashboardBodies = migrateLegacyDashboardBodies;
5613
+ exports.migrateLegacyRouteId = migrateLegacyRouteId;
5614
+ exports.migrateLegacyRow = migrateLegacyRow;
5615
+ exports.migrateLegacySourceId = migrateLegacySourceId;
5616
+ exports.migrateLegacySourcePath = migrateLegacySourcePath;
5400
5617
  exports.moveWidget = moveWidget;
5401
5618
  exports.normalizeSummaryFacts = normalizeSummaryFacts;
5402
5619
  exports.normalizeWql = normalizeWql;
5403
5620
  exports.notebookOfItem = notebookOfItem;
5621
+ exports.notebookOriginOfItem = notebookOriginOfItem;
5622
+ exports.notebookSlugOfSourceId = notebookSlugOfSourceId;
5404
5623
  exports.parseDashboardNote = parseDashboardNote;
5405
5624
  exports.parseDocument = parseDocument;
5406
5625
  exports.parseFlatProperties = parseFlatProperties;
@@ -5426,6 +5645,7 @@ exports.serializeDocument = serializeDocument;
5426
5645
  exports.serializeFrontmatter = serializeFrontmatter;
5427
5646
  exports.setDashboardTokenValue = setDashboardTokenValue;
5428
5647
  exports.sourceMatches = sourceMatches;
5648
+ exports.sourceShape = sourceShape;
5429
5649
  exports.splitAtWhere = splitAtWhere;
5430
5650
  exports.stripFrontmatter = stripFrontmatter;
5431
5651
  exports.substituteTokens = substituteTokens2;