patram 0.0.2 → 0.1.1

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 (51) hide show
  1. package/bin/patram.js +25 -147
  2. package/lib/build-graph-identity.js +238 -0
  3. package/lib/build-graph.js +143 -77
  4. package/lib/check-graph.js +23 -7
  5. package/lib/claim-helpers.js +55 -0
  6. package/lib/command-output.js +83 -0
  7. package/lib/layout-stored-queries.js +213 -0
  8. package/lib/list-queries.js +18 -0
  9. package/lib/list-source-files.js +50 -15
  10. package/lib/load-patram-config.js +106 -18
  11. package/lib/load-patram-config.types.ts +9 -0
  12. package/lib/load-project-graph.js +124 -0
  13. package/lib/output-view.types.ts +73 -0
  14. package/lib/parse-claims.js +38 -158
  15. package/lib/parse-claims.types.ts +7 -0
  16. package/lib/parse-cli-arguments-helpers.js +273 -0
  17. package/lib/parse-cli-arguments.js +114 -0
  18. package/lib/parse-cli-arguments.types.ts +24 -0
  19. package/lib/parse-cli-color-options.js +44 -0
  20. package/lib/parse-cli-query-pagination.js +49 -0
  21. package/lib/parse-jsdoc-blocks.js +184 -0
  22. package/lib/parse-jsdoc-claims.js +280 -0
  23. package/lib/parse-jsdoc-prose.js +111 -0
  24. package/lib/parse-markdown-claims.js +242 -0
  25. package/lib/parse-markdown-directives.js +136 -0
  26. package/lib/parse-where-clause.js +312 -0
  27. package/lib/patram-cli.js +337 -0
  28. package/lib/patram-config.js +3 -1
  29. package/lib/patram-config.types.ts +2 -1
  30. package/lib/query-graph.js +256 -0
  31. package/lib/render-check-output.js +315 -0
  32. package/lib/render-json-output.js +108 -0
  33. package/lib/render-output-view.js +193 -0
  34. package/lib/render-plain-output.js +237 -0
  35. package/lib/render-rich-output.js +293 -0
  36. package/lib/render-rich-source.js +1333 -0
  37. package/lib/resolve-check-target.js +190 -0
  38. package/lib/resolve-output-mode.js +60 -0
  39. package/lib/resolve-patram-graph-config.js +88 -0
  40. package/lib/resolve-where-clause.js +51 -0
  41. package/lib/show-document.js +311 -0
  42. package/lib/source-file-defaults.js +28 -0
  43. package/lib/write-paged-output.js +87 -0
  44. package/package.json +21 -10
  45. package/bin/patram.test.js +0 -184
  46. package/lib/build-graph.test.js +0 -141
  47. package/lib/check-graph.test.js +0 -103
  48. package/lib/list-source-files.test.js +0 -101
  49. package/lib/load-patram-config.test.js +0 -211
  50. package/lib/parse-claims.test.js +0 -113
  51. package/lib/patram-config.test.js +0 -147
package/bin/patram.js CHANGED
@@ -1,169 +1,47 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- /**
4
- * @import { PatramClaim } from '../lib/parse-claims.types.ts';
5
- */
6
-
7
- import { readFile } from 'node:fs/promises';
8
- import { resolve } from 'node:path';
3
+ import { realpath } from 'node:fs/promises';
9
4
  import process from 'node:process';
10
- import { pathToFileURL } from 'node:url';
5
+ import { fileURLToPath } from 'node:url';
11
6
 
12
- import { buildGraph } from '../lib/build-graph.js';
13
- import { checkGraph } from '../lib/check-graph.js';
14
- import { listSourceFiles } from '../lib/list-source-files.js';
15
- import { loadPatramConfig } from '../lib/load-patram-config.js';
16
- import { parseClaims } from '../lib/parse-claims.js';
17
- import { parsePatramConfig } from '../lib/patram-config.js';
7
+ import { main } from '../lib/patram-cli.js';
18
8
 
