patram 0.0.2
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/LICENSE +21 -0
- package/bin/patram.js +169 -0
- package/bin/patram.test.js +184 -0
- package/lib/build-graph.js +222 -0
- package/lib/build-graph.test.js +141 -0
- package/lib/build-graph.types.ts +23 -0
- package/lib/check-graph.js +125 -0
- package/lib/check-graph.test.js +103 -0
- package/lib/list-source-files.js +44 -0
- package/lib/list-source-files.test.js +101 -0
- package/lib/load-patram-config.js +244 -0
- package/lib/load-patram-config.test.js +211 -0
- package/lib/load-patram-config.types.ts +23 -0
- package/lib/parse-claims.js +178 -0
- package/lib/parse-claims.test.js +113 -0
- package/lib/parse-claims.types.ts +27 -0
- package/lib/patram-config.js +194 -0
- package/lib/patram-config.test.js +147 -0
- package/lib/patram-config.types.ts +33 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Maximilian Antoni
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/bin/patram.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
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';
|
|
9
|
+
import process from 'node:process';
|
|
10
|
+
import { pathToFileURL } from 'node:url';
|
|
11
|
+
|
|
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';
|
|
18
|
+
|
|
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
|
+
});
|
|
49
|
+
|
|
50
|
+
if (isEntrypoint(import.meta.url, process.argv[1])) {
|
|
51
|
+
process.exitCode = await main(process.argv.slice(2), {
|
|
52
|
+
stderr: process.stderr,
|
|
53
|
+
stdout: process.stdout,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
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
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @param {string} module_url
|
|
160
|
+
* @param {string | undefined} process_entry_path
|
|
161
|
+
* @returns {boolean}
|
|
162
|
+
*/
|
|
163
|
+
function isEntrypoint(module_url, process_entry_path) {
|
|
164
|
+
if (!process_entry_path) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return module_url === pathToFileURL(process_entry_path).href;
|
|
169
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { afterEach, expect, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { main } from './patram.js';
|
|
8
|
+
|
|
9
|
+
const test_context = createTestContext();
|
|
10
|
+
|
|
11
|
+
afterEach(async () => {
|
|
12
|
+
await cleanupTestContext(test_context);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('prints config diagnostics for check failures', async () => {
|
|
16
|
+
test_context.project_directory = await createTempProjectDirectory();
|
|
17
|
+
const io_context = createIoContext();
|
|
18
|
+
|
|
19
|
+
const exit_code = await main(['check', test_context.project_directory], {
|
|
20
|
+
stderr: io_context.stderr,
|
|
21
|
+
stdout: io_context.stdout,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(exit_code).toBe(1);
|
|
25
|
+
expect(io_context.stderr_chunks).toEqual([
|
|
26
|
+
'.patram.json:1:1 error config.not_found Config file ".patram.json" was not found.\n',
|
|
27
|
+
]);
|
|
28
|
+
expect(io_context.stdout_chunks).toEqual([]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('prints broken link diagnostics for check failures', async () => {
|
|
32
|
+
test_context.project_directory = await createTempProjectDirectory();
|
|
33
|
+
const io_context = createIoContext();
|
|
34
|
+
|
|
35
|
+
await writeProjectConfig(test_context.project_directory);
|
|
36
|
+
await writeProjectFile(
|
|
37
|
+
test_context.project_directory,
|
|
38
|
+
'docs/patram.md',
|
|
39
|
+
createBrokenLinkSource(),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const exit_code = await main(['check', test_context.project_directory], {
|
|
43
|
+
stderr: io_context.stderr,
|
|
44
|
+
stdout: io_context.stdout,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(exit_code).toBe(1);
|
|
48
|
+
expect(io_context.stderr_chunks).toEqual([
|
|
49
|
+
'docs/patram.md:3:5 error graph.link_broken Document link target "docs/missing.md" was not found.\n',
|
|
50
|
+
]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('defaults check to the current working directory and exits 0 on valid input', async () => {
|
|
54
|
+
test_context.project_directory = await createTempProjectDirectory();
|
|
55
|
+
const io_context = createIoContext();
|
|
56
|
+
|
|
57
|
+
await writeProjectConfig(test_context.project_directory);
|
|
58
|
+
await writeProjectFile(
|
|
59
|
+
test_context.project_directory,
|
|
60
|
+
'docs/patram.md',
|
|
61
|
+
createValidLinkSource(),
|
|
62
|
+
);
|
|
63
|
+
await writeProjectFile(
|
|
64
|
+
test_context.project_directory,
|
|
65
|
+
'docs/guide.md',
|
|
66
|
+
'# Guide\n',
|
|
67
|
+
);
|
|
68
|
+
process.chdir(test_context.project_directory);
|
|
69
|
+
|
|
70
|
+
const exit_code = await main(['check'], {
|
|
71
|
+
stderr: io_context.stderr,
|
|
72
|
+
stdout: io_context.stdout,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(exit_code).toBe(0);
|
|
76
|
+
expect(io_context.stderr_chunks).toEqual([]);
|
|
77
|
+
expect(io_context.stdout_chunks).toEqual([]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @returns {string}
|
|
82
|
+
*/
|
|
83
|
+
function createBrokenLinkSource() {
|
|
84
|
+
return ['# Patram', '', 'See [missing](./missing.md).'].join('\n');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @returns {string}
|
|
89
|
+
*/
|
|
90
|
+
function createValidLinkSource() {
|
|
91
|
+
return ['# Patram', '', 'See [guide](./guide.md).'].join('\n');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @returns {{ original_working_directory: string, project_directory: string | null }}
|
|
96
|
+
*/
|
|
97
|
+
function createTestContext() {
|
|
98
|
+
return {
|
|
99
|
+
original_working_directory: process.cwd(),
|
|
100
|
+
project_directory: null,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @returns {{ stderr: { write(chunk: string): boolean }, stderr_chunks: string[], stdout: { write(chunk: string): boolean }, stdout_chunks: string[] }}
|
|
106
|
+
*/
|
|
107
|
+
function createIoContext() {
|
|
108
|
+
/** @type {string[]} */
|
|
109
|
+
const stderr_chunks = [];
|
|
110
|
+
/** @type {string[]} */
|
|
111
|
+
const stdout_chunks = [];
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
stderr: {
|
|
115
|
+
/**
|
|
116
|
+
* @param {string} chunk
|
|
117
|
+
* @returns {boolean}
|
|
118
|
+
*/
|
|
119
|
+
write(chunk) {
|
|
120
|
+
stderr_chunks.push(chunk);
|
|
121
|
+
|
|
122
|
+
return true;
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
stderr_chunks,
|
|
126
|
+
stdout: {
|
|
127
|
+
/**
|
|
128
|
+
* @param {string} chunk
|
|
129
|
+
* @returns {boolean}
|
|
130
|
+
*/
|
|
131
|
+
write(chunk) {
|
|
132
|
+
stdout_chunks.push(chunk);
|
|
133
|
+
|
|
134
|
+
return true;
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
stdout_chunks,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @returns {Promise<string>}
|
|
143
|
+
*/
|
|
144
|
+
async function createTempProjectDirectory() {
|
|
145
|
+
return mkdtemp(join(tmpdir(), 'patram-check-command-'));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @param {{ original_working_directory: string, project_directory: string | null }} test_context
|
|
150
|
+
*/
|
|
151
|
+
async function cleanupTestContext(test_context) {
|
|
152
|
+
process.chdir(test_context.original_working_directory);
|
|
153
|
+
|
|
154
|
+
if (test_context.project_directory) {
|
|
155
|
+
await rm(test_context.project_directory, { force: true, recursive: true });
|
|
156
|
+
test_context.project_directory = null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* @param {string} project_directory
|
|
162
|
+
*/
|
|
163
|
+
async function writeProjectConfig(project_directory) {
|
|
164
|
+
await writeFile(
|
|
165
|
+
join(project_directory, '.patram.json'),
|
|
166
|
+
JSON.stringify({
|
|
167
|
+
include: ['docs/**/*.md'],
|
|
168
|
+
queries: {},
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @param {string} project_directory
|
|
175
|
+
* @param {string} relative_path
|
|
176
|
+
* @param {string} source_text
|
|
177
|
+
*/
|
|
178
|
+
async function writeProjectFile(project_directory, relative_path, source_text) {
|
|
179
|
+
const file_path = join(project_directory, relative_path);
|
|
180
|
+
const directory_path = file_path.slice(0, file_path.lastIndexOf('/'));
|
|
181
|
+
|
|
182
|
+
await mkdir(directory_path, { recursive: true });
|
|
183
|
+
await writeFile(file_path, source_text);
|
|
184
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { BuildGraphResult, GraphEdge, GraphNode } from './build-graph.types.ts';
|
|
3
|
+
* @import { PatramClaim } from './parse-claims.types.ts';
|
|
4
|
+
* @import { MappingDefinition, PatramConfig } from './patram-config.types.ts';
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { posix } from 'node:path';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Build a Patram graph from semantic config and parsed claims.
|
|
11
|
+
*
|
|
12
|
+
* @param {PatramConfig} patram_config
|
|
13
|
+
* @param {PatramClaim[]} claims
|
|
14
|
+
* @returns {BuildGraphResult}
|
|
15
|
+
*/
|
|
16
|
+
export function buildGraph(patram_config, claims) {
|
|
17
|
+
/** @type {Map<string, GraphNode>} */
|
|
18
|
+
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
|
+
);
|
|
52
|
+
|
|
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
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
edges: graph_edges,
|
|
65
|
+
nodes: Object.fromEntries(
|
|
66
|
+
[...graph_nodes.entries()].sort(compareNodeEntries),
|
|
67
|
+
),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {Record<string, MappingDefinition>} mappings
|
|
73
|
+
* @param {PatramClaim} claim
|
|
74
|
+
* @returns {MappingDefinition | null}
|
|
75
|
+
*/
|
|
76
|
+
function resolveMappingDefinition(mappings, claim) {
|
|
77
|
+
if (claim.type === 'directive') {
|
|
78
|
+
return resolveDirectiveMapping(mappings, claim);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return mappings[claim.type] ?? null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {Record<string, MappingDefinition>} mappings
|
|
86
|
+
* @param {PatramClaim} claim
|
|
87
|
+
* @returns {MappingDefinition | null}
|
|
88
|
+
*/
|
|
89
|
+
function resolveDirectiveMapping(mappings, claim) {
|
|
90
|
+
if (!claim.parser || !claim.name) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return mappings[`${claim.parser}.directive.${claim.name}`] ?? null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {Map<string, GraphNode>} graph_nodes
|
|
99
|
+
* @param {{ field: string, kind: string }} node_mapping
|
|
100
|
+
* @param {PatramClaim} claim
|
|
101
|
+
*/
|
|
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);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {'path'} target_type
|
|
111
|
+
* @param {PatramClaim} claim
|
|
112
|
+
* @returns {string}
|
|
113
|
+
*/
|
|
114
|
+
function resolveTargetKey(target_type, claim) {
|
|
115
|
+
if (target_type !== 'path') {
|
|
116
|
+
throw new Error(`Unsupported target type "${target_type}".`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const source_directory = posix.dirname(
|
|
120
|
+
normalizeRepoRelativePath(claim.origin.path),
|
|
121
|
+
);
|
|
122
|
+
const raw_target = getPathTargetValue(claim);
|
|
123
|
+
|
|
124
|
+
return normalizeRepoRelativePath(posix.join(source_directory, raw_target));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @param {PatramClaim} claim
|
|
129
|
+
* @returns {string}
|
|
130
|
+
*/
|
|
131
|
+
function getPathTargetValue(claim) {
|
|
132
|
+
if (typeof claim.value === 'string') {
|
|
133
|
+
return claim.value;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return claim.value.target;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @param {PatramClaim} claim
|
|
141
|
+
* @returns {string}
|
|
142
|
+
*/
|
|
143
|
+
function getStringClaimValue(claim) {
|
|
144
|
+
if (typeof claim.value === 'string') {
|
|
145
|
+
return claim.value;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
throw new Error(`Claim "${claim.id}" does not carry a string value.`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param {Map<string, GraphNode>} graph_nodes
|
|
153
|
+
* @param {string} kind_name
|
|
154
|
+
* @param {string} node_key
|
|
155
|
+
* @returns {GraphNode}
|
|
156
|
+
*/
|
|
157
|
+
function upsertNode(graph_nodes, kind_name, node_key) {
|
|
158
|
+
const node_id = getNodeId(kind_name, node_key);
|
|
159
|
+
const existing_node = graph_nodes.get(node_id);
|
|
160
|
+
|
|
161
|
+
if (existing_node) {
|
|
162
|
+
return existing_node;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const graph_node = createNode(node_id, kind_name, node_key);
|
|
166
|
+
|
|
167
|
+
graph_nodes.set(node_id, graph_node);
|
|
168
|
+
|
|
169
|
+
return graph_node;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* @param {string} node_id
|
|
174
|
+
* @param {string} kind_name
|
|
175
|
+
* @param {string} node_key
|
|
176
|
+
* @returns {GraphNode}
|
|
177
|
+
*/
|
|
178
|
+
function createNode(node_id, kind_name, node_key) {
|
|
179
|
+
if (kind_name === 'document') {
|
|
180
|
+
return {
|
|
181
|
+
id: node_id,
|
|
182
|
+
kind: kind_name,
|
|
183
|
+
path: node_key,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
id: node_id,
|
|
189
|
+
key: node_key,
|
|
190
|
+
kind: kind_name,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* @param {string} kind_name
|
|
196
|
+
* @param {string} node_key
|
|
197
|
+
* @returns {string}
|
|
198
|
+
*/
|
|
199
|
+
function getNodeId(kind_name, node_key) {
|
|
200
|
+
if (kind_name === 'document') {
|
|
201
|
+
return `doc:${node_key}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return `${kind_name}:${node_key}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* @param {string} source_path
|
|
209
|
+
* @returns {string}
|
|
210
|
+
*/
|
|
211
|
+
function normalizeRepoRelativePath(source_path) {
|
|
212
|
+
return posix.normalize(source_path.replaceAll('\\', '/'));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* @param {[string, GraphNode]} left_entry
|
|
217
|
+
* @param {[string, GraphNode]} right_entry
|
|
218
|
+
* @returns {number}
|
|
219
|
+
*/
|
|
220
|
+
function compareNodeEntries(left_entry, right_entry) {
|
|
221
|
+
return left_entry[0].localeCompare(right_entry[0], 'en');
|
|
222
|
+
}
|