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.
- package/lib/build-graph-identity.js +86 -99
- package/lib/build-graph.js +536 -31
- package/lib/build-graph.types.ts +6 -2
- package/lib/check-directive-metadata.js +534 -0
- package/lib/check-directive-value.js +291 -0
- package/lib/check-graph.js +23 -5
- package/lib/cli-help-metadata.js +56 -16
- package/lib/command-output.js +16 -1
- package/lib/derived-summary.js +10 -8
- package/lib/directive-diagnostics.js +38 -0
- package/lib/directive-type-rules.js +133 -0
- package/lib/discover-fields.js +435 -0
- package/lib/discover-fields.types.ts +52 -0
- package/lib/document-node-identity.js +317 -0
- package/lib/format-node-header.js +9 -7
- package/lib/format-output-metadata.js +15 -23
- package/lib/layout-stored-queries.js +124 -85
- package/lib/load-patram-config.js +433 -96
- package/lib/load-patram-config.types.ts +98 -3
- package/lib/load-project-graph.js +4 -1
- package/lib/output-view.types.ts +14 -6
- package/lib/parse-cli-arguments.types.ts +1 -1
- package/lib/parse-where-clause.js +344 -107
- package/lib/parse-where-clause.types.ts +25 -8
- package/lib/patram-cli.js +68 -4
- package/lib/patram-config.js +31 -31
- package/lib/patram-config.types.ts +10 -4
- package/lib/query-graph.js +269 -40
- package/lib/query-inspection.js +440 -60
- package/lib/render-field-discovery.js +184 -0
- package/lib/render-json-output.js +21 -22
- package/lib/render-output-view.js +301 -34
- package/lib/render-plain-output.js +1 -1
- package/lib/render-rich-output.js +1 -1
- package/lib/render-rich-source.js +245 -14
- package/lib/resolve-patram-graph-config.js +15 -9
- package/lib/show-document.js +66 -9
- package/package.json +5 -5
package/lib/build-graph.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
1
2
|
/**
|
|
2
3
|
* @import { BuildGraphResult, GraphEdge, GraphNode } from './build-graph.types.ts';
|
|
3
4
|
* @import { PatramClaim } from './parse-claims.types.ts';
|
|
5
|
+
* @import { MetadataFieldConfig } from './load-patram-config.types.ts';
|
|
4
6
|
* @import { MappingDefinition, PatramConfig } from './patram-config.types.ts';
|
|
5
7
|
*/
|
|
6
8
|
|
|
9
|
+
import { posix } from 'node:path';
|
|
10
|
+
|
|
7
11
|
import {
|
|
8
12
|
collectDocumentEntityKeys,
|
|
13
|
+
collectDocumentNodeReferences,
|
|
9
14
|
normalizeRepoRelativePath,
|
|
10
15
|
resolveNodeKey,
|
|
11
16
|
resolveTargetReference,
|
|
12
|
-
|
|
17
|
+
setCanonicalPath,
|
|
13
18
|
} from './build-graph-identity.js';
|
|
14
19
|
|
|
15
20
|
/**
|
|
@@ -25,7 +30,7 @@ import {
|
|
|
25
30
|
* Uses Term: ../docs/reference/terms/graph.md
|
|
26
31
|
* Uses Term: ../docs/reference/terms/mapping.md
|
|
27
32
|
* Uses Term: ../docs/reference/terms/relation.md
|
|
28
|
-
* Tracked in: ../docs/plans/v0/source-anchor-
|
|
33
|
+
* Tracked in: ../docs/plans/v0/source-anchor-dogfood.md
|
|
29
34
|
* Decided by: ../docs/decisions/graph-materialization.md
|
|
30
35
|
* Implements: ../docs/tasks/v0/materialize-graph.md
|
|
31
36
|
* @patram
|
|
@@ -33,6 +38,11 @@ import {
|
|
|
33
38
|
* @see {@link ../docs/decisions/graph-materialization.md}
|
|
34
39
|
*/
|
|
35
40
|
|
|
41
|
+
const STRUCTURAL_FIELD_NAMES = new Set(['$class', '$id', '$path']);
|
|
42
|
+
const DETERMINISTIC_LOCALE = 'en';
|
|
43
|
+
const DERIVED_TITLE_PRIORITY = 1;
|
|
44
|
+
const EXPLICIT_TITLE_PRIORITY = 2;
|
|
45
|
+
|
|
36
46
|
/**
|
|
37
47
|
* Build a Patram graph from semantic config and parsed claims.
|
|
38
48
|
*
|
|
@@ -43,6 +53,10 @@ import {
|
|
|
43
53
|
export function buildGraph(patram_config, claims) {
|
|
44
54
|
/** @type {Map<string, GraphNode>} */
|
|
45
55
|
const graph_nodes = new Map();
|
|
56
|
+
const document_node_references = collectDocumentNodeReferences(
|
|
57
|
+
patram_config.mappings,
|
|
58
|
+
claims,
|
|
59
|
+
);
|
|
46
60
|
const document_entity_keys = collectDocumentEntityKeys(
|
|
47
61
|
patram_config.mappings,
|
|
48
62
|
claims,
|
|
@@ -51,28 +65,43 @@ export function buildGraph(patram_config, claims) {
|
|
|
51
65
|
const document_paths = new Set(
|
|
52
66
|
claims.map((claim) => normalizeRepoRelativePath(claim.origin.path)),
|
|
53
67
|
);
|
|
68
|
+
/** @type {Map<string, number>} */
|
|
69
|
+
const title_priorities = new Map();
|
|
54
70
|
|
|
55
|
-
createDocumentNodes(graph_nodes,
|
|
71
|
+
createDocumentNodes(graph_nodes, document_node_references);
|
|
56
72
|
applyNodeMappings(
|
|
57
73
|
graph_nodes,
|
|
58
|
-
patram_config
|
|
74
|
+
patram_config,
|
|
59
75
|
claims,
|
|
60
76
|
document_entity_keys,
|
|
77
|
+
document_node_references,
|
|
78
|
+
title_priorities,
|
|
61
79
|
);
|
|
62
80
|
const graph_edges = createGraphEdges(
|
|
63
81
|
graph_nodes,
|
|
64
82
|
patram_config.mappings,
|
|
65
83
|
claims,
|
|
66
84
|
document_entity_keys,
|
|
85
|
+
document_node_references,
|
|
67
86
|
document_paths,
|
|
68
87
|
);
|
|
88
|
+
applyFallbackTitles(graph_nodes, title_priorities);
|
|
69
89
|
|
|
70
|
-
|
|
90
|
+
const graph_result = {
|
|
71
91
|
edges: graph_edges,
|
|
72
92
|
nodes: Object.fromEntries(
|
|
73
93
|
[...graph_nodes.entries()].sort(compareNodeEntries),
|
|
74
94
|
),
|
|
75
95
|
};
|
|
96
|
+
|
|
97
|
+
attachDocumentNodeAliases(graph_result.nodes, document_node_references);
|
|
98
|
+
Object.defineProperty(
|
|
99
|
+
graph_result,
|
|
100
|
+
'document_node_ids',
|
|
101
|
+
createDocumentNodeIdsProperty(document_node_references),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return graph_result;
|
|
76
105
|
}
|
|
77
106
|
|
|
78
107
|
/**
|
|
@@ -103,32 +132,41 @@ function resolveDirectiveMapping(mappings, claim) {
|
|
|
103
132
|
|
|
104
133
|
/**
|
|
105
134
|
* @param {Map<string, GraphNode>} graph_nodes
|
|
106
|
-
* @param {
|
|
135
|
+
* @param {Map<string, import('./document-node-identity.js').DocumentNodeReference>} document_node_references
|
|
107
136
|
*/
|
|
108
|
-
function createDocumentNodes(graph_nodes,
|
|
109
|
-
for (const
|
|
110
|
-
upsertNode(
|
|
137
|
+
function createDocumentNodes(graph_nodes, document_node_references) {
|
|
138
|
+
for (const document_node_reference of document_node_references.values()) {
|
|
139
|
+
const graph_node = upsertNode(
|
|
111
140
|
graph_nodes,
|
|
112
|
-
|
|
113
|
-
|
|
141
|
+
document_node_reference.class_name,
|
|
142
|
+
document_node_reference.key,
|
|
114
143
|
);
|
|
144
|
+
|
|
145
|
+
setCanonicalPath(graph_node, document_node_reference.path);
|
|
115
146
|
}
|
|
116
147
|
}
|
|
117
148
|
|
|
118
149
|
/**
|
|
119
150
|
* @param {Map<string, GraphNode>} graph_nodes
|
|
120
|
-
* @param {
|
|
151
|
+
* @param {PatramConfig} patram_config
|
|
121
152
|
* @param {PatramClaim[]} claims
|
|
122
153
|
* @param {Map<string, string>} document_entity_keys
|
|
154
|
+
* @param {Map<string, import('./document-node-identity.js').DocumentNodeReference>} document_node_references
|
|
155
|
+
* @param {Map<string, number>} title_priorities
|
|
123
156
|
*/
|
|
124
157
|
function applyNodeMappings(
|
|
125
158
|
graph_nodes,
|
|
126
|
-
|
|
159
|
+
patram_config,
|
|
127
160
|
claims,
|
|
128
161
|
document_entity_keys,
|
|
162
|
+
document_node_references,
|
|
163
|
+
title_priorities,
|
|
129
164
|
) {
|
|
130
165
|
for (const claim of claims) {
|
|
131
|
-
const mapping_definition = resolveMappingDefinition(
|
|
166
|
+
const mapping_definition = resolveMappingDefinition(
|
|
167
|
+
patram_config.mappings,
|
|
168
|
+
claim,
|
|
169
|
+
);
|
|
132
170
|
|
|
133
171
|
if (!mapping_definition?.node) {
|
|
134
172
|
continue;
|
|
@@ -136,9 +174,12 @@ function applyNodeMappings(
|
|
|
136
174
|
|
|
137
175
|
applyNodeMapping(
|
|
138
176
|
graph_nodes,
|
|
177
|
+
patram_config,
|
|
139
178
|
mapping_definition.node,
|
|
140
179
|
claim,
|
|
141
180
|
document_entity_keys,
|
|
181
|
+
document_node_references,
|
|
182
|
+
title_priorities,
|
|
142
183
|
);
|
|
143
184
|
}
|
|
144
185
|
}
|
|
@@ -148,6 +189,7 @@ function applyNodeMappings(
|
|
|
148
189
|
* @param {Record<string, MappingDefinition>} mappings
|
|
149
190
|
* @param {PatramClaim[]} claims
|
|
150
191
|
* @param {Map<string, string>} document_entity_keys
|
|
192
|
+
* @param {Map<string, import('./document-node-identity.js').DocumentNodeReference>} document_node_references
|
|
151
193
|
* @param {Set<string>} document_paths
|
|
152
194
|
* @returns {GraphEdge[]}
|
|
153
195
|
*/
|
|
@@ -156,6 +198,7 @@ function createGraphEdges(
|
|
|
156
198
|
mappings,
|
|
157
199
|
claims,
|
|
158
200
|
document_entity_keys,
|
|
201
|
+
document_node_references,
|
|
159
202
|
document_paths,
|
|
160
203
|
) {
|
|
161
204
|
/** @type {GraphEdge[]} */
|
|
@@ -169,25 +212,26 @@ function createGraphEdges(
|
|
|
169
212
|
continue;
|
|
170
213
|
}
|
|
171
214
|
|
|
172
|
-
const source_document_node =
|
|
215
|
+
const source_document_node = getDocumentGraphNode(
|
|
173
216
|
graph_nodes,
|
|
174
|
-
|
|
175
|
-
|
|
217
|
+
document_node_references,
|
|
218
|
+
claim,
|
|
176
219
|
);
|
|
177
220
|
const target_reference = resolveTargetReference(
|
|
178
|
-
mapping_definition.emit.
|
|
221
|
+
mapping_definition.emit.target_class,
|
|
179
222
|
mapping_definition.emit.target,
|
|
180
223
|
claim,
|
|
181
224
|
document_entity_keys,
|
|
225
|
+
document_node_references,
|
|
182
226
|
document_paths,
|
|
183
227
|
);
|
|
184
228
|
const target_node = upsertNode(
|
|
185
229
|
graph_nodes,
|
|
186
|
-
|
|
230
|
+
target_reference.class_name,
|
|
187
231
|
target_reference.key,
|
|
188
232
|
);
|
|
189
233
|
|
|
190
|
-
|
|
234
|
+
setCanonicalPath(target_node, target_reference.path);
|
|
191
235
|
|
|
192
236
|
edge_number += 1;
|
|
193
237
|
graph_edges.push({
|
|
@@ -204,23 +248,129 @@ function createGraphEdges(
|
|
|
204
248
|
|
|
205
249
|
/**
|
|
206
250
|
* @param {Map<string, GraphNode>} graph_nodes
|
|
207
|
-
* @param {
|
|
251
|
+
* @param {PatramConfig} patram_config
|
|
252
|
+
* @param {{ field: string, key?: 'path' | 'value', class: string }} node_mapping
|
|
208
253
|
* @param {PatramClaim} claim
|
|
209
254
|
* @param {Map<string, string>} document_entity_keys
|
|
255
|
+
* @param {Map<string, import('./document-node-identity.js').DocumentNodeReference>} document_node_references
|
|
256
|
+
* @param {Map<string, number>} title_priorities
|
|
210
257
|
*/
|
|
211
258
|
function applyNodeMapping(
|
|
212
259
|
graph_nodes,
|
|
260
|
+
patram_config,
|
|
213
261
|
node_mapping,
|
|
214
262
|
claim,
|
|
215
263
|
document_entity_keys,
|
|
264
|
+
document_node_references,
|
|
265
|
+
title_priorities,
|
|
216
266
|
) {
|
|
217
267
|
const source_key = normalizeRepoRelativePath(claim.origin.path);
|
|
218
|
-
const
|
|
219
|
-
|
|
268
|
+
const graph_node = resolveGraphNode(
|
|
269
|
+
graph_nodes,
|
|
270
|
+
node_mapping,
|
|
271
|
+
claim,
|
|
272
|
+
document_entity_keys,
|
|
273
|
+
document_node_references,
|
|
274
|
+
);
|
|
275
|
+
const field_value = resolveNodeFieldValue(
|
|
276
|
+
graph_node,
|
|
277
|
+
node_mapping,
|
|
278
|
+
claim,
|
|
279
|
+
source_key,
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
setCanonicalPath(graph_node, source_key);
|
|
283
|
+
validateNodeFieldMapping(
|
|
284
|
+
patram_config,
|
|
285
|
+
node_mapping.class,
|
|
286
|
+
node_mapping.field,
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
if (node_mapping.field === 'title') {
|
|
290
|
+
setNodeTitle(
|
|
291
|
+
graph_node,
|
|
292
|
+
title_priorities,
|
|
293
|
+
field_value,
|
|
294
|
+
claim.type === 'document.title'
|
|
295
|
+
? DERIVED_TITLE_PRIORITY
|
|
296
|
+
: EXPLICIT_TITLE_PRIORITY,
|
|
297
|
+
);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
setNodeFieldValue(
|
|
302
|
+
graph_node,
|
|
303
|
+
node_mapping.field,
|
|
304
|
+
field_value,
|
|
305
|
+
getFieldDefinition(patram_config, node_mapping.field),
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Validate one mapped node field against the configured field model.
|
|
311
|
+
*
|
|
312
|
+
* @param {PatramConfig} patram_config
|
|
313
|
+
* @param {string} node_class
|
|
314
|
+
* @param {string} field_name
|
|
315
|
+
*/
|
|
316
|
+
function validateNodeFieldMapping(patram_config, node_class, field_name) {
|
|
317
|
+
const validation_error = getNodeFieldValidationError(
|
|
318
|
+
patram_config,
|
|
319
|
+
node_class,
|
|
320
|
+
field_name,
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
if (validation_error) {
|
|
324
|
+
throw new Error(validation_error);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* @param {PatramConfig} patram_config
|
|
330
|
+
* @param {string} node_class
|
|
331
|
+
* @param {string} field_name
|
|
332
|
+
* @returns {string | null}
|
|
333
|
+
*/
|
|
334
|
+
function getNodeFieldValidationError(patram_config, node_class, field_name) {
|
|
335
|
+
if (isStructuralFieldName(field_name) || field_name === 'title') {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const field_definition = getFieldDefinition(patram_config, field_name);
|
|
340
|
+
|
|
341
|
+
if (!field_definition) {
|
|
342
|
+
return `Node class "${node_class}" maps to unknown field "${field_name}".`;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const class_schema = getClassSchema(patram_config, node_class);
|
|
346
|
+
const class_field_rule = class_schema?.fields?.[field_name];
|
|
347
|
+
|
|
348
|
+
if (isForbiddenClassField(class_field_rule)) {
|
|
349
|
+
return `Field "${field_name}" is forbidden for class "${node_class}".`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (isUndeclaredClassField(class_schema, class_field_rule)) {
|
|
353
|
+
return `Field "${field_name}" is not declared for class "${node_class}".`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
220
358
|
|
|
221
|
-
|
|
359
|
+
/**
|
|
360
|
+
* @param {{ presence: 'required' | 'optional' | 'forbidden' } | undefined} class_field_rule
|
|
361
|
+
* @returns {boolean}
|
|
362
|
+
*/
|
|
363
|
+
function isForbiddenClassField(class_field_rule) {
|
|
364
|
+
return class_field_rule?.presence === 'forbidden';
|
|
365
|
+
}
|
|
222
366
|
|
|
223
|
-
|
|
367
|
+
/**
|
|
368
|
+
* @param {{ fields?: Record<string, { presence: 'required' | 'optional' | 'forbidden' }>, unknown_fields?: 'ignore' | 'error' } | undefined} class_schema
|
|
369
|
+
* @param {{ presence: 'required' | 'optional' | 'forbidden' } | undefined} class_field_rule
|
|
370
|
+
* @returns {boolean}
|
|
371
|
+
*/
|
|
372
|
+
function isUndeclaredClassField(class_schema, class_field_rule) {
|
|
373
|
+
return class_schema?.unknown_fields === 'error' && !class_field_rule;
|
|
224
374
|
}
|
|
225
375
|
|
|
226
376
|
/**
|
|
@@ -246,23 +396,26 @@ function upsertNode(graph_nodes, kind_name, node_key) {
|
|
|
246
396
|
|
|
247
397
|
/**
|
|
248
398
|
* @param {string} node_id
|
|
249
|
-
* @param {string}
|
|
399
|
+
* @param {string} class_name
|
|
250
400
|
* @param {string} node_key
|
|
251
401
|
* @returns {GraphNode}
|
|
252
402
|
*/
|
|
253
|
-
function createNode(node_id,
|
|
254
|
-
if (
|
|
403
|
+
function createNode(node_id, class_name, node_key) {
|
|
404
|
+
if (class_name === 'document') {
|
|
255
405
|
return {
|
|
406
|
+
$class: class_name,
|
|
407
|
+
$id: node_id,
|
|
408
|
+
$path: node_key,
|
|
256
409
|
id: node_id,
|
|
257
|
-
kind: kind_name,
|
|
258
410
|
path: node_key,
|
|
259
411
|
};
|
|
260
412
|
}
|
|
261
413
|
|
|
262
414
|
return {
|
|
415
|
+
$class: class_name,
|
|
416
|
+
$id: node_id,
|
|
263
417
|
id: node_id,
|
|
264
418
|
key: node_key,
|
|
265
|
-
kind: kind_name,
|
|
266
419
|
};
|
|
267
420
|
}
|
|
268
421
|
|
|
@@ -291,11 +444,363 @@ function getNodeFieldValue(claim) {
|
|
|
291
444
|
throw new Error(`Claim "${claim.id}" does not carry a string value.`);
|
|
292
445
|
}
|
|
293
446
|
|
|
447
|
+
/**
|
|
448
|
+
* @param {Map<string, import('./document-node-identity.js').DocumentNodeReference>} document_node_references
|
|
449
|
+
* @returns {{ configurable: false, enumerable: false, value: Record<string, string>, writable: false }}
|
|
450
|
+
*/
|
|
451
|
+
function createDocumentNodeIdsProperty(document_node_references) {
|
|
452
|
+
return {
|
|
453
|
+
configurable: false,
|
|
454
|
+
enumerable: false,
|
|
455
|
+
value: Object.fromEntries(
|
|
456
|
+
[...document_node_references.entries()].map(
|
|
457
|
+
([document_path, document_node_reference]) => [
|
|
458
|
+
document_path,
|
|
459
|
+
document_node_reference.id,
|
|
460
|
+
],
|
|
461
|
+
),
|
|
462
|
+
),
|
|
463
|
+
writable: false,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* @param {Map<string, GraphNode>} graph_nodes
|
|
469
|
+
* @param {Map<string, import('./document-node-identity.js').DocumentNodeReference>} document_node_references
|
|
470
|
+
* @param {PatramClaim} claim
|
|
471
|
+
* @returns {GraphNode}
|
|
472
|
+
*/
|
|
473
|
+
function getDocumentGraphNode(graph_nodes, document_node_references, claim) {
|
|
474
|
+
const document_path = normalizeRepoRelativePath(claim.origin.path);
|
|
475
|
+
const document_node_reference = document_node_references.get(document_path);
|
|
476
|
+
|
|
477
|
+
if (!document_node_reference) {
|
|
478
|
+
throw new Error(`Missing document node reference for "${document_path}".`);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const graph_node = upsertNode(
|
|
482
|
+
graph_nodes,
|
|
483
|
+
document_node_reference.class_name,
|
|
484
|
+
document_node_reference.key,
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
setCanonicalPath(graph_node, document_node_reference.path);
|
|
488
|
+
|
|
489
|
+
return graph_node;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* @param {Map<string, GraphNode>} graph_nodes
|
|
494
|
+
* @param {{ field: string, key?: 'path' | 'value', class: string }} node_mapping
|
|
495
|
+
* @param {PatramClaim} claim
|
|
496
|
+
* @param {Map<string, string>} document_entity_keys
|
|
497
|
+
* @param {Map<string, import('./document-node-identity.js').DocumentNodeReference>} document_node_references
|
|
498
|
+
* @returns {GraphNode}
|
|
499
|
+
*/
|
|
500
|
+
function resolveGraphNode(
|
|
501
|
+
graph_nodes,
|
|
502
|
+
node_mapping,
|
|
503
|
+
claim,
|
|
504
|
+
document_entity_keys,
|
|
505
|
+
document_node_references,
|
|
506
|
+
) {
|
|
507
|
+
if (node_mapping.class === 'document') {
|
|
508
|
+
return getDocumentGraphNode(graph_nodes, document_node_references, claim);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const node_key = resolveNodeKey(node_mapping, claim, document_entity_keys);
|
|
512
|
+
|
|
513
|
+
return upsertNode(graph_nodes, node_mapping.class, node_key);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* @param {GraphNode} graph_node
|
|
518
|
+
* @param {{ field: string, key?: 'path' | 'value', class: string }} node_mapping
|
|
519
|
+
* @param {PatramClaim} claim
|
|
520
|
+
* @param {string} source_path
|
|
521
|
+
* @returns {string}
|
|
522
|
+
*/
|
|
523
|
+
function resolveNodeFieldValue(graph_node, node_mapping, claim, source_path) {
|
|
524
|
+
if (node_mapping.field === '$id') {
|
|
525
|
+
return graph_node.id;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (node_mapping.field === '$class') {
|
|
529
|
+
return graph_node.$class ?? graph_node.kind ?? node_mapping.class;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (node_mapping.field === '$path') {
|
|
533
|
+
return source_path;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return getNodeFieldValue(claim);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* @param {Record<string, GraphNode>} graph_nodes
|
|
541
|
+
* @param {Map<string, import('./document-node-identity.js').DocumentNodeReference>} document_node_references
|
|
542
|
+
*/
|
|
543
|
+
function attachDocumentNodeAliases(graph_nodes, document_node_references) {
|
|
544
|
+
for (const [
|
|
545
|
+
document_path,
|
|
546
|
+
document_node_reference,
|
|
547
|
+
] of document_node_references) {
|
|
548
|
+
const document_node_id = `doc:${document_path}`;
|
|
549
|
+
|
|
550
|
+
if (document_node_id === document_node_reference.id) {
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
Object.defineProperty(graph_nodes, document_node_id, {
|
|
555
|
+
configurable: false,
|
|
556
|
+
enumerable: false,
|
|
557
|
+
value: graph_nodes[document_node_reference.id],
|
|
558
|
+
writable: false,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* @param {Map<string, GraphNode>} graph_nodes
|
|
565
|
+
* @param {Map<string, number>} title_priorities
|
|
566
|
+
*/
|
|
567
|
+
function applyFallbackTitles(graph_nodes, title_priorities) {
|
|
568
|
+
for (const graph_node of graph_nodes.values()) {
|
|
569
|
+
if (graph_node.title !== undefined) {
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const fallback_title = getFallbackTitle(graph_node);
|
|
574
|
+
|
|
575
|
+
graph_node.title = fallback_title;
|
|
576
|
+
title_priorities.set(graph_node.id, 0);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* @param {GraphNode} graph_node
|
|
582
|
+
* @returns {string}
|
|
583
|
+
*/
|
|
584
|
+
function getFallbackTitle(graph_node) {
|
|
585
|
+
if (graph_node.$path) {
|
|
586
|
+
return posix.basename(graph_node.$path);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return getNodeIdKey(graph_node.$id ?? graph_node.id);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* @param {GraphNode} graph_node
|
|
594
|
+
* @param {Map<string, number>} title_priorities
|
|
595
|
+
* @param {string} title_value
|
|
596
|
+
* @param {number} source_priority
|
|
597
|
+
*/
|
|
598
|
+
function setNodeTitle(
|
|
599
|
+
graph_node,
|
|
600
|
+
title_priorities,
|
|
601
|
+
title_value,
|
|
602
|
+
source_priority,
|
|
603
|
+
) {
|
|
604
|
+
const current_priority = title_priorities.get(graph_node.id);
|
|
605
|
+
|
|
606
|
+
if (current_priority === undefined) {
|
|
607
|
+
graph_node.title = title_value;
|
|
608
|
+
title_priorities.set(graph_node.id, source_priority);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (source_priority > current_priority) {
|
|
613
|
+
graph_node.title = title_value;
|
|
614
|
+
title_priorities.set(graph_node.id, source_priority);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (
|
|
619
|
+
source_priority === current_priority &&
|
|
620
|
+
graph_node.title !== title_value
|
|
621
|
+
) {
|
|
622
|
+
throw new Error(
|
|
623
|
+
`Node "${graph_node.id}" has conflicting title values "${graph_node.title}" and "${title_value}".`,
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* @param {GraphNode} graph_node
|
|
630
|
+
* @param {string} field_name
|
|
631
|
+
* @param {string} field_value
|
|
632
|
+
* @param {MetadataFieldConfig | undefined} field_definition
|
|
633
|
+
*/
|
|
634
|
+
function setNodeFieldValue(
|
|
635
|
+
graph_node,
|
|
636
|
+
field_name,
|
|
637
|
+
field_value,
|
|
638
|
+
field_definition,
|
|
639
|
+
) {
|
|
640
|
+
if (
|
|
641
|
+
field_name === '$id' ||
|
|
642
|
+
field_name === '$class' ||
|
|
643
|
+
field_name === '$path'
|
|
644
|
+
) {
|
|
645
|
+
setStructuralFieldValue(graph_node, field_name, field_value);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (!field_definition || field_definition.multiple !== true) {
|
|
650
|
+
setSingleValueField(graph_node, field_name, field_value);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
setMultiValueField(graph_node, field_name, field_value);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* @param {GraphNode} graph_node
|
|
659
|
+
* @param {'$id' | '$class' | '$path'} field_name
|
|
660
|
+
* @param {string} field_value
|
|
661
|
+
*/
|
|
662
|
+
function setStructuralFieldValue(graph_node, field_name, field_value) {
|
|
663
|
+
const current_value = graph_node[field_name];
|
|
664
|
+
|
|
665
|
+
if (current_value === undefined) {
|
|
666
|
+
assignStructuralFieldValue(graph_node, field_name, field_value);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (
|
|
671
|
+
field_name === '$class' &&
|
|
672
|
+
graph_node.id.startsWith('doc:') &&
|
|
673
|
+
current_value === 'document'
|
|
674
|
+
) {
|
|
675
|
+
assignStructuralFieldValue(graph_node, field_name, field_value);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (current_value !== field_value) {
|
|
680
|
+
throw new Error(
|
|
681
|
+
`Node "${graph_node.id}" has conflicting structural values for "${field_name}".`,
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Keep legacy mirrors in sync while the rest of the codebase still reads them.
|
|
688
|
+
*
|
|
689
|
+
* @param {GraphNode} graph_node
|
|
690
|
+
* @param {'$id' | '$class' | '$path'} field_name
|
|
691
|
+
* @param {string} field_value
|
|
692
|
+
*/
|
|
693
|
+
function assignStructuralFieldValue(graph_node, field_name, field_value) {
|
|
694
|
+
graph_node[field_name] = field_value;
|
|
695
|
+
|
|
696
|
+
if (field_name === '$path') {
|
|
697
|
+
graph_node.path = field_value;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* @param {GraphNode} graph_node
|
|
703
|
+
* @param {string} field_name
|
|
704
|
+
* @param {string} field_value
|
|
705
|
+
*/
|
|
706
|
+
function setSingleValueField(graph_node, field_name, field_value) {
|
|
707
|
+
const current_value = graph_node[field_name];
|
|
708
|
+
|
|
709
|
+
if (current_value === undefined) {
|
|
710
|
+
graph_node[field_name] = field_value;
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if (current_value !== field_value) {
|
|
715
|
+
throw new Error(
|
|
716
|
+
`Node "${graph_node.id}" has conflicting values for field "${field_name}": "${current_value}" and "${field_value}".`,
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* @param {GraphNode} graph_node
|
|
723
|
+
* @param {string} field_name
|
|
724
|
+
* @param {string} field_value
|
|
725
|
+
*/
|
|
726
|
+
function setMultiValueField(graph_node, field_name, field_value) {
|
|
727
|
+
const current_value = graph_node[field_name];
|
|
728
|
+
|
|
729
|
+
if (current_value === undefined) {
|
|
730
|
+
graph_node[field_name] = [field_value];
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (Array.isArray(current_value)) {
|
|
735
|
+
if (current_value.includes(field_value)) {
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
graph_node[field_name] = [...current_value, field_value].sort(
|
|
740
|
+
compareFieldValues,
|
|
741
|
+
);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
graph_node[field_name] = [current_value, field_value].sort(
|
|
746
|
+
compareFieldValues,
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* @param {string} left_value
|
|
752
|
+
* @param {string} right_value
|
|
753
|
+
* @returns {number}
|
|
754
|
+
*/
|
|
755
|
+
function compareFieldValues(left_value, right_value) {
|
|
756
|
+
return left_value.localeCompare(right_value, DETERMINISTIC_LOCALE);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* @param {PatramConfig} patram_config
|
|
761
|
+
* @param {string} field_name
|
|
762
|
+
* @returns {MetadataFieldConfig | undefined}
|
|
763
|
+
*/
|
|
764
|
+
function getFieldDefinition(patram_config, field_name) {
|
|
765
|
+
return patram_config.fields?.[field_name];
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* @param {PatramConfig} patram_config
|
|
770
|
+
* @param {string} class_name
|
|
771
|
+
* @returns {{ fields?: Record<string, { presence: 'required' | 'optional' | 'forbidden' }>, unknown_fields?: 'ignore' | 'error' } | undefined}
|
|
772
|
+
*/
|
|
773
|
+
function getClassSchema(patram_config, class_name) {
|
|
774
|
+
return patram_config.class_schemas?.[class_name];
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* @param {string} field_name
|
|
779
|
+
* @returns {field_name is '$class' | '$id' | '$path'}
|
|
780
|
+
*/
|
|
781
|
+
function isStructuralFieldName(field_name) {
|
|
782
|
+
return STRUCTURAL_FIELD_NAMES.has(field_name);
|
|
783
|
+
}
|
|
784
|
+
|
|
294
785
|
/**
|
|
295
786
|
* @param {[string, GraphNode]} left_entry
|
|
296
787
|
* @param {[string, GraphNode]} right_entry
|
|
297
788
|
* @returns {number}
|
|
298
789
|
*/
|
|
299
790
|
function compareNodeEntries(left_entry, right_entry) {
|
|
300
|
-
return left_entry[0].localeCompare(right_entry[0],
|
|
791
|
+
return left_entry[0].localeCompare(right_entry[0], DETERMINISTIC_LOCALE);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* @param {string} node_id
|
|
796
|
+
* @returns {string}
|
|
797
|
+
*/
|
|
798
|
+
function getNodeIdKey(node_id) {
|
|
799
|
+
const separator_index = node_id.indexOf(':');
|
|
800
|
+
|
|
801
|
+
if (separator_index < 0) {
|
|
802
|
+
return node_id;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
return node_id.slice(separator_index + 1);
|
|
301
806
|
}
|