patram 0.3.0 → 0.5.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.
- package/lib/build-graph-identity.js +70 -84
- package/lib/build-graph.js +171 -19
- package/lib/build-graph.types.ts +1 -0
- package/lib/check-directive-metadata.js +36 -4
- package/lib/check-directive-value.js +9 -0
- package/lib/check-graph.js +1 -2
- package/lib/cli-help-metadata.js +12 -0
- package/lib/command-output.js +16 -1
- package/lib/discover-fields.js +9 -1
- package/lib/document-node-identity.js +317 -0
- package/lib/layout-stored-queries.js +122 -29
- package/lib/load-patram-config.js +172 -112
- package/lib/load-patram-config.types.ts +50 -152
- package/lib/parse-claims.js +2 -3
- package/lib/parse-jsdoc-claims.js +3 -3
- package/lib/parse-where-clause.js +237 -66
- package/lib/parse-where-clause.types.ts +21 -6
- package/lib/patram-cli.js +34 -2
- package/lib/patram-config.js +26 -9
- package/lib/patram-config.types.ts +18 -36
- package/lib/query-graph.js +29 -19
- package/lib/query-inspection.js +173 -68
- package/lib/render-field-discovery.js +44 -8
- package/lib/render-output-view.js +72 -27
- package/lib/render-rich-source.js +245 -14
- package/lib/resolve-patram-graph-config.js +40 -2
- package/lib/show-document.js +15 -2
- package/package.json +1 -1
|
@@ -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
|
|
package/lib/check-graph.js
CHANGED
|
@@ -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 (
|
|
127
|
+
if (!target_path) {
|
|
129
128
|
return;
|
|
130
129
|
}
|
|
131
130
|
|
package/lib/cli-help-metadata.js
CHANGED
|
@@ -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
|
};
|
package/lib/command-output.js
CHANGED
|
@@ -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 === '
|
|
79
|
+
(parsed_command.command_name === 'fields' ||
|
|
80
|
+
parsed_command.command_name === 'query' ||
|
|
66
81
|
parsed_command.command_name === 'show')
|
|
67
82
|
);
|
|
68
83
|
}
|
package/lib/discover-fields.js
CHANGED
|
@@ -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(
|
|
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.
|
|
95
|
-
|
|
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').
|
|
101
|
-
* @param {
|
|
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
|
|
122
|
+
function createExpressionPhrase(expression, operator, should_prefix_operator) {
|
|
105
123
|
/** @type {StoredQuerySegment[]} */
|
|
106
124
|
const phrase = [];
|
|
107
125
|
|
|
108
|
-
if (
|
|
109
|
-
phrase.push({ kind: 'keyword', text:
|
|
126
|
+
if (should_prefix_operator) {
|
|
127
|
+
phrase.push({ kind: 'keyword', text: operator });
|
|
110
128
|
phrase.push({ kind: 'plain', text: ' ' });
|
|
111
129
|
}
|
|
112
130
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
154
|
+
return [
|
|
155
|
+
{ kind: 'operator', text: '(' },
|
|
156
|
+
...expression_segments,
|
|
157
|
+
{ kind: 'operator', text: ')' },
|
|
158
|
+
];
|
|
159
|
+
}
|
|
119
160
|
|
|
120
|
-
|
|
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
|
-
...
|
|
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[]}
|