patram 0.2.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.
Files changed (38) hide show
  1. package/lib/build-graph-identity.js +86 -99
  2. package/lib/build-graph.js +536 -31
  3. package/lib/build-graph.types.ts +6 -2
  4. package/lib/check-directive-metadata.js +534 -0
  5. package/lib/check-directive-value.js +291 -0
  6. package/lib/check-graph.js +23 -5
  7. package/lib/cli-help-metadata.js +56 -16
  8. package/lib/command-output.js +16 -1
  9. package/lib/derived-summary.js +10 -8
  10. package/lib/directive-diagnostics.js +38 -0
  11. package/lib/directive-type-rules.js +133 -0
  12. package/lib/discover-fields.js +435 -0
  13. package/lib/discover-fields.types.ts +52 -0
  14. package/lib/document-node-identity.js +317 -0
  15. package/lib/format-node-header.js +9 -7
  16. package/lib/format-output-metadata.js +15 -23
  17. package/lib/layout-stored-queries.js +124 -85
  18. package/lib/load-patram-config.js +433 -96
  19. package/lib/load-patram-config.types.ts +98 -3
  20. package/lib/load-project-graph.js +4 -1
  21. package/lib/output-view.types.ts +14 -6
  22. package/lib/parse-cli-arguments.types.ts +1 -1
  23. package/lib/parse-where-clause.js +344 -107
  24. package/lib/parse-where-clause.types.ts +25 -8
  25. package/lib/patram-cli.js +68 -4
  26. package/lib/patram-config.js +31 -31
  27. package/lib/patram-config.types.ts +10 -4
  28. package/lib/query-graph.js +269 -40
  29. package/lib/query-inspection.js +440 -60
  30. package/lib/render-field-discovery.js +184 -0
  31. package/lib/render-json-output.js +21 -22
  32. package/lib/render-output-view.js +301 -34
  33. package/lib/render-plain-output.js +1 -1
  34. package/lib/render-rich-output.js +1 -1
  35. package/lib/render-rich-source.js +245 -14
  36. package/lib/resolve-patram-graph-config.js +15 -9
  37. package/lib/show-document.js +66 -9
  38. package/package.json +5 -5
@@ -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
+ }
@@ -3,17 +3,19 @@
3
3
  * @returns {string}
4
4
  */
