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.
- package/bin/patram.js +25 -147
- package/lib/build-graph-identity.js +238 -0
- package/lib/build-graph.js +143 -77
- package/lib/check-graph.js +23 -7
- package/lib/claim-helpers.js +55 -0
- package/lib/command-output.js +83 -0
- package/lib/layout-stored-queries.js +213 -0
- package/lib/list-queries.js +18 -0
- package/lib/list-source-files.js +50 -15
- package/lib/load-patram-config.js +106 -18
- package/lib/load-patram-config.types.ts +9 -0
- package/lib/load-project-graph.js +124 -0
- package/lib/output-view.types.ts +73 -0
- package/lib/parse-claims.js +38 -158
- package/lib/parse-claims.types.ts +7 -0
- package/lib/parse-cli-arguments-helpers.js +273 -0
- package/lib/parse-cli-arguments.js +114 -0
- package/lib/parse-cli-arguments.types.ts +24 -0
- package/lib/parse-cli-color-options.js +44 -0
- package/lib/parse-cli-query-pagination.js +49 -0
- package/lib/parse-jsdoc-blocks.js +184 -0
- package/lib/parse-jsdoc-claims.js +280 -0
- package/lib/parse-jsdoc-prose.js +111 -0
- package/lib/parse-markdown-claims.js +242 -0
- package/lib/parse-markdown-directives.js +136 -0
- package/lib/parse-where-clause.js +312 -0
- package/lib/patram-cli.js +337 -0
- package/lib/patram-config.js +3 -1
- package/lib/patram-config.types.ts +2 -1
- package/lib/query-graph.js +256 -0
- package/lib/render-check-output.js +315 -0
- package/lib/render-json-output.js +108 -0
- package/lib/render-output-view.js +193 -0
- package/lib/render-plain-output.js +237 -0
- package/lib/render-rich-output.js +293 -0
- package/lib/render-rich-source.js +1333 -0
- package/lib/resolve-check-target.js +190 -0
- package/lib/resolve-output-mode.js +60 -0
- package/lib/resolve-patram-graph-config.js +88 -0
- package/lib/resolve-where-clause.js +51 -0
- package/lib/show-document.js +311 -0
- package/lib/source-file-defaults.js +28 -0
- package/lib/write-paged-output.js +87 -0
- package/package.json +21 -10
- package/bin/patram.test.js +0 -184
- package/lib/build-graph.test.js +0 -141
- package/lib/check-graph.test.js +0 -103
- package/lib/list-source-files.test.js +0 -101
- package/lib/load-patram-config.test.js +0 -211
- package/lib/parse-claims.test.js +0 -113
- 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 {
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
11
6
|
|
|
12
|
-
import {
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/lib/build-graph.js
CHANGED
|
@@ -4,7 +4,34 @@
|
|
|
4
4
|
* @import { MappingDefinition, PatramConfig } from './patram-config.types.ts';
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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 {
|
|
100
|
-
* @param {PatramClaim} claim
|
|
101
|
+
* @param {PatramClaim[]} claims
|
|
101
102
|
*/
|
|
102
|
-
function
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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 {
|
|
111
|
-
* @param {
|
|
112
|
-
* @
|
|
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
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const raw_target = getPathTargetValue(claim);
|
|
128
|
+
if (!mapping_definition?.node) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
123
131
|
|
|
124
|
-
|
|
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 {
|
|
129
|
-
* @
|
|
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
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
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
|
-
* @
|
|
196
|
+
* @param {Map<string, string>} document_entity_keys
|
|
142
197
|
*/
|
|
143
|
-
function
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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 {
|
|
270
|
+
* @param {PatramClaim} claim
|
|
209
271
|
* @returns {string}
|
|
210
272
|
*/
|
|
211
|
-
function
|
|
212
|
-
|
|
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
|
/**
|