patram 0.3.0 → 0.4.0

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.
@@ -21,6 +21,7 @@ import {
21
21
  * @param {PatramRepoConfig} repo_config
22
22
  * @param {MetadataDirectiveRuleConfig | undefined} _directive_rule
23
23
  * @param {Map<string, string>} document_entity_keys
24
+ * @param {Map<string, import('./document-node-identity.js').DocumentNodeReference>} document_node_references
24
25
  * @param {Set<string>} document_paths
25
26
  * @returns {PatramDiagnostic[]}
26
27
  */
@@ -31,6 +32,7 @@ export function checkDirectiveValue(
31
32
  repo_config,
32
33
  _directive_rule,
33
34
  document_entity_keys,
35
+ document_node_references,
34
36
  document_paths,
35
37
  ) {
36
38
  const mapping_definition = resolveDirectiveMapping(mappings, claim);
@@ -79,6 +81,7 @@ export function checkDirectiveValue(
79
81
  repo_config,
80
82
  type_definition,
81
83
  document_entity_keys,
84
+ document_node_references,
82
85
  document_paths,
83
86
  );
84
87
  }
@@ -157,6 +160,7 @@ function createInvalidTypeDiagnostic(
157
160
  * @param {PatramRepoConfig} repo_config
158
161
  * @param {Exclude<DirectiveTypeConfig, { type: 'enum' }>} type_definition
159
162
  * @param {Map<string, string>} document_entity_keys
163
+ * @param {Map<string, import('./document-node-identity.js').DocumentNodeReference>} document_node_references
160
164
  * @param {Set<string>} document_paths
161
165
  * @returns {PatramDiagnostic[]}
162
166
  */
@@ -167,6 +171,7 @@ function createPathClassDiagnostics(
167
171
  repo_config,
168
172
  type_definition,
169
173
  document_entity_keys,
174
+ document_node_references,
170
175
  document_paths,
171
176
  ) {
172
177
  if (
@@ -177,6 +182,7 @@ function createPathClassDiagnostics(
177
182
  claim,
178
183
  type_definition.path_class,
179
184
  document_entity_keys,
185
+ document_node_references,
180
186
  document_paths,
181
187
  repo_config,
182
188
  )
@@ -198,6 +204,7 @@ function createPathClassDiagnostics(
198
204
  * @param {PatramClaim} claim
199
205
  * @param {string} path_class_name
200
206
  * @param {Map<string, string>} document_entity_keys
207
+ * @param {Map<string, import('./document-node-identity.js').DocumentNodeReference>} document_node_references
201
208
  * @param {Set<string>} document_paths
202
209
  * @param {PatramRepoConfig} repo_config
203
210
  * @returns {boolean}
@@ -207,6 +214,7 @@ function isDirectivePathInClass(
207
214
  claim,
208
215
  path_class_name,
209
216
  document_entity_keys,
217
+ document_node_references,
210
218
  document_paths,
211
219
  repo_config,
212
220
  ) {
@@ -223,6 +231,7 @@ function isDirectivePathInClass(
223
231
  'path',
224
232
  claim,
225
233
  document_entity_keys,
234
+ document_node_references,
226
235
  document_paths,
227
236
  );
228
237
 
@@ -118,14 +118,13 @@ function collectBrokenLinkDiagnostics(
118
118
  target_node,
119
119
  existing_file_path_set,
120
120
  ) {
121
- const target_class = target_node.$class ?? target_node.kind;
122
121
  const target_path = target_node.$path ?? target_node.path;
123
122
 
124
123
  if (graph_edge.relation !== 'links_to') {
125
124
  return;
126
125
  }
127
126
 
128
- if (target_class !== 'document' || !target_path) {
127
+ if (!target_path) {
129
128
  return;
130
129
  }
131
130
 
@@ -280,6 +280,8 @@ const HELP_TOPIC_DEFINITIONS = {
280
280
  'query-language': {
281
281
  examples: [
282
282
  '$class=decision and status=accepted',
283
+ '$class=task or status=done',
284
+ '($class=task or status=blocked) and title~Show',
283
285
  '$path^=docs/plans/',
284
286
  'title~query',
285
287
  'tracked_in=doc:docs/plans/v0/worktracking-agent-guidance.md',
@@ -320,6 +322,14 @@ const HELP_TOPIC_DEFINITIONS = {
320
322
  description: 'Combine terms',
321
323
  label: 'and',
322
324
  },
325
+ {
326
+ description: 'Match either side',
327
+ label: 'or',
328
+ },
329
+ {
330
+ description: 'Group boolean expressions',
331
+ label: '( )',
332
+ },
323
333
  {
324
334
  description: 'Count comparisons',
325
335
  label: '!= < > >= <=',
@@ -363,6 +373,8 @@ const HELP_TOPIC_DEFINITIONS = {
363
373
  'count(<traversal>, <term> and <term>) <comparison> <number>',
364
374
  'not <term>',
365
375
  '<term> and <term>',
376
+ '<term> or <term>',
377
+ '(<expression>)',
366
378
  ],
367
379
  },
368
380
  };
@@ -45,6 +45,20 @@ export async function writeCommandOutput(
45
45
  parsed_command,
46
46
  );
47
47
 
48
+ await writeRenderedCommandOutput(io_context, parsed_command, rendered_output);
49
+ }
50
+
51
+ /**
52
+ * @param {{ stdout: { isTTY?: boolean, write(chunk: string): boolean }, write_paged_output?: (output_text: string) => Promise<void> }} io_context
53
+ * @param {ParsedCliArguments} parsed_command
54
+ * @param {string} rendered_output
55
+ * @returns {Promise<void>}
56
+ */
57
+ export async function writeRenderedCommandOutput(
58
+ io_context,
59
+ parsed_command,
60
+ rendered_output,
61
+ ) {
48
62
  if (shouldPageCommandOutput(parsed_command, io_context.stdout)) {
49
63
  await writeInteractiveOutput(io_context, rendered_output);
50
64
 
@@ -62,7 +76,8 @@ export async function writeCommandOutput(
62
76
  export function shouldPageCommandOutput(parsed_command, output_stream) {
63
77
  return (
64
78
  output_stream.isTTY === true &&
65
- (parsed_command.command_name === 'query' ||
79
+ (parsed_command.command_name === 'fields' ||
80
+ parsed_command.command_name === 'query' ||
66
81
  parsed_command.command_name === 'show')
67
82
  );
68
83
  }
@@ -63,9 +63,14 @@ const PATH_PATTERN = /^[a-z0-9_.-]+\.[a-z0-9]+$/du;
63
63
  * Discover likely field schema from source files.
64
64
  *
65
65
  * @param {string} [project_directory]
66
+ * @param {{ defined_field_names?: ReadonlySet<string> }} [options]
66
67
  * @returns {Promise<FieldDiscoveryResult>}
67
68
  */
68
- export async function discoverFields(project_directory = process.cwd()) {
69
+ export async function discoverFields(
70
+ project_directory = process.cwd(),
71
+ options,
72
+ ) {
73
+ const defined_field_names = options?.defined_field_names ?? new Set();
69
74
  const source_file_paths = await listSourceFiles(
70
75
  DEFAULT_INCLUDE_PATTERNS,
71
76
  project_directory,
@@ -140,6 +145,9 @@ export async function discoverFields(project_directory = process.cwd()) {
140
145
  );
141
146
  const fields = [...field_buckets.values()]
142
147
  .map(buildFieldSuggestion)
148
+ .filter(
149
+ (field_suggestion) => !defined_field_names.has(field_suggestion.name),
150
+ )
143
151
  .sort((left_suggestion, right_suggestion) =>
144
152
  left_suggestion.confidence !== right_suggestion.confidence
145
153
  ? right_suggestion.confidence - left_suggestion.confidence
@@ -0,0 +1,317 @@
1
+ /**
2
+ * @import { PatramClaim } from './parse-claims.types.ts';
3
+ * @import { MappingDefinition } from './patram-config.types.ts';
4
+ */
5
+
6
+ import { posix } from 'node:path';
7
+
8
+ /**
9
+ * @typedef {{
10
+ * class_name: string,
11
+ * id: string,
12
+ * key: string,
13
+ * path: string,
14
+ * }} DocumentNodeReference
15
+ */
16
+
17
+ /**
18
+ * Collect semantic entity keys defined by canonical documents.
19
+ *
20
+ * @param {Record<string, MappingDefinition>} mappings
21
+ * @param {PatramClaim[]} claims
22
+ * @returns {Map<string, string>}
23
+ */
24
+ export function collectDocumentEntityKeys(mappings, claims) {
25
+ /** @type {Map<string, string>} */
26
+ const document_entity_keys = new Map();
27
+
28
+ for (const claim of claims) {
29
+ const mapping_definition = resolveMappingDefinition(mappings, claim);
30
+
31
+ if (
32
+ mapping_definition?.node?.key !== 'value' ||
33
+ mapping_definition.node.class === 'document'
34
+ ) {
35
+ continue;
36
+ }
37
+
38
+ const source_path = normalizeRepoRelativePath(claim.origin.path);
39
+ const entity_map_key = getDocumentEntityMapKey(
40
+ source_path,
41
+ mapping_definition.node.class,
42
+ );
43
+ const entity_key = getStringClaimValue(claim);
44
+ const existing_entity_key = document_entity_keys.get(entity_map_key);
45
+
46
+ if (existing_entity_key && existing_entity_key !== entity_key) {
47
+ throw new Error(
48
+ `Document "${source_path}" defines multiple ${mapping_definition.node.class} ids.`,
49
+ );
50
+ }
51
+
52
+ document_entity_keys.set(entity_map_key, entity_key);
53
+ }
54
+
55
+ return document_entity_keys;
56
+ }
57
+
58
+ /**
59
+ * Collect canonical graph identities for document-backed source paths.
60
+ *
61
+ * @param {Record<string, MappingDefinition>} mappings
62
+ * @param {PatramClaim[]} claims
63
+ * @returns {Map<string, DocumentNodeReference>}
64
+ */
65
+ export function collectDocumentNodeReferences(mappings, claims) {
66
+ /** @type {Map<string, DocumentNodeReference>} */
67
+ const document_node_references = new Map();
68
+ /** @type {Map<string, string>} */
69
+ const pending_document_keys = new Map();
70
+
71
+ for (const claim of claims) {
72
+ const source_path = normalizeRepoRelativePath(claim.origin.path);
73
+ const document_node_reference =
74
+ document_node_references.get(source_path) ??
75
+ createDefaultDocumentNodeReference(source_path);
76
+ const mapping_definition = resolveMappingDefinition(mappings, claim);
77
+
78
+ document_node_references.set(source_path, document_node_reference);
79
+
80
+ if (mapping_definition?.node?.class !== 'document') {
81
+ continue;
82
+ }
83
+
84
+ applyDocumentNodeMapping(
85
+ document_node_reference,
86
+ mapping_definition.node,
87
+ claim,
88
+ pending_document_keys,
89
+ source_path,
90
+ );
91
+ }
92
+
93
+ return document_node_references;
94
+ }
95
+
96
+ /**
97
+ * Resolve the canonical node id for a source document path.
98
+ *
99
+ * @param {Record<string, string> | undefined} document_node_ids
100
+ * @param {string} document_path
101
+ * @returns {string}
102
+ */
103
+ export function resolveDocumentNodeId(document_node_ids, document_path) {
104
+ return document_node_ids?.[document_path] ?? `doc:${document_path}`;
105
+ }
106
+
107
+ /**
108
+ * Normalize one repo-relative source path.
109
+ *
110
+ * @param {string} source_path
111
+ * @returns {string}
112
+ */
113
+ export function normalizeRepoRelativePath(source_path) {
114
+ return posix.normalize(source_path.replaceAll('\\', '/'));
115
+ }
116
+
117
+ /**
118
+ * @param {DocumentNodeReference} document_node_reference
119
+ * @param {{ field: string, key?: 'path' | 'value', class: string }} node_mapping
120
+ * @param {PatramClaim} claim
121
+ * @param {Map<string, string>} pending_document_keys
122
+ * @param {string} source_path
123
+ */
124
+ function applyDocumentNodeMapping(
125
+ document_node_reference,
126
+ node_mapping,
127
+ claim,
128
+ pending_document_keys,
129
+ source_path,
130
+ ) {
131
+ if (node_mapping.field === '$class') {
132
+ assignDocumentNodeClass(
133
+ document_node_reference,
134
+ getStringClaimValue(claim),
135
+ );
136
+ applyPendingDocumentKey(
137
+ document_node_reference,
138
+ pending_document_keys,
139
+ source_path,
140
+ );
141
+ return;
142
+ }
143
+
144
+ if (node_mapping.field !== '$id' || node_mapping.key !== 'value') {
145
+ return;
146
+ }
147
+
148
+ const document_node_key = getStringClaimValue(claim);
149
+
150
+ if (document_node_reference.class_name === 'document') {
151
+ assignPendingDocumentKey(
152
+ pending_document_keys,
153
+ source_path,
154
+ document_node_key,
155
+ );
156
+ return;
157
+ }
158
+
159
+ assignDocumentNodeKey(document_node_reference, document_node_key);
160
+ }
161
+
162
+ /**
163
+ * @param {DocumentNodeReference} document_node_reference
164
+ * @param {Map<string, string>} pending_document_keys
165
+ * @param {string} source_path
166
+ */
167
+ function applyPendingDocumentKey(
168
+ document_node_reference,
169
+ pending_document_keys,
170
+ source_path,
171
+ ) {
172
+ if (document_node_reference.class_name === 'document') {
173
+ return;
174
+ }
175
+
176
+ const pending_document_key = pending_document_keys.get(source_path);
177
+
178
+ if (!pending_document_key) {
179
+ return;
180
+ }
181
+
182
+ assignDocumentNodeKey(document_node_reference, pending_document_key);
183
+ }
184
+
185
+ /**
186
+ * @param {Record<string, MappingDefinition>} mappings
187
+ * @param {PatramClaim} claim
188
+ * @returns {MappingDefinition | null}
189
+ */
190
+ function resolveMappingDefinition(mappings, claim) {
191
+ if (claim.type === 'directive') {
192
+ return resolveDirectiveMapping(mappings, claim);
193
+ }
194
+
195
+ return mappings[claim.type] ?? null;
196
+ }
197
+
198
+ /**
199
+ * @param {Record<string, MappingDefinition>} mappings
200
+ * @param {PatramClaim} claim
201
+ * @returns {MappingDefinition | null}
202
+ */
203
+ function resolveDirectiveMapping(mappings, claim) {
204
+ if (!claim.parser || !claim.name) {
205
+ return null;
206
+ }
207
+
208
+ return mappings[`${claim.parser}.directive.${claim.name}`] ?? null;
209
+ }
210
+
211
+ /**
212
+ * @param {PatramClaim} claim
213
+ * @returns {string}
214
+ */
215
+ function getStringClaimValue(claim) {
216
+ if (typeof claim.value === 'string') {
217
+ return claim.value;
218
+ }
219
+
220
+ throw new Error(`Claim "${claim.id}" does not carry a string value.`);
221
+ }
222
+
223
+ /**
224
+ * @param {string} document_path
225
+ * @param {string} class_name
226
+ * @returns {string}
227
+ */
228
+ function getDocumentEntityMapKey(document_path, class_name) {
229
+ return `${class_name}:${document_path}`;
230
+ }
231
+
232
+ /**
233
+ * @param {string} source_path
234
+ * @returns {DocumentNodeReference}
235
+ */
236
+ function createDefaultDocumentNodeReference(source_path) {
237
+ return {
238
+ class_name: 'document',
239
+ id: `doc:${source_path}`,
240
+ key: source_path,
241
+ path: source_path,
242
+ };
243
+ }
244
+
245
+ /**
246
+ * @param {DocumentNodeReference} document_node_reference
247
+ * @param {string} class_name
248
+ */
249
+ function assignDocumentNodeClass(document_node_reference, class_name) {
250
+ if (
251
+ document_node_reference.class_name !== 'document' &&
252
+ document_node_reference.class_name !== class_name
253
+ ) {
254
+ throw new Error(
255
+ `Document "${document_node_reference.path}" defines multiple semantic classes.`,
256
+ );
257
+ }
258
+
259
+ document_node_reference.class_name = class_name;
260
+ document_node_reference.id = getNodeId(
261
+ document_node_reference.class_name,
262
+ document_node_reference.key,
263
+ );
264
+ }
265
+
266
+ /**
267
+ * @param {DocumentNodeReference} document_node_reference
268
+ * @param {string} node_key
269
+ */
270
+ function assignDocumentNodeKey(document_node_reference, node_key) {
271
+ if (
272
+ document_node_reference.key !== document_node_reference.path &&
273
+ document_node_reference.key !== node_key
274
+ ) {
275
+ throw new Error(
276
+ `Document "${document_node_reference.path}" defines multiple semantic ids.`,
277
+ );
278
+ }
279
+
280
+ document_node_reference.key = node_key;
281
+ document_node_reference.id = getNodeId(
282
+ document_node_reference.class_name,
283
+ document_node_reference.key,
284
+ );
285
+ }
286
+
287
+ /**
288
+ * @param {Map<string, string>} pending_document_keys
289
+ * @param {string} source_path
290
+ * @param {string} node_key
291
+ */
292
+ function assignPendingDocumentKey(
293
+ pending_document_keys,
294
+ source_path,
295
+ node_key,
296
+ ) {
297
+ const existing_node_key = pending_document_keys.get(source_path);
298
+
299
+ if (existing_node_key && existing_node_key !== node_key) {
300
+ throw new Error(`Document "${source_path}" defines multiple semantic ids.`);
301
+ }
302
+
303
+ pending_document_keys.set(source_path, node_key);
304
+ }
305
+
306
+ /**
307
+ * @param {string} class_name
308
+ * @param {string} node_key
309
+ * @returns {string}
310
+ */
311
+ function getNodeId(class_name, node_key) {
312
+ if (class_name === 'document') {
313
+ return `doc:${node_key}`;
314
+ }
315
+
316
+ return `${class_name}:${node_key}`;
317
+ }
@@ -1,3 +1,4 @@
1
+ /* eslint-disable max-lines */
1
2
  /**
2
3
  * @import { OutputStoredQueryItem } from './output-view.types.ts';
3
4
  */
@@ -91,33 +92,141 @@ function createStoredQueryPhrases(where_clause) {
91
92
  return createFallbackPhrases(where_clause);
92
93
  }
93
94
 
94
- return parse_result.clauses.map((clause, clause_index) =>
95
- createClausePhrase(clause, clause_index > 0),
95
+ return createExpressionPhrases(parse_result.expression);
96
+ }
97
+
98
+ /**
99
+ * @param {import('./parse-where-clause.types.ts').ParsedExpression} expression
100
+ * @returns {StoredQuerySegment[][]}
101
+ */
102
+ function createExpressionPhrases(expression) {
103
+ if (expression.kind !== 'and' && expression.kind !== 'or') {
104
+ return [[...createExpressionSegments(expression, 0)]];
105
+ }
106
+
107
+ return expression.expressions.map((subexpression, expression_index) =>
108
+ createExpressionPhrase(
109
+ subexpression,
110
+ expression.kind,
111
+ expression_index > 0,
112
+ ),
96
113
  );
97
114
  }
98
115
 
99
116
  /**
100
- * @param {import('./parse-where-clause.types.ts').ParsedClause} clause
101
- * @param {boolean} should_prefix_and
117
+ * @param {import('./parse-where-clause.types.ts').ParsedExpression} expression
118
+ * @param {'and' | 'or'} operator
119
+ * @param {boolean} should_prefix_operator
102
120
  * @returns {StoredQuerySegment[]}
103
121
  */
104
- function createClausePhrase(clause, should_prefix_and) {
122
+ function createExpressionPhrase(expression, operator, should_prefix_operator) {
105
123
  /** @type {StoredQuerySegment[]} */
106
124
  const phrase = [];
107
125
 
108
- if (should_prefix_and) {
109
- phrase.push({ kind: 'keyword', text: 'and' });
126
+ if (should_prefix_operator) {
127
+ phrase.push({ kind: 'keyword', text: operator });
110
128
  phrase.push({ kind: 'plain', text: ' ' });
111
129
  }
112
130
 
113
- if (clause.is_negated) {
114
- phrase.push({ kind: 'keyword', text: 'not' });
115
- phrase.push({ kind: 'plain', text: ' ' });
131
+ phrase.push(
132
+ ...createExpressionSegments(
133
+ expression,
134
+ getBooleanExpressionPrecedence(operator),
135
+ ),
136
+ );
137
+
138
+ return phrase;
139
+ }
140
+
141
+ /**
142
+ * @param {import('./parse-where-clause.types.ts').ParsedExpression} expression
143
+ * @param {number} parent_precedence
144
+ * @returns {StoredQuerySegment[]}
145
+ */
146
+ function createExpressionSegments(expression, parent_precedence) {
147
+ const expression_precedence = getExpressionPrecedence(expression);
148
+ const expression_segments = createRawExpressionSegments(expression);
149
+
150
+ if (expression_precedence >= parent_precedence) {
151
+ return expression_segments;
116
152
  }
117
153
 
118
- phrase.push(...createTermSegments(clause.term));
154
+ return [
155
+ { kind: 'operator', text: '(' },
156
+ ...expression_segments,
157
+ { kind: 'operator', text: ')' },
158
+ ];
159
+ }
119
160
 
120
- return phrase;
161
+ /**
162
+ * @param {import('./parse-where-clause.types.ts').ParsedExpression} expression
163
+ * @returns {StoredQuerySegment[]}
164
+ */
165
+ function createRawExpressionSegments(expression) {
166
+ if (expression.kind === 'and' || expression.kind === 'or') {
167
+ return expression.expressions.flatMap((subexpression, expression_index) => {
168
+ const subexpression_segments = createExpressionSegments(
169
+ subexpression,
170
+ getExpressionPrecedence(expression),
171
+ );
172
+
173
+ if (expression_index === 0) {
174
+ return subexpression_segments;
175
+ }
176
+
177
+ return [
178
+ { kind: 'plain', text: ' ' },
179
+ { kind: 'keyword', text: expression.kind },
180
+ { kind: 'plain', text: ' ' },
181
+ ...subexpression_segments,
182
+ ];
183
+ });
184
+ }
185
+
186
+ if (expression.kind === 'not') {
187
+ return [
188
+ { kind: 'keyword', text: 'not' },
189
+ { kind: 'plain', text: ' ' },
190
+ ...createExpressionSegments(
191
+ expression.expression,
192
+ getExpressionPrecedence(expression),
193
+ ),
194
+ ];
195
+ }
196
+
197
+ if (expression.kind === 'term') {
198
+ return createTermSegments(expression.term);
199
+ }
200
+
201
+ throw new Error('Unsupported stored-query expression.');
202
+ }
203
+
204
+ /**
205
+ * @param {import('./parse-where-clause.types.ts').ParsedExpression} expression
206
+ * @returns {number}
207
+ */
208
+ function getExpressionPrecedence(expression) {
209
+ if (expression.kind === 'or') {
210
+ return 1;
211
+ }
212
+
213
+ if (expression.kind === 'and') {
214
+ return 2;
215
+ }
216
+
217
+ if (expression.kind === 'not') {
218
+ return 3;
219
+ }
220
+
221
+ return 4;
222
+ }
223
+
224
+ /**
225
+ * @param {'and' | 'or'} operator
226
+ * @returns {number}
227
+ */
228
+ function getBooleanExpressionPrecedence(operator) {
229
+ return operator === 'or' ? 1 : 2;
121
230
  }
122
231
 
123
232
  /**
@@ -182,7 +291,7 @@ function createAggregateSegments(term) {
182
291
  { kind: 'operator', text: '(' },
183
292
  ...createTraversalSegments(term.traversal),
184
293
  { kind: 'operator', text: ', ' },
185
- ...createNestedClauseSegments(term.clauses),
294
+ ...createExpressionSegments(term.expression, 0),
186
295
  { kind: 'operator', text: ')' },
187
296
  ];
188
297
 
@@ -208,22 +317,6 @@ function createTraversalSegments(traversal) {
208
317
  ];
209
318
  }
210
319
 
211
- /**
212
- * @param {import('./parse-where-clause.types.ts').ParsedClause[]} clauses
213
- * @returns {StoredQuerySegment[]}
214
- */
215
- function createNestedClauseSegments(clauses) {
216
- return clauses.flatMap((clause, clause_index) => {
217
- const clause_phrase = createClausePhrase(clause, clause_index > 0);
218
-
219
- if (clause_index === 0) {
220
- return clause_phrase;
221
- }
222
-
223
- return [{ kind: 'plain', text: ' ' }, ...clause_phrase];
224
- });
225
- }
226
-
227
320
  /**
228
321
  * @param {string[]} values
229
322
  * @returns {StoredQuerySegment[]}
@@ -966,7 +966,7 @@ function collectWhereClauseDiagnostics(
966
966
  const semantic_diagnostics = getQuerySemanticDiagnostics(
967
967
  repo_config,
968
968
  { kind: 'ad_hoc' },
969
- parse_result.clauses,
969
+ parse_result.expression,
970
970
  );
971
971
 
972
972
  for (const semantic_diagnostic of semantic_diagnostics) {