5
5
  export function formatNodeHeader(output_item) {
6
- if (isDocumentNode(output_item)) {
7
- return `document ${output_item.path}`;
6
+ if (output_item.path) {
7
+ return `${output_item.node_kind} ${output_item.path}`;
8
8
  }
9
9
 
10
- return `${output_item.node_kind} ${output_item.id}`;
10
+ return `${output_item.node_kind} ${getOutputNodeKey(output_item.id)}`;
11
11
  }
12
12
 
13
13
  /**
14
- * @param {import('./output-view.types.ts').OutputNodeItem} output_item
15
- * @returns {boolean}
14
+ * @param {string} node_id
15
+ * @returns {string}
16
16
  */
17
- export function isDocumentNode(output_item) {
18
- return output_item.id === `doc:${output_item.path}`;
17
+ function getOutputNodeKey(node_id) {
18
+ return node_id.includes(':')
19
+ ? node_id.split(':').slice(1).join(':')
20
+ : node_id;
19
21
  }
@@ -1,5 +1,4 @@
1
1
  import { formatDerivedSummaryRow } from './format-derived-summary-row.js';
2
- import { isDocumentNode } from './format-node-header.js';
3
2
 
4
3
  /**
5
4
  * @param {import('./output-view.types.ts').OutputNodeItem} output_item
@@ -8,18 +7,8 @@ import { isDocumentNode } from './format-node-header.js';
8
7
  export function formatOutputNodeMetadataRows(output_item) {
9
8
  /** @type {string[]} */
10
9
  const metadata_rows = [];
11
- /** @type {string[]} */
12
- const stored_metadata_fields = [];
13
-
14
- if (isDocumentNode(output_item)) {
15
- stored_metadata_fields.push(`kind: ${output_item.node_kind}`);
16
- } else {
17
- stored_metadata_fields.push(`path: ${output_item.path}`);
18
- }
19
-
20
- if (output_item.status) {
21
- stored_metadata_fields.push(`status: ${output_item.status}`);
22
- }
10
+ const stored_metadata_fields =
11
+ output_item.visible_fields.map(formatMetadataField);
23
12
 
24
13
  if (stored_metadata_fields.length > 0) {
25
14
  metadata_rows.push(stored_metadata_fields.join(' '));
@@ -39,16 +28,7 @@ export function formatOutputNodeMetadataRows(output_item) {
39
28
  export function formatResolvedLinkMetadataRows(target) {
40
29
  /** @type {string[]} */
41
30
  const metadata_rows = [];
42
- /** @type {string[]} */
43
- const stored_metadata_fields = [];
44
-
45
- if (target.kind) {
46
- stored_metadata_fields.push(`kind: ${target.kind}`);
47
- }
48
-
49
- if (target.status) {
50
- stored_metadata_fields.push(`status: ${target.status}`);
51
- }
31
+ const stored_metadata_fields = target.visible_fields.map(formatMetadataField);
52
32
 
53
33
  if (stored_metadata_fields.length > 0) {
54
34
  metadata_rows.push(stored_metadata_fields.join(' '));
@@ -60,3 +40,15 @@ export function formatResolvedLinkMetadataRows(target) {
60
40
 
61
41
  return metadata_rows;
62
42
  }
43
+
44
+ /**
45
+ * @param {import('./output-view.types.ts').OutputMetadataField} output_field
46
+ * @returns {string}
47
+ */
48
+ function formatMetadataField(output_field) {
49
+ const value = Array.isArray(output_field.value)
50
+ ? output_field.value.join(', ')
51
+ : output_field.value;
52
+
53
+ return `${output_field.name}: ${value}`;
54
+ }
@@ -92,65 +92,145 @@ function createStoredQueryPhrases(where_clause) {
92
92
  return createFallbackPhrases(where_clause);
93
93
  }
94
94
 
95
- return parse_result.clauses.map((clause, clause_index) =>
96
- 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
+ ),
97
113
  );
98
114
  }
99
115
 
100
116
  /**
101
- * @param {{
102
- * is_negated: boolean,
103
- * term:
104
- * | { field_name: 'id' | 'kind' | 'path' | 'status' | 'title', kind: 'field', operator: '=' | '^=' | '~', value: string }
105
- * | { field_name: 'id' | 'kind' | 'path' | 'status' | 'title', kind: 'field_set', operator: 'in' | 'not in', values: string[] }
106
- * | { kind: 'relation', relation_name: string }
107
- * | { kind: 'relation_target', relation_name: string, target_id: string }
108
- * | {
109
- * aggregate_name: 'any' | 'count' | 'none',
110
- * clauses: unknown[],
111
- * comparison?: '!=' | '<' | '<=' | '=' | '>' | '>=',
112
- * kind: 'aggregate',
113
- * traversal: { direction: 'in' | 'out', relation_name: string },
114
- * value?: number,
115
- * },
116
- * }} clause
117
- * @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
118
120
  * @returns {StoredQuerySegment[]}
119
121
  */
120
- function createClausePhrase(clause, should_prefix_and) {
122
+ function createExpressionPhrase(expression, operator, should_prefix_operator) {
121
123
  /** @type {StoredQuerySegment[]} */
122
124
  const phrase = [];
123
125
 
124
- if (should_prefix_and) {
125
- phrase.push({ kind: 'keyword', text: 'and' });
126
+ if (should_prefix_operator) {
127
+ phrase.push({ kind: 'keyword', text: operator });
126
128
  phrase.push({ kind: 'plain', text: ' ' });
127
129
  }
128
130
 
129
- if (clause.is_negated) {
130
- phrase.push({ kind: 'keyword', text: 'not' });
131
- 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;
132
152
  }
133
153
 
134
- phrase.push(...createTermSegments(clause.term));
154
+ return [
155
+ { kind: 'operator', text: '(' },
156
+ ...expression_segments,
157
+ { kind: 'operator', text: ')' },
158
+ ];
159
+ }
135
160
 
136
- 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;
137
230
  }
138
231
 
139
232
  /**
140
- * @param {
141
- * | { field_name: 'id' | 'kind' | 'path' | 'status' | 'title', kind: 'field', operator: '=' | '^=' | '~', value: string }
142
- * | { field_name: 'id' | 'kind' | 'path' | 'status' | 'title', kind: 'field_set', operator: 'in' | 'not in', values: string[] }
143
- * | { kind: 'relation', relation_name: string }
144
- * | { kind: 'relation_target', relation_name: string, target_id: string }
145
- * | {
146
- * aggregate_name: 'any' | 'count' | 'none',
147
- * clauses: { is_negated: boolean, term: unknown }[],
148
- * comparison?: '!=' | '<' | '<=' | '=' | '>' | '>=',
149
- * kind: 'aggregate',
150
- * traversal: { direction: 'in' | 'out', relation_name: string },
151
- * value?: number,
152
- * }
153
- * } term
233
+ * @param {import('./parse-where-clause.types.ts').ParsedTerm} term
154
234
  * @returns {StoredQuerySegment[]}
155
235
  */
156
236
  function createTermSegments(term) {
@@ -185,7 +265,7 @@ function createTermSegments(term) {
185
265
  }
186
266
 
187
267
  /**
188
- * @param {{ field_name: 'id' | 'kind' | 'path' | 'status' | 'title', kind: 'field_set', operator: 'in' | 'not in', values: string[] }} term
268
+ * @param {import('./parse-where-clause.types.ts').ParsedFieldSetTerm} term
189
269
  * @returns {StoredQuerySegment[]}
190
270
  */
191
271
  function createFieldSetSegments(term) {
@@ -201,14 +281,7 @@ function createFieldSetSegments(term) {
201
281
  }
202
282
 
203
283
  /**
204
- * @param {{
205
- * aggregate_name: 'any' | 'count' | 'none',
206
- * clauses: { is_negated: boolean, term: unknown }[],
207
- * comparison?: '!=' | '<' | '<=' | '=' | '>' | '>=',
208
- * kind: 'aggregate',
209
- * traversal: { direction: 'in' | 'out', relation_name: string },
210
- * value?: number,
211
- * }} term
284
+ * @param {import('./parse-where-clause.types.ts').ParsedAggregateTerm} term
212
285
  * @returns {StoredQuerySegment[]}
213
286
  */
214
287
  function createAggregateSegments(term) {
@@ -218,7 +291,7 @@ function createAggregateSegments(term) {
218
291
  { kind: 'operator', text: '(' },
219
292
  ...createTraversalSegments(term.traversal),
220
293
  { kind: 'operator', text: ', ' },
221
- ...createNestedClauseSegments(term.clauses),
294
+ ...createExpressionSegments(term.expression, 0),
222
295
  { kind: 'operator', text: ')' },
223
296
  ];
224
297
 
@@ -244,40 +317,6 @@ function createTraversalSegments(traversal) {
244
317
  ];
245
318
  }
246
319
 
247
- /**
248
- * @param {{ is_negated: boolean, term: unknown }[]} clauses
249
- * @returns {StoredQuerySegment[]}
250
- */
251
- function createNestedClauseSegments(clauses) {
252
- return clauses.flatMap((clause, clause_index) => {
253
- const clause_phrase = createClausePhrase(
254
- /** @type {{
255
- * is_negated: boolean,
256
- * term:
257
- * | { field_name: 'id' | 'kind' | 'path' | 'status' | 'title', kind: 'field', operator: '=' | '^=' | '~', value: string }
258
- * | { field_name: 'id' | 'kind' | 'path' | 'status' | 'title', kind: 'field_set', operator: 'in' | 'not in', values: string[] }
259
- * | { kind: 'relation', relation_name: string }
260
- * | { kind: 'relation_target', relation_name: string, target_id: string }
261
- * | {
262
- * aggregate_name: 'any' | 'count' | 'none',
263
- * clauses: { is_negated: boolean, term: unknown }[],
264
- * comparison?: '!=' | '<' | '<=' | '=' | '>' | '>=',
265
- * kind: 'aggregate',
266
- * traversal: { direction: 'in' | 'out', relation_name: string },
267
- * value?: number,
268
- * },
269
- * }} */ (clause),
270
- clause_index > 0,
271
- );
272
-
273
- if (clause_index === 0) {
274
- return clause_phrase;
275
- }
276
-
277
- return [{ kind: 'plain', text: ' ' }, ...clause_phrase];
278
- });
279
- }
280
-
281
320
  /**
282
321
  * @param {string[]} values
283
322
  * @returns {StoredQuerySegment[]}