19
- const CHECK_GRAPH_CONFIG = parsePatramConfig({
20
- kinds: {
21
- document: {
22
- builtin: true,
23
- label: 'Document',
24
- },
25
- },
26
- mappings: {
27
- 'document.title': {
28
- node: {
29
- field: 'title',
30
- kind: 'document',
31
- },
32
- },
33
- 'markdown.link': {
34
- emit: {
35
- relation: 'links_to',
36
- target: 'path',
37
- target_kind: 'document',
38
- },
39
- },
40
- },
41
- relations: {
42
- links_to: {
43
- builtin: true,
44
- from: ['document'],
45
- to: ['document'],
46
- },
47
- },
48
- });
9
+ /**
10
+ * Patram CLI entrypoint.
11
+ *
12
+ * Detects direct process execution and forwards command handling to the shared
13
+ * CLI runtime.
14
+ *
15
+ * Kind: entrypoint
16
+ * Status: active
17
+ * Tracked in: ../docs/plans/v0/source-anchor-dogfooding.md
18
+ * Decided by: ../docs/decisions/cli-entrypoint-symlink.md
19
+ * @patram
20
+ * @see {@link ../lib/patram-cli.js}
21
+ * @see {@link ../docs/patram.md}
22
+ */
49
23
 
50
- if (isEntrypoint(import.meta.url, process.argv[1])) {
24
+ if (await isEntrypoint(import.meta.url, process.argv[1])) {
51
25
  process.exitCode = await main(process.argv.slice(2), {
52
26
  stderr: process.stderr,
53
27
  stdout: process.stdout,
54
28
  });
55
29
  }
56
30
 
57
- /**
58
- * Run the Patram CLI.
59
- *
60
- * @param {string[]} cli_arguments
61
- * @param {{ stderr: { write(chunk: string): boolean }, stdout: { write(chunk: string): boolean } }} io_context
62
- * @returns {Promise<number>}
63
- */
64
- export async function main(cli_arguments, io_context) {
65
- const command_name = cli_arguments[0];
66
-
67
- if (command_name === 'check') {
68
- return runCheckCommand(cli_arguments.slice(1), io_context);
69
- }
70
-
71
- io_context.stderr.write('Unknown command.\n');
72
-
73
- return 1;
74
- }
75
-
76
- /**
77
- * @param {string[]} command_arguments
78
- * @param {{ stderr: { write(chunk: string): boolean }, stdout: { write(chunk: string): boolean } }} io_context
79
- * @returns {Promise<number>}
80
- */
81
- async function runCheckCommand(command_arguments, io_context) {
82
- const project_directory = command_arguments[0] ?? process.cwd();
83
- const load_result = await loadPatramConfig(project_directory);
84
-
85
- if (load_result.diagnostics.length > 0) {
86
- writeDiagnostics(io_context.stderr, load_result.diagnostics);
87
-
88
- return 1;
89
- }
90
-
91
- const repo_config = load_result.config;
92
-
93
- if (!repo_config) {
94
- throw new Error('Expected a valid Patram repo config.');
95
- }
96
-
97
- const source_file_paths = await listSourceFiles(
98
- repo_config.include,
99
- project_directory,
100
- );
101
- const claims = await collectClaims(source_file_paths, project_directory);
102
- const graph = buildGraph(CHECK_GRAPH_CONFIG, claims);
103
- const diagnostics = checkGraph(graph, source_file_paths);
104
-
105
- if (diagnostics.length > 0) {
106
- writeDiagnostics(io_context.stderr, diagnostics);
107
-
108
- return 1;
109
- }
110
-
111
- return 0;
112
- }
113
-
114
- /**
115
- * @param {string[]} source_file_paths
116
- * @param {string} project_directory
117
- * @returns {Promise<PatramClaim[]>}
118
- */
119
- async function collectClaims(source_file_paths, project_directory) {
120
- /** @type {PatramClaim[]} */
121
- const claims = [];
122
-
123
- for (const source_file_path of source_file_paths) {
124
- const source_text = await readFile(
125
- resolve(project_directory, source_file_path),
126
- 'utf8',
127
- );
128
-
129
- claims.push(
130
- ...parseClaims({
131
- path: source_file_path,
132
- source: source_text,
133
- }),
134
- );
135
- }
136
-
137
- return claims;
138
- }
139
-
140
- /**
141
- * @param {{ write(chunk: string): boolean }} output_stream
142
- * @param {import('../lib/load-patram-config.types.ts').PatramDiagnostic[]} diagnostics
143
- */
144
- function writeDiagnostics(output_stream, diagnostics) {
145
- for (const diagnostic of diagnostics) {
146
- output_stream.write(formatDiagnostic(diagnostic));
147
- }
148
- }
149
-
150
- /**
151
- * @param {import('../lib/load-patram-config.types.ts').PatramDiagnostic} diagnostic
152
- * @returns {string}
153
- */
154
- function formatDiagnostic(diagnostic) {
155
- return `${diagnostic.path}:${diagnostic.line}:${diagnostic.column} ${diagnostic.level} ${diagnostic.code} ${diagnostic.message}\n`;
156
- }
31
+ export { main };
157
32
 
158
33
  /**
159
34
  * @param {string} module_url
160
35
  * @param {string | undefined} process_entry_path
161
- * @returns {boolean}
36
+ * @returns {Promise<boolean>}
162
37
  */
163
- function isEntrypoint(module_url, process_entry_path) {
38
+ async function isEntrypoint(module_url, process_entry_path) {
164
39
  if (!process_entry_path) {
165
40
  return false;
166
41
  }
167
42
 
168
- return module_url === pathToFileURL(process_entry_path).href;
43
+ const module_path = await realpath(fileURLToPath(module_url));
44
+ const entry_path = await realpath(process_entry_path);
45
+
46
+ return module_path === entry_path;
169
47
  }
@@ -0,0 +1,238 @@
1
+ /**
2
+ * @import { GraphNode } from './build-graph.types.ts';
3
+ * @import { PatramClaim } from './parse-claims.types.ts';
4
+ * @import { MappingDefinition } from './patram-config.types.ts';
5
+ */
6
+
7
+ import { posix } from 'node:path';
8
+
9
+ /**
10
+ * Collect semantic entity keys defined by canonical documents.
11
+ *
12
+ * @param {Record<string, MappingDefinition>} mappings
13
+ * @param {PatramClaim[]} claims
14
+ * @returns {Map<string, string>}
15
+ */
16
+ export function collectDocumentEntityKeys(mappings, claims) {
17
+ /** @type {Map<string, string>} */
18
+ const document_entity_keys = new Map();
19
+
20
+ for (const claim of claims) {
21
+ const mapping_definition = resolveMappingDefinition(mappings, claim);
22
+
23
+ if (mapping_definition?.node?.key !== 'value') {
24
+ continue;
25
+ }
26
+
27
+ const source_path = normalizeRepoRelativePath(claim.origin.path);
28
+ const entity_map_key = getDocumentEntityMapKey(
29
+ source_path,
30
+ mapping_definition.node.kind,
31
+ );
32
+ const entity_key = getStringClaimValue(claim);
33
+ const existing_entity_key = document_entity_keys.get(entity_map_key);
34
+
35
+ if (existing_entity_key && existing_entity_key !== entity_key) {
36
+ throw new Error(
37
+ `Document "${source_path}" defines multiple ${mapping_definition.node.kind} ids.`,
38
+ );
39
+ }
40
+
41
+ document_entity_keys.set(entity_map_key, entity_key);
42
+ }
43
+
44
+ return document_entity_keys;
45
+ }
46
+
47
+ /**
48
+ * Resolve the node key for one mapped claim.
49
+ *
50
+ * @param {{ field: string, key?: 'path' | 'value', kind: string }} node_mapping
51
+ * @param {PatramClaim} claim
52
+ * @param {Map<string, string>} document_entity_keys
53
+ * @returns {string}
54
+ */
55
+ export function resolveNodeKey(node_mapping, claim, document_entity_keys) {
56
+ const source_key = normalizeRepoRelativePath(claim.origin.path);
57
+
58
+ if (node_mapping.kind === 'document') {
59
+ return source_key;
60
+ }
61
+
62
+ if (node_mapping.key === 'value') {
63
+ return getStringClaimValue(claim);
64
+ }
65
+
66
+ return (
67
+ document_entity_keys.get(
68
+ getDocumentEntityMapKey(source_key, node_mapping.kind),
69
+ ) ?? source_key
70
+ );
71
+ }
72
+
73
+ /**
74
+ * Resolve one edge target key and canonical path.
75
+ *
76
+ * @param {string} target_kind
77
+ * @param {'path' | 'value'} target_type
78
+ * @param {PatramClaim} claim
79
+ * @param {Map<string, string>} document_entity_keys
80
+ * @returns {{ key: string, path?: string }}
81
+ */
82
+ export function resolveTargetReference(
83
+ target_kind,
84
+ target_type,
85
+ claim,
86
+ document_entity_keys,
87
+ ) {
88
+ if (target_type === 'value') {
89
+ return resolveValueTargetReference(target_kind, claim);
90
+ }
91
+
92
+ return resolvePathTargetReference(target_kind, claim, document_entity_keys);
93
+ }
94
+
95
+ /**
96
+ * Attach one canonical path to a non-document graph node.
97
+ *
98
+ * @param {GraphNode} graph_node
99
+ * @param {string | undefined} source_path
100
+ */
101
+ export function setNonDocumentPath(graph_node, source_path) {
102
+ if (!source_path || graph_node.kind === 'document') {
103
+ return;
104
+ }
105
+
106
+ if (!graph_node.path) {
107
+ graph_node.path = source_path;
108
+ return;
109
+ }
110
+
111
+ if (graph_node.path !== source_path) {
112
+ throw new Error(
113
+ `Node "${graph_node.id}" maps to multiple canonical paths.`,
114
+ );
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Normalize one repo-relative source path.
120
+ *
121
+ * @param {string} source_path
122
+ * @returns {string}
123
+ */
124
+ export function normalizeRepoRelativePath(source_path) {
125
+ return posix.normalize(source_path.replaceAll('\\', '/'));
126
+ }
127
+
128
+ /**
129
+ * @param {Record<string, MappingDefinition>} mappings
130
+ * @param {PatramClaim} claim
131
+ * @returns {MappingDefinition | null}
132
+ */
133
+ function resolveMappingDefinition(mappings, claim) {
134
+ if (claim.type === 'directive') {
135
+ return resolveDirectiveMapping(mappings, claim);
136
+ }
137
+
138
+ return mappings[claim.type] ?? null;
139
+ }
140
+
141
+ /**
142
+ * @param {Record<string, MappingDefinition>} mappings
143
+ * @param {PatramClaim} claim
144
+ * @returns {MappingDefinition | null}
145
+ */
146
+ function resolveDirectiveMapping(mappings, claim) {
147
+ if (!claim.parser || !claim.name) {
148
+ return null;
149
+ }
150
+
151
+ return mappings[`${claim.parser}.directive.${claim.name}`] ?? null;
152
+ }
153
+
154
+ /**
155
+ * @param {string} target_kind
156
+ * @param {PatramClaim} claim
157
+ * @returns {{ key: string, path?: string }}
158
+ */
159
+ function resolveValueTargetReference(target_kind, claim) {
160
+ const target_key = getStringClaimValue(claim);
161
+
162
+ if (target_kind === 'document') {
163
+ return {
164
+ key: target_key,
165
+ path: target_key,
166
+ };
167
+ }
168
+
169
+ return {
170
+ key: target_key,
171
+ path: normalizeRepoRelativePath(claim.origin.path),
172
+ };
173
+ }
174
+
175
+ /**
176
+ * @param {string} target_kind
177
+ * @param {PatramClaim} claim
178
+ * @param {Map<string, string>} document_entity_keys
179
+ * @returns {{ key: string, path?: string }}
180
+ */
181
+ function resolvePathTargetReference(target_kind, claim, document_entity_keys) {
182
+ const source_directory = posix.dirname(
183
+ normalizeRepoRelativePath(claim.origin.path),
184
+ );
185
+ const raw_target = getPathTargetValue(claim);
186
+ const target_path = normalizeRepoRelativePath(
187
+ posix.join(source_directory, raw_target),
188
+ );
189
+
190
+ if (target_kind === 'document') {
191
+ return {
192
+ key: target_path,
193
+ path: target_path,
194
+ };
195
+ }
196
+
197
+ const semantic_target_key = document_entity_keys.get(
198
+ getDocumentEntityMapKey(target_path, target_kind),
199
+ );
200
+
201
+ return {
202
+ key: semantic_target_key ?? target_path,
203
+ path: target_path,
204
+ };
205
+ }
206
+
207
+ /**
208
+ * @param {PatramClaim} claim
209
+ * @returns {string}
210
+ */
211
+ function getPathTargetValue(claim) {
212
+ if (typeof claim.value === 'string') {
213
+ return claim.value;
214
+ }
215
+
216
+ return claim.value.target;
217
+ }
218
+
219
+ /**
220
+ * @param {PatramClaim} claim
221
+ * @returns {string}
222
+ */
223
+ function getStringClaimValue(claim) {
224
+ if (typeof claim.value === 'string') {
225
+ return claim.value;
226
+ }
227
+
228
+ throw new Error(`Claim "${claim.id}" does not carry a string value.`);
229
+ }
230
+
231
+ /**
232
+ * @param {string} document_path
233
+ * @param {string} kind_name
234
+ * @returns {string}
235
+ */
236
+ function getDocumentEntityMapKey(document_path, kind_name) {
237
+ return `${kind_name}:${document_path}`;
238
+ }
@@ -4,7 +4,34 @@
4
4
  * @import { MappingDefinition, PatramConfig } from './patram-config.types.ts';
5
5
  */
6
6
 
7
- import { posix } from 'node:path';
7
+ import {
8
+ collectDocumentEntityKeys,
9
+ normalizeRepoRelativePath,
10
+ resolveNodeKey,
11
+ resolveTargetReference,
12
+ setNonDocumentPath,
13
+ } from './build-graph-identity.js';
14
+
15
+ /**
16
+ * Claim-to-graph materialization.
17
+ *
18
+ * Maps parsed claims into document nodes and relations using the resolved
19
+ * Patram graph config.
20
+ *
21
+ * Kind: graph
22
+ * Status: active
23
+ * Uses Term: ../docs/reference/terms/claim.md
24
+ * Uses Term: ../docs/reference/terms/document.md
25
+ * Uses Term: ../docs/reference/terms/graph.md
26
+ * Uses Term: ../docs/reference/terms/mapping.md
27
+ * Uses Term: ../docs/reference/terms/relation.md
28
+ * Tracked in: ../docs/plans/v0/source-anchor-dogfooding.md
29
+ * Decided by: ../docs/decisions/graph-materialization.md
30
+ * Implements: ../docs/tasks/v0/materialize-graph.md
31
+ * @patram
32
+ * @see {@link ./load-project-graph.js}
33
+ * @see {@link ../docs/decisions/graph-materialization.md}
34
+ */
8
35
 
9
36
  /**
10
37
  * Build a Patram graph from semantic config and parsed claims.
@@ -16,49 +43,24 @@ import { posix } from 'node:path';
16
43
  export function buildGraph(patram_config, claims) {
17
44
  /** @type {Map<string, GraphNode>} */
18
45
  const graph_nodes = new Map();
19
- /** @type {GraphEdge[]} */
20
- const graph_edges = [];
21
- let edge_number = 0;
22
-
23
- for (const claim of claims) {
24
- const source_document_node = upsertNode(
25
- graph_nodes,
26
- 'document',
27
- normalizeRepoRelativePath(claim.origin.path),
28
- );
29
- const mapping_definition = resolveMappingDefinition(
30
- patram_config.mappings,
31
- claim,
32
- );
33
-
34
- if (!mapping_definition) {
35
- continue;
36
- }
37
-
38
- if (mapping_definition.node) {
39
- applyNodeMapping(graph_nodes, mapping_definition.node, claim);
40
- }
41
-
42
- if (!mapping_definition.emit) {
43
- continue;
44
- }
45
-
46
- const target_key = resolveTargetKey(mapping_definition.emit.target, claim);
47
- const target_node = upsertNode(
48
- graph_nodes,
49
- mapping_definition.emit.target_kind,
50
- target_key,
51
- );
46
+ const document_entity_keys = collectDocumentEntityKeys(
47
+ patram_config.mappings,
48
+ claims,
49
+ );
52
50
 
53
- edge_number += 1;
54
- graph_edges.push({
55
- from: source_document_node.id,
56
- id: `edge:${edge_number}`,
57
- origin: claim.origin,
58
- relation: mapping_definition.emit.relation,
59
- to: target_node.id,
60
- });
61
- }
51
+ createDocumentNodes(graph_nodes, claims);
52
+ applyNodeMappings(
53
+ graph_nodes,
54
+ patram_config.mappings,
55
+ claims,
56
+ document_entity_keys,
57
+ );
58
+ const graph_edges = createGraphEdges(
59
+ graph_nodes,
60
+ patram_config.mappings,
61
+ claims,
62
+ document_entity_keys,
63
+ );
62
64
 
63
65
  return {
64
66
  edges: graph_edges,
@@ -96,56 +98,116 @@ function resolveDirectiveMapping(mappings, claim) {
96
98
 
97
99
  /**
98
100
  * @param {Map<string, GraphNode>} graph_nodes
99
- * @param {{ field: string, kind: string }} node_mapping
100
- * @param {PatramClaim} claim
101
+ * @param {PatramClaim[]} claims
101
102
  */
102
- function applyNodeMapping(graph_nodes, node_mapping, claim) {
103
- const source_key = normalizeRepoRelativePath(claim.origin.path);
104
- const graph_node = upsertNode(graph_nodes, node_mapping.kind, source_key);
105
-
106
- graph_node[node_mapping.field] = getStringClaimValue(claim);
103
+ function createDocumentNodes(graph_nodes, claims) {
104
+ for (const claim of claims) {
105
+ upsertNode(
106
+ graph_nodes,
107
+ 'document',
108
+ normalizeRepoRelativePath(claim.origin.path),
109
+ );
110
+ }
107
111
  }
108
112
 
109
113
  /**
110
- * @param {'path'} target_type
111
- * @param {PatramClaim} claim
112
- * @returns {string}
114
+ * @param {Map<string, GraphNode>} graph_nodes
115
+ * @param {Record<string, MappingDefinition>} mappings
116
+ * @param {PatramClaim[]} claims
117
+ * @param {Map<string, string>} document_entity_keys
113
118
  */
114
- function resolveTargetKey(target_type, claim) {
115
- if (target_type !== 'path') {
116
- throw new Error(`Unsupported target type "${target_type}".`);
117
- }
119
+ function applyNodeMappings(
120
+ graph_nodes,
121
+ mappings,
122
+ claims,
123
+ document_entity_keys,
124
+ ) {
125
+ for (const claim of claims) {
126
+ const mapping_definition = resolveMappingDefinition(mappings, claim);
118
127
 
119
- const source_directory = posix.dirname(
120
- normalizeRepoRelativePath(claim.origin.path),
121
- );
122
- const raw_target = getPathTargetValue(claim);
128
+ if (!mapping_definition?.node) {
129
+ continue;
130
+ }
123
131
 
124
- return normalizeRepoRelativePath(posix.join(source_directory, raw_target));
132
+ applyNodeMapping(
133
+ graph_nodes,
134
+ mapping_definition.node,
135
+ claim,
136
+ document_entity_keys,
137
+ );
138
+ }
125
139
  }
126
140
 
127
141
  /**
128
- * @param {PatramClaim} claim
129
- * @returns {string}
142
+ * @param {Map<string, GraphNode>} graph_nodes
143
+ * @param {Record<string, MappingDefinition>} mappings
144
+ * @param {PatramClaim[]} claims
145
+ * @param {Map<string, string>} document_entity_keys
146
+ * @returns {GraphEdge[]}
130
147
  */
131
- function getPathTargetValue(claim) {
132
- if (typeof claim.value === 'string') {
133
- return claim.value;
148
+ function createGraphEdges(graph_nodes, mappings, claims, document_entity_keys) {
149
+ /** @type {GraphEdge[]} */
150
+ const graph_edges = [];
151
+ let edge_number = 0;
152
+
153
+ for (const claim of claims) {
154
+ const mapping_definition = resolveMappingDefinition(mappings, claim);
155
+
156
+ if (!mapping_definition?.emit) {
157
+ continue;
158
+ }
159
+
160
+ const source_document_node = upsertNode(
161
+ graph_nodes,
162
+ 'document',
163
+ normalizeRepoRelativePath(claim.origin.path),
164
+ );
165
+ const target_reference = resolveTargetReference(
166
+ mapping_definition.emit.target_kind,
167
+ mapping_definition.emit.target,
168
+ claim,
169
+ document_entity_keys,
170
+ );
171
+ const target_node = upsertNode(
172
+ graph_nodes,
173
+ mapping_definition.emit.target_kind,
174
+ target_reference.key,
175
+ );
176
+
177
+ setNonDocumentPath(target_node, target_reference.path);
178
+
179
+ edge_number += 1;
180
+ graph_edges.push({
181
+ from: source_document_node.id,
182
+ id: `edge:${edge_number}`,
183
+ origin: claim.origin,
184
+ relation: mapping_definition.emit.relation,
185
+ to: target_node.id,
186
+ });
134
187
  }
135
188
 
136
- return claim.value.target;
189
+ return graph_edges;
137
190
  }
138
191
 
139
192
  /**
193
+ * @param {Map<string, GraphNode>} graph_nodes
194
+ * @param {{ field: string, key?: 'path' | 'value', kind: string }} node_mapping
140
195
  * @param {PatramClaim} claim
141
- * @returns {string}
196
+ * @param {Map<string, string>} document_entity_keys
142
197
  */
143
- function getStringClaimValue(claim) {
144
- if (typeof claim.value === 'string') {
145
- return claim.value;
146
- }
198
+ function applyNodeMapping(
199
+ graph_nodes,
200
+ node_mapping,
201
+ claim,
202
+ document_entity_keys,
203
+ ) {
204
+ const source_key = normalizeRepoRelativePath(claim.origin.path);
205
+ const node_key = resolveNodeKey(node_mapping, claim, document_entity_keys);
206
+ const graph_node = upsertNode(graph_nodes, node_mapping.kind, node_key);
147
207
 
148
- throw new Error(`Claim "${claim.id}" does not carry a string value.`);
208
+ setNonDocumentPath(graph_node, source_key);
209
+
210
+ graph_node[node_mapping.field] = getNodeFieldValue(claim);
149
211
  }
150
212
 
151
213
  /**
@@ -205,11 +267,15 @@ function getNodeId(kind_name, node_key) {
205
267
  }
206
268
 
207
269
  /**
208
- * @param {string} source_path
270
+ * @param {PatramClaim} claim
209
271
  * @returns {string}
210
272
  */
211
- function normalizeRepoRelativePath(source_path) {
212
- return posix.normalize(source_path.replaceAll('\\', '/'));
273
+ function getNodeFieldValue(claim) {
274
+ if (typeof claim.value === 'string') {
275
+ return claim.value;
276
+ }
277
+
278
+ throw new Error(`Claim "${claim.id}" does not carry a string value.`);
213
279
  }
214
280
 
215
281
  /**