patram 0.2.0 → 0.3.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 +20 -19
- package/lib/build-graph.js +369 -16
- package/lib/build-graph.types.ts +5 -2
- package/lib/check-directive-metadata.js +516 -0
- package/lib/check-directive-value.js +282 -0
- package/lib/check-graph.js +24 -5
- package/lib/cli-help-metadata.js +44 -16
- 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 +427 -0
- package/lib/discover-fields.types.ts +52 -0
- package/lib/format-node-header.js +9 -7
- package/lib/format-output-metadata.js +15 -23
- package/lib/layout-stored-queries.js +6 -60
- 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 +117 -51
- package/lib/parse-where-clause.types.ts +4 -2
- package/lib/patram-cli.js +36 -4
- package/lib/patram-config.js +31 -31
- package/lib/patram-config.types.ts +10 -4
- package/lib/query-graph.js +241 -22
- package/lib/query-inspection.js +285 -10
- package/lib/render-field-discovery.js +148 -0
- package/lib/render-json-output.js +21 -22
- package/lib/render-output-view.js +240 -19
- package/lib/render-plain-output.js +1 -1
- package/lib/render-rich-output.js +1 -1
- package/lib/resolve-patram-graph-config.js +15 -9
- package/lib/show-document.js +51 -7
- package/package.json +5 -5
|
@@ -27,14 +27,14 @@ export function collectDocumentEntityKeys(mappings, claims) {
|
|
|
27
27
|
const source_path = normalizeRepoRelativePath(claim.origin.path);
|
|
28
28
|
const entity_map_key = getDocumentEntityMapKey(
|
|
29
29
|
source_path,
|
|
30
|
-
mapping_definition.node.
|
|
30
|
+
mapping_definition.node.class,
|
|
31
31
|
);
|
|
32
32
|
const entity_key = getStringClaimValue(claim);
|
|
33
33
|
const existing_entity_key = document_entity_keys.get(entity_map_key);
|
|
34
34
|
|
|
35
35
|
if (existing_entity_key && existing_entity_key !== entity_key) {
|
|
36
36
|
throw new Error(
|
|
37
|
-
`Document "${source_path}" defines multiple ${mapping_definition.node.
|
|
37
|
+
`Document "${source_path}" defines multiple ${mapping_definition.node.class} ids.`,
|
|
38
38
|
);
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -47,7 +47,7 @@ export function collectDocumentEntityKeys(mappings, claims) {
|
|
|
47
47
|
/**
|
|
48
48
|
* Resolve the node key for one mapped claim.
|
|
49
49
|
*
|
|
50
|
-
* @param {{ field: string, key?: 'path' | 'value',
|
|
50
|
+
* @param {{ field: string, key?: 'path' | 'value', class: string }} node_mapping
|
|
51
51
|
* @param {PatramClaim} claim
|
|
52
52
|
* @param {Map<string, string>} document_entity_keys
|
|
53
53
|
* @returns {string}
|
|
@@ -55,7 +55,7 @@ export function collectDocumentEntityKeys(mappings, claims) {
|
|
|
55
55
|
export function resolveNodeKey(node_mapping, claim, document_entity_keys) {
|
|
56
56
|
const source_key = normalizeRepoRelativePath(claim.origin.path);
|
|
57
57
|
|
|
58
|
-
if (node_mapping.
|
|
58
|
+
if (node_mapping.class === 'document') {
|
|
59
59
|
return source_key;
|
|
60
60
|
}
|
|
61
61
|
|
|
@@ -65,7 +65,7 @@ export function resolveNodeKey(node_mapping, claim, document_entity_keys) {
|
|
|
65
65
|
|
|
66
66
|
return (
|
|
67
67
|
document_entity_keys.get(
|
|
68
|
-
getDocumentEntityMapKey(source_key, node_mapping.
|
|
68
|
+
getDocumentEntityMapKey(source_key, node_mapping.class),
|
|
69
69
|
) ?? source_key
|
|
70
70
|
);
|
|
71
71
|
}
|
|
@@ -73,7 +73,7 @@ export function resolveNodeKey(node_mapping, claim, document_entity_keys) {
|
|
|
73
73
|
/**
|
|
74
74
|
* Resolve one edge target key and canonical path.
|
|
75
75
|
*
|
|
76
|
-
* @param {string}
|
|
76
|
+
* @param {string} target_class
|
|
77
77
|
* @param {'path' | 'value'} target_type
|
|
78
78
|
* @param {PatramClaim} claim
|
|
79
79
|
* @param {Map<string, string>} document_entity_keys
|
|
@@ -81,18 +81,18 @@ export function resolveNodeKey(node_mapping, claim, document_entity_keys) {
|
|
|
81
81
|
* @returns {{ key: string, path?: string }}
|
|
82
82
|
*/
|
|
83
83
|
export function resolveTargetReference(
|
|
84
|
-
|
|
84
|
+
target_class,
|
|
85
85
|
target_type,
|
|
86
86
|
claim,
|
|
87
87
|
document_entity_keys,
|
|
88
88
|
document_paths,
|
|
89
89
|
) {
|
|
90
90
|
if (target_type === 'value') {
|
|
91
|
-
return resolveValueTargetReference(
|
|
91
|
+
return resolveValueTargetReference(target_class, claim);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
return resolvePathTargetReference(
|
|
95
|
-
|
|
95
|
+
target_class,
|
|
96
96
|
claim,
|
|
97
97
|
document_entity_keys,
|
|
98
98
|
document_paths,
|
|
@@ -106,16 +106,17 @@ export function resolveTargetReference(
|
|
|
106
106
|
* @param {string | undefined} source_path
|
|
107
107
|
*/
|
|
108
108
|
export function setNonDocumentPath(graph_node, source_path) {
|
|
109
|
-
if (!source_path || graph_node
|
|
109
|
+
if (!source_path || graph_node.$class === 'document') {
|
|
110
110
|
return;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
if (!graph_node
|
|
113
|
+
if (!graph_node.$path) {
|
|
114
|
+
graph_node.$path = source_path;
|
|
114
115
|
graph_node.path = source_path;
|
|
115
116
|
return;
|
|
116
117
|
}
|
|
117
118
|
|
|
118
|
-
if (graph_node
|
|
119
|
+
if (graph_node.$path !== source_path) {
|
|
119
120
|
throw new Error(
|
|
120
121
|
`Node "${graph_node.id}" maps to multiple canonical paths.`,
|
|
121
122
|
);
|
|
@@ -180,14 +181,14 @@ function resolveValueTargetReference(target_kind, claim) {
|
|
|
180
181
|
}
|
|
181
182
|
|
|
182
183
|
/**
|
|
183
|
-
* @param {string}
|
|
184
|
+
* @param {string} target_class
|
|
184
185
|
* @param {PatramClaim} claim
|
|
185
186
|
* @param {Map<string, string>} document_entity_keys
|
|
186
187
|
* @param {Set<string>} document_paths
|
|
187
188
|
* @returns {{ key: string, path?: string }}
|
|
188
189
|
*/
|
|
189
190
|
function resolvePathTargetReference(
|
|
190
|
-
|
|
191
|
+
target_class,
|
|
191
192
|
claim,
|
|
192
193
|
document_entity_keys,
|
|
193
194
|
document_paths,
|
|
@@ -199,7 +200,7 @@ function resolvePathTargetReference(
|
|
|
199
200
|
document_paths,
|
|
200
201
|
);
|
|
201
202
|
|
|
202
|
-
if (
|
|
203
|
+
if (target_class === 'document') {
|
|
203
204
|
return {
|
|
204
205
|
key: target_path,
|
|
205
206
|
path: target_path,
|
|
@@ -207,7 +208,7 @@ function resolvePathTargetReference(
|
|
|
207
208
|
}
|
|
208
209
|
|
|
209
210
|
const semantic_target_key = document_entity_keys.get(
|
|
210
|
-
getDocumentEntityMapKey(target_path,
|
|
211
|
+
getDocumentEntityMapKey(target_path, target_class),
|
|
211
212
|
);
|
|
212
213
|
|
|
213
214
|
return {
|
|
@@ -262,9 +263,9 @@ function getStringClaimValue(claim) {
|
|
|
262
263
|
|
|
263
264
|
/**
|
|
264
265
|
* @param {string} document_path
|
|
265
|
-
* @param {string}
|
|
266
|
+
* @param {string} class_name
|
|
266
267
|
* @returns {string}
|
|
267
268
|
*/
|
|
268
|
-
function getDocumentEntityMapKey(document_path,
|
|
269
|
-
return `${
|
|
269
|
+
function getDocumentEntityMapKey(document_path, class_name) {
|
|
270
|
+
return `${class_name}:${document_path}`;
|
|
270
271
|
}
|
package/lib/build-graph.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
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,
|
|
9
13
|
normalizeRepoRelativePath,
|
|
@@ -25,7 +29,7 @@ import {
|
|
|
25
29
|
* Uses Term: ../docs/reference/terms/graph.md
|
|
26
30
|
* Uses Term: ../docs/reference/terms/mapping.md
|
|
27
31
|
* Uses Term: ../docs/reference/terms/relation.md
|
|
28
|
-
* Tracked in: ../docs/plans/v0/source-anchor-
|
|
32
|
+
* Tracked in: ../docs/plans/v0/source-anchor-dogfood.md
|
|
29
33
|
* Decided by: ../docs/decisions/graph-materialization.md
|
|
30
34
|
* Implements: ../docs/tasks/v0/materialize-graph.md
|
|
31
35
|
* @patram
|
|
@@ -33,6 +37,11 @@ import {
|
|
|
33
37
|
* @see {@link ../docs/decisions/graph-materialization.md}
|
|
34
38
|
*/
|
|
35
39
|
|
|
40
|
+
const STRUCTURAL_FIELD_NAMES = new Set(['$class', '$id', '$path']);
|
|
41
|
+
const DETERMINISTIC_LOCALE = 'en';
|
|
42
|
+
const DERIVED_TITLE_PRIORITY = 1;
|
|
43
|
+
const EXPLICIT_TITLE_PRIORITY = 2;
|
|
44
|
+
|
|
36
45
|
/**
|
|
37
46
|
* Build a Patram graph from semantic config and parsed claims.
|
|
38
47
|
*
|
|
@@ -51,13 +60,16 @@ export function buildGraph(patram_config, claims) {
|
|
|
51
60
|
const document_paths = new Set(
|
|
52
61
|
claims.map((claim) => normalizeRepoRelativePath(claim.origin.path)),
|
|
53
62
|
);
|
|
63
|
+
/** @type {Map<string, number>} */
|
|
64
|
+
const title_priorities = new Map();
|
|
54
65
|
|
|
55
66
|
createDocumentNodes(graph_nodes, claims);
|
|
56
67
|
applyNodeMappings(
|
|
57
68
|
graph_nodes,
|
|
58
|
-
patram_config
|
|
69
|
+
patram_config,
|
|
59
70
|
claims,
|
|
60
71
|
document_entity_keys,
|
|
72
|
+
title_priorities,
|
|
61
73
|
);
|
|
62
74
|
const graph_edges = createGraphEdges(
|
|
63
75
|
graph_nodes,
|
|
@@ -66,6 +78,7 @@ export function buildGraph(patram_config, claims) {
|
|
|
66
78
|
document_entity_keys,
|
|
67
79
|
document_paths,
|
|
68
80
|
);
|
|
81
|
+
applyFallbackTitles(graph_nodes, title_priorities);
|
|
69
82
|
|
|
70
83
|
return {
|
|
71
84
|
edges: graph_edges,
|
|
@@ -117,18 +130,23 @@ function createDocumentNodes(graph_nodes, claims) {
|
|
|
117
130
|
|
|
118
131
|
/**
|
|
119
132
|
* @param {Map<string, GraphNode>} graph_nodes
|
|
120
|
-
* @param {
|
|
133
|
+
* @param {PatramConfig} patram_config
|
|
121
134
|
* @param {PatramClaim[]} claims
|
|
122
135
|
* @param {Map<string, string>} document_entity_keys
|
|
136
|
+
* @param {Map<string, number>} title_priorities
|
|
123
137
|
*/
|
|
124
138
|
function applyNodeMappings(
|
|
125
139
|
graph_nodes,
|
|
126
|
-
|
|
140
|
+
patram_config,
|
|
127
141
|
claims,
|
|
128
142
|
document_entity_keys,
|
|
143
|
+
title_priorities,
|
|
129
144
|
) {
|
|
130
145
|
for (const claim of claims) {
|
|
131
|
-
const mapping_definition = resolveMappingDefinition(
|
|
146
|
+
const mapping_definition = resolveMappingDefinition(
|
|
147
|
+
patram_config.mappings,
|
|
148
|
+
claim,
|
|
149
|
+
);
|
|
132
150
|
|
|
133
151
|
if (!mapping_definition?.node) {
|
|
134
152
|
continue;
|
|
@@ -136,9 +154,11 @@ function applyNodeMappings(
|
|
|
136
154
|
|
|
137
155
|
applyNodeMapping(
|
|
138
156
|
graph_nodes,
|
|
157
|
+
patram_config,
|
|
139
158
|
mapping_definition.node,
|
|
140
159
|
claim,
|
|
141
160
|
document_entity_keys,
|
|
161
|
+
title_priorities,
|
|
142
162
|
);
|
|
143
163
|
}
|
|
144
164
|
}
|
|
@@ -175,7 +195,7 @@ function createGraphEdges(
|
|
|
175
195
|
normalizeRepoRelativePath(claim.origin.path),
|
|
176
196
|
);
|
|
177
197
|
const target_reference = resolveTargetReference(
|
|
178
|
-
mapping_definition.emit.
|
|
198
|
+
mapping_definition.emit.target_class,
|
|
179
199
|
mapping_definition.emit.target,
|
|
180
200
|
claim,
|
|
181
201
|
document_entity_keys,
|
|
@@ -183,7 +203,7 @@ function createGraphEdges(
|
|
|
183
203
|
);
|
|
184
204
|
const target_node = upsertNode(
|
|
185
205
|
graph_nodes,
|
|
186
|
-
mapping_definition.emit.
|
|
206
|
+
mapping_definition.emit.target_class,
|
|
187
207
|
target_reference.key,
|
|
188
208
|
);
|
|
189
209
|
|
|
@@ -204,23 +224,117 @@ function createGraphEdges(
|
|
|
204
224
|
|
|
205
225
|
/**
|
|
206
226
|
* @param {Map<string, GraphNode>} graph_nodes
|
|
207
|
-
* @param {
|
|
227
|
+
* @param {PatramConfig} patram_config
|
|
228
|
+
* @param {{ field: string, key?: 'path' | 'value', class: string }} node_mapping
|
|
208
229
|
* @param {PatramClaim} claim
|
|
209
230
|
* @param {Map<string, string>} document_entity_keys
|
|
231
|
+
* @param {Map<string, number>} title_priorities
|
|
210
232
|
*/
|
|
211
233
|
function applyNodeMapping(
|
|
212
234
|
graph_nodes,
|
|
235
|
+
patram_config,
|
|
213
236
|
node_mapping,
|
|
214
237
|
claim,
|
|
215
238
|
document_entity_keys,
|
|
239
|
+
title_priorities,
|
|
216
240
|
) {
|
|
217
241
|
const source_key = normalizeRepoRelativePath(claim.origin.path);
|
|
218
242
|
const node_key = resolveNodeKey(node_mapping, claim, document_entity_keys);
|
|
219
|
-
const graph_node = upsertNode(graph_nodes, node_mapping.
|
|
243
|
+
const graph_node = upsertNode(graph_nodes, node_mapping.class, node_key);
|
|
244
|
+
const field_value = getNodeFieldValue(claim);
|
|
220
245
|
|
|
221
246
|
setNonDocumentPath(graph_node, source_key);
|
|
247
|
+
validateNodeFieldMapping(
|
|
248
|
+
patram_config,
|
|
249
|
+
node_mapping.class,
|
|
250
|
+
node_mapping.field,
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
if (node_mapping.field === 'title') {
|
|
254
|
+
setNodeTitle(
|
|
255
|
+
graph_node,
|
|
256
|
+
title_priorities,
|
|
257
|
+
field_value,
|
|
258
|
+
claim.type === 'document.title'
|
|
259
|
+
? DERIVED_TITLE_PRIORITY
|
|
260
|
+
: EXPLICIT_TITLE_PRIORITY,
|
|
261
|
+
);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
setNodeFieldValue(
|
|
266
|
+
graph_node,
|
|
267
|
+
node_mapping.field,
|
|
268
|
+
field_value,
|
|
269
|
+
getFieldDefinition(patram_config, node_mapping.field),
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Validate one mapped node field against the configured field model.
|
|
275
|
+
*
|
|
276
|
+
* @param {PatramConfig} patram_config
|
|
277
|
+
* @param {string} node_class
|
|
278
|
+
* @param {string} field_name
|
|
279
|
+
*/
|
|
280
|
+
function validateNodeFieldMapping(patram_config, node_class, field_name) {
|
|
281
|
+
const validation_error = getNodeFieldValidationError(
|
|
282
|
+
patram_config,
|
|
283
|
+
node_class,
|
|
284
|
+
field_name,
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
if (validation_error) {
|
|
288
|
+
throw new Error(validation_error);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* @param {PatramConfig} patram_config
|
|
294
|
+
* @param {string} node_class
|
|
295
|
+
* @param {string} field_name
|
|
296
|
+
* @returns {string | null}
|
|
297
|
+
*/
|
|
298
|
+
function getNodeFieldValidationError(patram_config, node_class, field_name) {
|
|
299
|
+
if (isStructuralFieldName(field_name) || field_name === 'title') {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const field_definition = getFieldDefinition(patram_config, field_name);
|
|
304
|
+
|
|
305
|
+
if (!field_definition) {
|
|
306
|
+
return `Node class "${node_class}" maps to unknown field "${field_name}".`;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const class_schema = getClassSchema(patram_config, node_class);
|
|
310
|
+
const class_field_rule = class_schema?.fields?.[field_name];
|
|
311
|
+
|
|
312
|
+
if (isForbiddenClassField(class_field_rule)) {
|
|
313
|
+
return `Field "${field_name}" is forbidden for class "${node_class}".`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (isUndeclaredClassField(class_schema, class_field_rule)) {
|
|
317
|
+
return `Field "${field_name}" is not declared for class "${node_class}".`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* @param {{ presence: 'required' | 'optional' | 'forbidden' } | undefined} class_field_rule
|
|
325
|
+
* @returns {boolean}
|
|
326
|
+
*/
|
|
327
|
+
function isForbiddenClassField(class_field_rule) {
|
|
328
|
+
return class_field_rule?.presence === 'forbidden';
|
|
329
|
+
}
|
|
222
330
|
|
|
223
|
-
|
|
331
|
+
/**
|
|
332
|
+
* @param {{ fields?: Record<string, { presence: 'required' | 'optional' | 'forbidden' }>, unknown_fields?: 'ignore' | 'error' } | undefined} class_schema
|
|
333
|
+
* @param {{ presence: 'required' | 'optional' | 'forbidden' } | undefined} class_field_rule
|
|
334
|
+
* @returns {boolean}
|
|
335
|
+
*/
|
|
336
|
+
function isUndeclaredClassField(class_schema, class_field_rule) {
|
|
337
|
+
return class_schema?.unknown_fields === 'error' && !class_field_rule;
|
|
224
338
|
}
|
|
225
339
|
|
|
226
340
|
/**
|
|
@@ -246,23 +360,26 @@ function upsertNode(graph_nodes, kind_name, node_key) {
|
|
|
246
360
|
|
|
247
361
|
/**
|
|
248
362
|
* @param {string} node_id
|
|
249
|
-
* @param {string}
|
|
363
|
+
* @param {string} class_name
|
|
250
364
|
* @param {string} node_key
|
|
251
365
|
* @returns {GraphNode}
|
|
252
366
|
*/
|
|
253
|
-
function createNode(node_id,
|
|
254
|
-
if (
|
|
367
|
+
function createNode(node_id, class_name, node_key) {
|
|
368
|
+
if (class_name === 'document') {
|
|
255
369
|
return {
|
|
370
|
+
$class: class_name,
|
|
371
|
+
$id: node_id,
|
|
372
|
+
$path: node_key,
|
|
256
373
|
id: node_id,
|
|
257
|
-
kind: kind_name,
|
|
258
374
|
path: node_key,
|
|
259
375
|
};
|
|
260
376
|
}
|
|
261
377
|
|
|
262
378
|
return {
|
|
379
|
+
$class: class_name,
|
|
380
|
+
$id: node_id,
|
|
263
381
|
id: node_id,
|
|
264
382
|
key: node_key,
|
|
265
|
-
kind: kind_name,
|
|
266
383
|
};
|
|
267
384
|
}
|
|
268
385
|
|
|
@@ -291,11 +408,247 @@ function getNodeFieldValue(claim) {
|
|
|
291
408
|
throw new Error(`Claim "${claim.id}" does not carry a string value.`);
|
|
292
409
|
}
|
|
293
410
|
|
|
411
|
+
/**
|
|
412
|
+
* @param {Map<string, GraphNode>} graph_nodes
|
|
413
|
+
* @param {Map<string, number>} title_priorities
|
|
414
|
+
*/
|
|
415
|
+
function applyFallbackTitles(graph_nodes, title_priorities) {
|
|
416
|
+
for (const graph_node of graph_nodes.values()) {
|
|
417
|
+
if (graph_node.title !== undefined) {
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const fallback_title = getFallbackTitle(graph_node);
|
|
422
|
+
|
|
423
|
+
graph_node.title = fallback_title;
|
|
424
|
+
title_priorities.set(graph_node.id, 0);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* @param {GraphNode} graph_node
|
|
430
|
+
* @returns {string}
|
|
431
|
+
*/
|
|
432
|
+
function getFallbackTitle(graph_node) {
|
|
433
|
+
if (graph_node.$path) {
|
|
434
|
+
return posix.basename(graph_node.$path);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return getNodeIdKey(graph_node.$id ?? graph_node.id);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* @param {GraphNode} graph_node
|
|
442
|
+
* @param {Map<string, number>} title_priorities
|
|
443
|
+
* @param {string} title_value
|
|
444
|
+
* @param {number} source_priority
|
|
445
|
+
*/
|
|
446
|
+
function setNodeTitle(
|
|
447
|
+
graph_node,
|
|
448
|
+
title_priorities,
|
|
449
|
+
title_value,
|
|
450
|
+
source_priority,
|
|
451
|
+
) {
|
|
452
|
+
const current_priority = title_priorities.get(graph_node.id);
|
|
453
|
+
|
|
454
|
+
if (current_priority === undefined) {
|
|
455
|
+
graph_node.title = title_value;
|
|
456
|
+
title_priorities.set(graph_node.id, source_priority);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (source_priority > current_priority) {
|
|
461
|
+
graph_node.title = title_value;
|
|
462
|
+
title_priorities.set(graph_node.id, source_priority);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (
|
|
467
|
+
source_priority === current_priority &&
|
|
468
|
+
graph_node.title !== title_value
|
|
469
|
+
) {
|
|
470
|
+
throw new Error(
|
|
471
|
+
`Node "${graph_node.id}" has conflicting title values "${graph_node.title}" and "${title_value}".`,
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* @param {GraphNode} graph_node
|
|
478
|
+
* @param {string} field_name
|
|
479
|
+
* @param {string} field_value
|
|
480
|
+
* @param {MetadataFieldConfig | undefined} field_definition
|
|
481
|
+
*/
|
|
482
|
+
function setNodeFieldValue(
|
|
483
|
+
graph_node,
|
|
484
|
+
field_name,
|
|
485
|
+
field_value,
|
|
486
|
+
field_definition,
|
|
487
|
+
) {
|
|
488
|
+
if (
|
|
489
|
+
field_name === '$id' ||
|
|
490
|
+
field_name === '$class' ||
|
|
491
|
+
field_name === '$path'
|
|
492
|
+
) {
|
|
493
|
+
setStructuralFieldValue(graph_node, field_name, field_value);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (!field_definition || field_definition.multiple !== true) {
|
|
498
|
+
setSingleValueField(graph_node, field_name, field_value);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
setMultiValueField(graph_node, field_name, field_value);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* @param {GraphNode} graph_node
|
|
507
|
+
* @param {'$id' | '$class' | '$path'} field_name
|
|
508
|
+
* @param {string} field_value
|
|
509
|
+
*/
|
|
510
|
+
function setStructuralFieldValue(graph_node, field_name, field_value) {
|
|
511
|
+
const current_value = graph_node[field_name];
|
|
512
|
+
|
|
513
|
+
if (current_value === undefined) {
|
|
514
|
+
assignStructuralFieldValue(graph_node, field_name, field_value);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (
|
|
519
|
+
field_name === '$class' &&
|
|
520
|
+
graph_node.id.startsWith('doc:') &&
|
|
521
|
+
current_value === 'document'
|
|
522
|
+
) {
|
|
523
|
+
assignStructuralFieldValue(graph_node, field_name, field_value);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (current_value !== field_value) {
|
|
528
|
+
throw new Error(
|
|
529
|
+
`Node "${graph_node.id}" has conflicting structural values for "${field_name}".`,
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Keep legacy mirrors in sync while the rest of the codebase still reads them.
|
|
536
|
+
*
|
|
537
|
+
* @param {GraphNode} graph_node
|
|
538
|
+
* @param {'$id' | '$class' | '$path'} field_name
|
|
539
|
+
* @param {string} field_value
|
|
540
|
+
*/
|
|
541
|
+
function assignStructuralFieldValue(graph_node, field_name, field_value) {
|
|
542
|
+
graph_node[field_name] = field_value;
|
|
543
|
+
|
|
544
|
+
if (field_name === '$path') {
|
|
545
|
+
graph_node.path = field_value;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* @param {GraphNode} graph_node
|
|
551
|
+
* @param {string} field_name
|
|
552
|
+
* @param {string} field_value
|
|
553
|
+
*/
|
|
554
|
+
function setSingleValueField(graph_node, field_name, field_value) {
|
|
555
|
+
const current_value = graph_node[field_name];
|
|
556
|
+
|
|
557
|
+
if (current_value === undefined) {
|
|
558
|
+
graph_node[field_name] = field_value;
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (current_value !== field_value) {
|
|
563
|
+
throw new Error(
|
|
564
|
+
`Node "${graph_node.id}" has conflicting values for field "${field_name}": "${current_value}" and "${field_value}".`,
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* @param {GraphNode} graph_node
|
|
571
|
+
* @param {string} field_name
|
|
572
|
+
* @param {string} field_value
|
|
573
|
+
*/
|
|
574
|
+
function setMultiValueField(graph_node, field_name, field_value) {
|
|
575
|
+
const current_value = graph_node[field_name];
|
|
576
|
+
|
|
577
|
+
if (current_value === undefined) {
|
|
578
|
+
graph_node[field_name] = [field_value];
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (Array.isArray(current_value)) {
|
|
583
|
+
if (current_value.includes(field_value)) {
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
graph_node[field_name] = [...current_value, field_value].sort(
|
|
588
|
+
compareFieldValues,
|
|
589
|
+
);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
graph_node[field_name] = [current_value, field_value].sort(
|
|
594
|
+
compareFieldValues,
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* @param {string} left_value
|
|
600
|
+
* @param {string} right_value
|
|
601
|
+
* @returns {number}
|
|
602
|
+
*/
|
|
603
|
+
function compareFieldValues(left_value, right_value) {
|
|
604
|
+
return left_value.localeCompare(right_value, DETERMINISTIC_LOCALE);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* @param {PatramConfig} patram_config
|
|
609
|
+
* @param {string} field_name
|
|
610
|
+
* @returns {MetadataFieldConfig | undefined}
|
|
611
|
+
*/
|
|
612
|
+
function getFieldDefinition(patram_config, field_name) {
|
|
613
|
+
return patram_config.fields?.[field_name];
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* @param {PatramConfig} patram_config
|
|
618
|
+
* @param {string} class_name
|
|
619
|
+
* @returns {{ fields?: Record<string, { presence: 'required' | 'optional' | 'forbidden' }>, unknown_fields?: 'ignore' | 'error' } | undefined}
|
|
620
|
+
*/
|
|
621
|
+
function getClassSchema(patram_config, class_name) {
|
|
622
|
+
return patram_config.class_schemas?.[class_name];
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* @param {string} field_name
|
|
627
|
+
* @returns {field_name is '$class' | '$id' | '$path'}
|
|
628
|
+
*/
|
|
629
|
+
function isStructuralFieldName(field_name) {
|
|
630
|
+
return STRUCTURAL_FIELD_NAMES.has(field_name);
|
|
631
|
+
}
|
|
632
|
+
|
|
294
633
|
/**
|
|
295
634
|
* @param {[string, GraphNode]} left_entry
|
|
296
635
|
* @param {[string, GraphNode]} right_entry
|
|
297
636
|
* @returns {number}
|
|
298
637
|
*/
|
|
299
638
|
function compareNodeEntries(left_entry, right_entry) {
|
|
300
|
-
return left_entry[0].localeCompare(right_entry[0],
|
|
639
|
+
return left_entry[0].localeCompare(right_entry[0], DETERMINISTIC_LOCALE);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* @param {string} node_id
|
|
644
|
+
* @returns {string}
|
|
645
|
+
*/
|
|
646
|
+
function getNodeIdKey(node_id) {
|
|
647
|
+
const separator_index = node_id.indexOf(':');
|
|
648
|
+
|
|
649
|
+
if (separator_index < 0) {
|
|
650
|
+
return node_id;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return node_id.slice(separator_index + 1);
|
|
301
654
|
}
|
package/lib/build-graph.types.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import type { ClaimOrigin } from './parse-claims.types.ts';
|
|
2
2
|
|
|
3
3
|
export interface GraphNode {
|
|
4
|
+
$class?: string;
|
|
5
|
+
$id?: string;
|
|
6
|
+
$path?: string;
|
|
4
7
|
id: string;
|
|
5
|
-
kind
|
|
8
|
+
kind?: string;
|
|
6
9
|
key?: string;
|
|
7
10
|
path?: string;
|
|
8
11
|
title?: string;
|
|
9
|
-
[field: string]: string | undefined;
|
|
12
|
+
[field: string]: string | string[] | undefined;
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
export interface GraphEdge {
|