patram 0.11.0 → 0.12.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 (110) hide show
  1. package/bin/patram.js +4 -4
  2. package/lib/cli/commands/fields.js +0 -4
  3. package/lib/cli/commands/queries.js +10 -20
  4. package/lib/cli/commands/query.js +1 -8
  5. package/lib/cli/commands/refs.js +3 -10
  6. package/lib/cli/commands/show.js +1 -8
  7. package/lib/cli/help-metadata.js +71 -106
  8. package/lib/cli/main.js +10 -10
  9. package/lib/cli/parse-arguments-helpers.js +165 -59
  10. package/lib/cli/parse-arguments.js +4 -4
  11. package/lib/cli/render-help.js +2 -2
  12. package/lib/config/defaults.js +33 -25
  13. package/lib/config/load-patram-config.d.ts +8 -33
  14. package/lib/config/load-patram-config.js +9 -33
  15. package/lib/config/load-patram-config.types.d.ts +3 -40
  16. package/lib/config/manage-stored-queries-helpers.d.ts +4 -4
  17. package/lib/config/manage-stored-queries-helpers.js +91 -33
  18. package/lib/config/manage-stored-queries.d.ts +4 -4
  19. package/lib/config/manage-stored-queries.js +11 -5
  20. package/lib/config/patram-config.d.ts +34 -34
  21. package/lib/config/patram-config.js +3 -3
  22. package/lib/config/patram-config.types.d.ts +5 -11
  23. package/lib/config/resolve-patram-graph-config.d.ts +5 -1
  24. package/lib/config/resolve-patram-graph-config.js +3 -119
  25. package/lib/config/schema.d.ts +158 -269
  26. package/lib/config/schema.js +72 -210
  27. package/lib/config/validate-patram-config-value.js +6 -31
  28. package/lib/config/validation.d.ts +2 -12
  29. package/lib/config/validation.js +125 -483
  30. package/lib/find-close-match.d.ts +4 -1
  31. package/lib/graph/build-graph-identity.d.ts +1 -32
  32. package/lib/graph/build-graph-identity.js +5 -269
  33. package/lib/graph/build-graph.d.ts +13 -4
  34. package/lib/graph/build-graph.js +347 -488
  35. package/lib/graph/build-graph.types.d.ts +8 -9
  36. package/lib/graph/check-directive-metadata-helpers.d.ts +30 -0
  37. package/lib/graph/check-directive-metadata-helpers.js +126 -0
  38. package/lib/graph/check-directive-metadata.d.ts +8 -9
  39. package/lib/graph/check-directive-metadata.js +70 -561
  40. package/lib/graph/check-directive-path-target.d.ts +6 -13
  41. package/lib/graph/check-directive-path-target.js +26 -57
  42. package/lib/graph/check-directive-value.d.ts +1 -5
  43. package/lib/graph/check-directive-value.js +40 -180
  44. package/lib/graph/check-graph.d.ts +5 -5
  45. package/lib/graph/check-graph.js +8 -6
  46. package/lib/graph/document-node-identity.d.ts +23 -7
  47. package/lib/graph/document-node-identity.js +417 -160
  48. package/lib/graph/graph-node.d.ts +42 -0
  49. package/lib/graph/graph-node.js +83 -0
  50. package/lib/graph/inspect-reverse-references.js +16 -11
  51. package/lib/graph/load-project-graph.d.ts +7 -7
  52. package/lib/graph/load-project-graph.js +7 -7
  53. package/lib/graph/parse-where-clause.types.d.ts +3 -2
  54. package/lib/graph/query/cypher-reader.d.ts +59 -0
  55. package/lib/graph/query/cypher-reader.js +151 -0
  56. package/lib/graph/query/cypher-support.d.ts +79 -0
  57. package/lib/graph/query/cypher-support.js +213 -0
  58. package/lib/graph/query/cypher-tokenize.d.ts +13 -0
  59. package/lib/graph/query/cypher-tokenize.js +225 -0
  60. package/lib/graph/query/cypher.types.d.ts +43 -0
  61. package/lib/graph/query/execute.d.ts +7 -7
  62. package/lib/graph/query/execute.js +71 -33
  63. package/lib/graph/query/inspect.js +58 -24
  64. package/lib/graph/query/parse-cypher-patterns.d.ts +27 -0
  65. package/lib/graph/query/parse-cypher-patterns.js +382 -0
  66. package/lib/graph/query/parse-cypher.d.ts +7 -0
  67. package/lib/graph/query/parse-cypher.js +580 -0
  68. package/lib/graph/query/parse-query.d.ts +13 -0
  69. package/lib/graph/query/parse-query.js +97 -0
  70. package/lib/graph/query/resolve.js +77 -23
  71. package/lib/output/command-output.js +12 -5
  72. package/lib/output/compact-layout.js +221 -0
  73. package/lib/output/format-output-item-block.js +31 -1
  74. package/lib/output/format-output-metadata.js +16 -29
  75. package/lib/output/format-stored-query-block.js +95 -0
  76. package/lib/output/layout-incoming-references.js +101 -19
  77. package/lib/output/layout-stored-queries.js +23 -330
  78. package/lib/output/list-queries.js +1 -1
  79. package/lib/output/render-field-discovery.js +11 -2
  80. package/lib/output/render-output-view.js +9 -5
  81. package/lib/output/renderers/json.js +5 -26
  82. package/lib/output/renderers/plain.js +155 -35
  83. package/lib/output/renderers/rich.js +250 -36
  84. package/lib/output/resolved-link-layout.js +43 -0
  85. package/lib/output/rich-source/render.js +193 -35
  86. package/lib/output/show-document.js +25 -18
  87. package/lib/output/view-model/index.js +124 -103
  88. package/lib/parse/jsdoc/parse-jsdoc-blocks.js +1 -1
  89. package/lib/parse/jsdoc/parse-jsdoc-claims.js +12 -6
  90. package/lib/parse/markdown/parse-markdown-claims.js +99 -62
  91. package/lib/parse/markdown/parse-markdown-directives.d.ts +10 -6
  92. package/lib/parse/markdown/parse-markdown-directives.js +104 -18
  93. package/lib/parse/markdown/parse-markdown-prose.d.ts +27 -0
  94. package/lib/parse/markdown/parse-markdown-prose.js +243 -0
  95. package/lib/parse/parse-claims.d.ts +2 -6
  96. package/lib/parse/parse-claims.js +11 -53
  97. package/lib/parse/tagged-fenced/tagged-fenced-blocks.d.ts +4 -4
  98. package/lib/parse/tagged-fenced/tagged-fenced-blocks.js +4 -4
  99. package/lib/parse/yaml/parse-yaml-claims.js +4 -4
  100. package/lib/patram.d.ts +3 -5
  101. package/lib/patram.js +1 -1
  102. package/lib/scan/discover-fields.js +194 -55
  103. package/lib/scan/list-source-files.d.ts +4 -4
  104. package/lib/scan/list-source-files.js +4 -4
  105. package/package.json +1 -1
  106. package/lib/directive-validation-test-helpers.js +0 -87
  107. package/lib/graph/query/parse.d.ts +0 -75
  108. package/lib/graph/query/parse.js +0 -1064
  109. package/lib/output/derived-summary.js +0 -280
  110. package/lib/output/format-derived-summary-row.js +0 -9
@@ -1,9 +1,10 @@
1
+ /* eslint-disable complexity, max-lines */
1
2
  /**
2
3
  * @import { PatramClaim } from '../parse/parse-claims.types.ts';
3
- * @import { MappingDefinition } from '../config/patram-config.types.ts';
4
+ * @import { PatramConfig } from '../config/patram-config.types.ts';
4
5
  */
5
6
 
6
- import { posix } from 'node:path';
7
+ import { matchesGlob, posix } from 'node:path';
7
8
 
8
9
  /**
9
10
  * @typedef {{
@@ -17,39 +18,33 @@ import { posix } from 'node:path';
17
18
  /**
18
19
  * Collect semantic entity keys defined by canonical documents.
19
20
  *
20
- * @param {Record<string, MappingDefinition>} mappings
21
+ * @param {PatramConfig} patram_config
21
22
  * @param {PatramClaim[]} claims
22
23
  * @returns {Map<string, string>}
23
24
  */
24
- export function collectDocumentEntityKeys(mappings, claims) {
25
+ export function collectDocumentEntityKeys(patram_config, claims) {
25
26
  /** @type {Map<string, string>} */
26
27
  const document_entity_keys = new Map();
28
+ const document_node_references = collectDocumentNodeReferences(
29
+ patram_config,
30
+ claims,
31
+ );
27
32
 
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
- ) {
33
+ for (const [
34
+ document_path,
35
+ document_node_reference,
36
+ ] of document_node_references) {
37
+ if (document_node_reference.class_name === 'document') {
35
38
  continue;
36
39
  }
37
40
 
38
- const source_path = normalizeRepoRelativePath(claim.origin.path);
39
- const entity_map_key = getDocumentEntityMapKey(
40
- source_path,
41
- mapping_definition.node.class,
41
+ document_entity_keys.set(
42
+ getDocumentEntityMapKey(
43
+ document_path,
44
+ document_node_reference.class_name,
45
+ ),
46
+ document_node_reference.key,
42
47
  );
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
48
  }
54
49
 
55
50
  return document_entity_keys;
@@ -58,35 +53,19 @@ export function collectDocumentEntityKeys(mappings, claims) {
58
53
  /**
59
54
  * Collect canonical graph identities for document-backed source paths.
60
55
  *
61
- * @param {Record<string, MappingDefinition>} mappings
56
+ * @param {PatramConfig} patram_config
62
57
  * @param {PatramClaim[]} claims
63
58
  * @returns {Map<string, DocumentNodeReference>}
64
59
  */
65
- export function collectDocumentNodeReferences(mappings, claims) {
60
+ export function collectDocumentNodeReferences(patram_config, claims) {
66
61
  /** @type {Map<string, DocumentNodeReference>} */
67
62
  const document_node_references = new Map();
68
- /** @type {Map<string, string>} */
69
- const pending_document_keys = new Map();
63
+ const claims_by_path = groupClaimsByPath(claims);
70
64
 
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,
65
+ for (const [source_path, path_claims] of claims_by_path) {
66
+ document_node_references.set(
89
67
  source_path,
68
+ resolveDocumentNodeReference(patram_config, source_path, path_claims),
90
69
  );
91
70
  }
92
71
 
@@ -96,12 +75,12 @@ export function collectDocumentNodeReferences(mappings, claims) {
96
75
  /**
97
76
  * Resolve the canonical node id for a source document path.
98
77
  *
99
- * @param {Record<string, string> | undefined} document_node_ids
78
+ * @param {Record<string, string> | undefined} document_path_ids
100
79
  * @param {string} document_path
101
80
  * @returns {string}
102
81
  */
103
- export function resolveDocumentNodeId(document_node_ids, document_path) {
104
- return document_node_ids?.[document_path] ?? `doc:${document_path}`;
82
+ export function resolveDocumentNodeId(document_path_ids, document_path) {
83
+ return document_path_ids?.[document_path] ?? `doc:${document_path}`;
105
84
  }
106
85
 
107
86
  /**
@@ -115,192 +94,470 @@ export function normalizeRepoRelativePath(source_path) {
115
94
  }
116
95
 
117
96
  /**
118
- * @param {DocumentNodeReference} document_node_reference
119
- * @param {{ field: string, key?: 'path' | 'value', class: string }} node_mapping
97
+ * Resolve one edge target key and canonical path.
98
+ *
99
+ * @param {string} target_class
100
+ * @param {'path' | 'value'} target_type
120
101
  * @param {PatramClaim} claim
121
- * @param {Map<string, string>} pending_document_keys
122
- * @param {string} source_path
102
+ * @param {Map<string, string>} document_entity_keys
103
+ * @param {Map<string, DocumentNodeReference>} document_node_references
104
+ * @param {Set<string>} document_paths
105
+ * @returns {{ class_name: string, key: string, path?: string }}
123
106
  */
124
- function applyDocumentNodeMapping(
125
- document_node_reference,
126
- node_mapping,
107
+ export function resolveTargetReference(
108
+ target_class,
109
+ target_type,
127
110
  claim,
128
- pending_document_keys,
129
- source_path,
111
+ document_entity_keys,
112
+ document_node_references,
113
+ document_paths,
130
114
  ) {
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;
115
+ if (target_type === 'value') {
116
+ return resolveValueTargetReference(target_class, claim);
142
117
  }
143
118
 
144
- if (node_mapping.field !== '$id' || node_mapping.key !== 'value') {
145
- return;
119
+ return resolvePathTargetReference(
120
+ target_class,
121
+ claim,
122
+ document_entity_keys,
123
+ document_node_references,
124
+ document_paths,
125
+ );
126
+ }
127
+
128
+ /**
129
+ * @param {PatramConfig} patram_config
130
+ * @param {string} source_path
131
+ * @param {PatramClaim[]} claims
132
+ * @returns {DocumentNodeReference}
133
+ */
134
+ function resolveDocumentNodeReference(patram_config, source_path, claims) {
135
+ const defined_by_reference = resolveDefinedByReference(
136
+ patram_config.types,
137
+ claims,
138
+ source_path,
139
+ );
140
+
141
+ if (defined_by_reference) {
142
+ return defined_by_reference;
146
143
  }
147
144
 
148
- const document_node_key = getStringClaimValue(claim);
145
+ const path_type_reference = resolvePathTypeReference(
146
+ patram_config.types,
147
+ source_path,
148
+ );
149
+
150
+ if (path_type_reference) {
151
+ return path_type_reference;
152
+ }
153
+
154
+ return createDefaultDocumentNodeReference(source_path);
155
+ }
156
+
157
+ /**
158
+ * @param {PatramConfig['types']} types
159
+ * @param {PatramClaim[]} claims
160
+ * @param {string} source_path
161
+ * @returns {DocumentNodeReference | null}
162
+ */
163
+ function resolveDefinedByReference(types, claims, source_path) {
164
+ /** @type {DocumentNodeReference | null} */
165
+ let document_node_reference = null;
166
+
167
+ for (const [type_name, type_definition] of Object.entries(types ?? {})) {
168
+ if (!type_definition.defined_by) {
169
+ continue;
170
+ }
171
+
172
+ const node_key = resolveDefinedByKey(
173
+ type_definition.defined_by,
174
+ claims,
175
+ source_path,
176
+ type_name,
177
+ );
178
+
179
+ if (!node_key) {
180
+ continue;
181
+ }
149
182
 
150
- if (document_node_reference.class_name === 'document') {
151
- assignPendingDocumentKey(
152
- pending_document_keys,
183
+ const next_reference = createDocumentNodeReference(
184
+ type_name,
185
+ node_key,
153
186
  source_path,
154
- document_node_key,
155
187
  );
156
- return;
188
+
189
+ if (
190
+ document_node_reference &&
191
+ document_node_reference.id !== next_reference.id
192
+ ) {
193
+ throw new Error(
194
+ `Document "${source_path}" defines multiple semantic types.`,
195
+ );
196
+ }
197
+
198
+ document_node_reference = next_reference;
157
199
  }
158
200
 
159
- assignDocumentNodeKey(document_node_reference, document_node_key);
201
+ return document_node_reference;
160
202
  }
161
203
 
162
204
  /**
163
- * @param {DocumentNodeReference} document_node_reference
164
- * @param {Map<string, string>} pending_document_keys
205
+ * @param {string} field_name
206
+ * @param {PatramClaim[]} claims
165
207
  * @param {string} source_path
208
+ * @param {string} type_name
209
+ * @returns {string | null}
166
210
  */
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;
211
+ function resolveDefinedByKey(field_name, claims, source_path, type_name) {
212
+ /** @type {string | null} */
213
+ let node_key = null;
214
+
215
+ for (const claim of claims) {
216
+ if (claim.type !== 'directive' || claim.name !== field_name) {
217
+ continue;
218
+ }
219
+
220
+ const claim_value = getStringClaimValue(claim);
221
+
222
+ if (node_key && node_key !== claim_value) {
223
+ throw new Error(
224
+ `Document "${source_path}" defines multiple semantic ids for type "${type_name}".`,
225
+ );
226
+ }
227
+
228
+ node_key = claim_value;
174
229
  }
175
230
 
176
- const pending_document_key = pending_document_keys.get(source_path);
231
+ return node_key;
232
+ }
233
+
234
+ /**
235
+ * @param {PatramConfig['types']} types
236
+ * @param {string} source_path
237
+ * @returns {DocumentNodeReference | null}
238
+ */
239
+ function resolvePathTypeReference(types, source_path) {
240
+ /** @type {{ prefix: string, type_name: string } | null} */
241
+ let best_match = null;
242
+
243
+ for (const [type_name, type_definition] of Object.entries(types ?? {})) {
244
+ for (const pattern of type_definition.in ?? []) {
245
+ if (!matchesGlob(source_path, pattern)) {
246
+ continue;
247
+ }
248
+
249
+ const prefix = getGlobPrefix(pattern);
250
+
251
+ if (!best_match || prefix.length > best_match.prefix.length) {
252
+ best_match = {
253
+ prefix,
254
+ type_name,
255
+ };
256
+ continue;
257
+ }
258
+
259
+ if (
260
+ prefix.length === best_match.prefix.length &&
261
+ best_match.type_name !== type_name
262
+ ) {
263
+ throw new Error(
264
+ `Document "${source_path}" matches multiple path-backed types.`,
265
+ );
266
+ }
267
+ }
268
+ }
177
269
 
178
- if (!pending_document_key) {
179
- return;
270
+ if (!best_match) {
271
+ return null;
180
272
  }
181
273
 
182
- assignDocumentNodeKey(document_node_reference, pending_document_key);
274
+ return createDocumentNodeReference(
275
+ best_match.type_name,
276
+ deriveDocumentPathIdentityKey(source_path, best_match.prefix),
277
+ source_path,
278
+ );
183
279
  }
184
280
 
185
281
  /**
186
- * @param {Record<string, MappingDefinition>} mappings
282
+ * @param {string} target_class
187
283
  * @param {PatramClaim} claim
188
- * @returns {MappingDefinition | null}
284
+ * @returns {{ class_name: string, key: string, path?: string }}
189
285
  */
190
- function resolveMappingDefinition(mappings, claim) {
191
- if (claim.type === 'directive') {
192
- return resolveDirectiveMapping(mappings, claim);
286
+ function resolveValueTargetReference(target_class, claim) {
287
+ const target_key = getStringClaimValue(claim);
288
+
289
+ if (target_class === 'document') {
290
+ return {
291
+ class_name: 'document',
292
+ key: target_key,
293
+ path: target_key,
294
+ };
193
295
  }
194
296
 
195
- return mappings[claim.type] ?? null;
297
+ return {
298
+ class_name: target_class,
299
+ key: target_key,
300
+ path: normalizeRepoRelativePath(claim.origin.path),
301
+ };
196
302
  }
197
303
 
198
304
  /**
199
- * @param {Record<string, MappingDefinition>} mappings
305
+ * @param {string} target_class
200
306
  * @param {PatramClaim} claim
201
- * @returns {MappingDefinition | null}
307
+ * @param {Map<string, string>} document_entity_keys
308
+ * @param {Map<string, DocumentNodeReference>} document_node_references
309
+ * @param {Set<string>} document_paths
310
+ * @returns {{ class_name: string, key: string, path?: string }}
202
311
  */
203
- function resolveDirectiveMapping(mappings, claim) {
204
- if (!claim.parser || !claim.name) {
205
- return null;
312
+ function resolvePathTargetReference(
313
+ target_class,
314
+ claim,
315
+ document_entity_keys,
316
+ document_node_references,
317
+ document_paths,
318
+ ) {
319
+ const raw_target = getPathTargetValue(claim);
320
+ const target_path = resolveDirectiveAwareTargetPath(
321
+ claim,
322
+ raw_target,
323
+ document_paths,
324
+ );
325
+
326
+ if (target_class === 'document') {
327
+ return resolveDocumentTargetReference(
328
+ target_path,
329
+ document_node_references,
330
+ );
331
+ }
332
+
333
+ const document_node_reference = document_node_references.get(target_path);
334
+
335
+ if (document_node_reference?.class_name === target_class) {
336
+ return {
337
+ class_name: document_node_reference.class_name,
338
+ key: document_node_reference.key,
339
+ path: target_path,
340
+ };
206
341
  }
207
342
 
208
- return mappings[`${claim.parser}.directive.${claim.name}`] ?? null;
343
+ return {
344
+ class_name: target_class,
345
+ key:
346
+ document_entity_keys.get(
347
+ getDocumentEntityMapKey(target_path, target_class),
348
+ ) ?? target_path,
349
+ path: target_path,
350
+ };
351
+ }
352
+
353
+ /**
354
+ * @param {string} target_path
355
+ * @param {Map<string, DocumentNodeReference>} document_node_references
356
+ * @returns {{ class_name: string, key: string, path?: string }}
357
+ */
358
+ function resolveDocumentTargetReference(target_path, document_node_references) {
359
+ const document_node_reference = document_node_references.get(target_path);
360
+
361
+ if (!document_node_reference) {
362
+ return {
363
+ class_name: 'document',
364
+ key: target_path,
365
+ path: target_path,
366
+ };
367
+ }
368
+
369
+ return {
370
+ class_name: document_node_reference.class_name,
371
+ key: document_node_reference.key,
372
+ path: target_path,
373
+ };
209
374
  }
210
375
 
211
376
  /**
212
377
  * @param {PatramClaim} claim
378
+ * @param {string} raw_target
379
+ * @param {Set<string>} document_paths
213
380
  * @returns {string}
214
381
  */
215
- function getStringClaimValue(claim) {
216
- if (typeof claim.value === 'string') {
217
- return claim.value;
382
+ function resolveDirectiveAwareTargetPath(claim, raw_target, document_paths) {
383
+ const normalized_raw_target = normalizeRepoRelativePath(raw_target);
384
+
385
+ if (
386
+ claim.type === 'directive' &&
387
+ shouldKeepDirectiveTargetRepoRelative(
388
+ raw_target,
389
+ normalized_raw_target,
390
+ document_paths,
391
+ )
392
+ ) {
393
+ return normalized_raw_target;
218
394
  }
219
395
 
220
- throw new Error(`Claim "${claim.id}" does not carry a string value.`);
396
+ const source_directory = posix.dirname(
397
+ normalizeRepoRelativePath(claim.origin.path),
398
+ );
399
+
400
+ return normalizeRepoRelativePath(posix.join(source_directory, raw_target));
221
401
  }
222
402
 
223
403
  /**
224
- * @param {string} document_path
225
- * @param {string} class_name
226
- * @returns {string}
404
+ * @param {string} raw_target
405
+ * @param {string} normalized_raw_target
406
+ * @param {Set<string>} document_paths
407
+ * @returns {boolean}
227
408
  */
228
- function getDocumentEntityMapKey(document_path, class_name) {
229
- return `${class_name}:${document_path}`;
409
+ function shouldKeepDirectiveTargetRepoRelative(
410
+ raw_target,
411
+ normalized_raw_target,
412
+ document_paths,
413
+ ) {
414
+ if (raw_target.startsWith('./') || raw_target.startsWith('../')) {
415
+ return false;
416
+ }
417
+
418
+ if (document_paths.has(normalized_raw_target)) {
419
+ return true;
420
+ }
421
+
422
+ const target_root_segment = normalized_raw_target.split('/')[0];
423
+
424
+ if (!target_root_segment) {
425
+ return false;
426
+ }
427
+
428
+ for (const document_path of document_paths) {
429
+ if (document_path.split('/')[0] === target_root_segment) {
430
+ return true;
431
+ }
432
+ }
433
+
434
+ return false;
230
435
  }
231
436
 
232
437
  /**
233
438
  * @param {string} source_path
234
- * @returns {DocumentNodeReference}
439
+ * @param {string} path_prefix
440
+ * @returns {string}
235
441
  */
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
- };
442
+ function deriveDocumentPathIdentityKey(source_path, path_prefix) {
443
+ const normalized_prefix = normalizeRepoRelativePath(path_prefix);
444
+ const relative_path = source_path.startsWith(normalized_prefix)
445
+ ? source_path.slice(normalized_prefix.length)
446
+ : source_path;
447
+ const relative_directory = posix.dirname(relative_path);
448
+ const base_name = posix.basename(relative_path, posix.extname(relative_path));
449
+
450
+ if (relative_directory === '.') {
451
+ return base_name;
452
+ }
453
+
454
+ return posix.join(relative_directory, base_name);
243
455
  }
244
456
 
245
457
  /**
246
- * @param {DocumentNodeReference} document_node_reference
247
- * @param {string} class_name
458
+ * @param {string} glob_pattern
459
+ * @returns {string}
248
460
  */
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
- );
461
+ function getGlobPrefix(glob_pattern) {
462
+ const normalized_pattern = normalizeRepoRelativePath(glob_pattern);
463
+ const wildcard_index = normalized_pattern.search(/[*?[{]/du);
464
+
465
+ if (wildcard_index < 0) {
466
+ return normalized_pattern;
257
467
  }
258
468
 
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
- );
469
+ const prefix = normalized_pattern.slice(0, wildcard_index);
470
+
471
+ return prefix.endsWith('/')
472
+ ? prefix
473
+ : posix.dirname(prefix).replace(/\/?$/u, '/');
264
474
  }
265
475
 
266
476
  /**
267
- * @param {DocumentNodeReference} document_node_reference
268
- * @param {string} node_key
477
+ * @param {PatramClaim} claim
478
+ * @returns {string}
269
479
  */
270
- function assignDocumentNodeKey(document_node_reference, node_key) {
480
+ function getPathTargetValue(claim) {
481
+ if (typeof claim.value === 'string') {
482
+ return claim.value;
483
+ }
484
+
271
485
  if (
272
- document_node_reference.key !== document_node_reference.path &&
273
- document_node_reference.key !== node_key
486
+ claim.value &&
487
+ typeof claim.value === 'object' &&
488
+ 'target' in claim.value
274
489
  ) {
275
- throw new Error(
276
- `Document "${document_node_reference.path}" defines multiple semantic ids.`,
277
- );
490
+ const target_value = claim.value.target;
491
+
492
+ if (typeof target_value === 'string') {
493
+ return target_value;
494
+ }
278
495
  }
279
496
 
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
- );
497
+ throw new Error(`Claim "${claim.id}" does not carry a path target.`);
285
498
  }
286
499
 
287
500
  /**
288
- * @param {Map<string, string>} pending_document_keys
289
- * @param {string} source_path
501
+ * @param {PatramClaim[]} claims
502
+ * @returns {Map<string, PatramClaim[]>}
503
+ */
504
+ function groupClaimsByPath(claims) {
505
+ /** @type {Map<string, PatramClaim[]>} */
506
+ const claims_by_path = new Map();
507
+
508
+ for (const claim of claims) {
509
+ const source_path = normalizeRepoRelativePath(claim.origin.path);
510
+ const path_claims = claims_by_path.get(source_path) ?? [];
511
+
512
+ path_claims.push(claim);
513
+ claims_by_path.set(source_path, path_claims);
514
+ }
515
+
516
+ return claims_by_path;
517
+ }
518
+
519
+ /**
520
+ * @param {string} class_name
290
521
  * @param {string} node_key
522
+ * @param {string} source_path
523
+ * @returns {DocumentNodeReference}
291
524
  */
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);
525
+ function createDocumentNodeReference(class_name, node_key, source_path) {
526
+ return {
527
+ class_name,
528
+ id: getNodeId(class_name, node_key),
529
+ key: node_key,
530
+ path: source_path,
531
+ };
532
+ }
298
533
 
299
- if (existing_node_key && existing_node_key !== node_key) {
300
- throw new Error(`Document "${source_path}" defines multiple semantic ids.`);
534
+ /**
535
+ * @param {PatramClaim} claim
536
+ * @returns {string}
537
+ */
538
+ function getStringClaimValue(claim) {
539
+ if (typeof claim.value === 'string') {
540
+ return claim.value;
301
541
  }
302
542
 
303
- pending_document_keys.set(source_path, node_key);
543
+ throw new Error(`Claim "${claim.id}" does not carry a string value.`);
544
+ }
545
+
546
+ /**
547
+ * @param {string} document_path
548
+ * @param {string} class_name
549
+ * @returns {string}
550
+ */
551
+ function getDocumentEntityMapKey(document_path, class_name) {
552
+ return `${class_name}:${document_path}`;
553
+ }
554
+
555
+ /**
556
+ * @param {string} source_path
557
+ * @returns {DocumentNodeReference}
558
+ */
559
+ function createDefaultDocumentNodeReference(source_path) {
560
+ return createDocumentNodeReference('document', source_path, source_path);
304
561
  }
305
562
 
306
563
  /**