patram 0.0.2 → 0.2.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 (67) hide show
  1. package/bin/patram.js +25 -147
  2. package/lib/build-graph-identity.js +270 -0
  3. package/lib/build-graph.js +156 -77
  4. package/lib/check-graph.js +23 -7
  5. package/lib/claim-helpers.js +55 -0
  6. package/lib/cli-help-metadata.js +552 -0
  7. package/lib/command-output.js +83 -0
  8. package/lib/derived-summary.js +278 -0
  9. package/lib/format-derived-summary-row.js +9 -0
  10. package/lib/format-node-header.js +19 -0
  11. package/lib/format-output-item-block.js +22 -0
  12. package/lib/format-output-metadata.js +62 -0
  13. package/lib/layout-stored-queries.js +361 -0
  14. package/lib/list-queries.js +18 -0
  15. package/lib/list-source-files.js +50 -15
  16. package/lib/load-patram-config.js +505 -18
  17. package/lib/load-patram-config.types.ts +40 -0
  18. package/lib/load-project-graph.js +124 -0
  19. package/lib/output-view.types.ts +88 -0
  20. package/lib/parse-claims.js +38 -158
  21. package/lib/parse-claims.types.ts +7 -0
  22. package/lib/parse-cli-arguments-helpers.js +446 -0
  23. package/lib/parse-cli-arguments.js +266 -0
  24. package/lib/parse-cli-arguments.types.ts +69 -0
  25. package/lib/parse-cli-color-options.js +44 -0
  26. package/lib/parse-cli-query-pagination.js +49 -0
  27. package/lib/parse-jsdoc-blocks.js +184 -0
  28. package/lib/parse-jsdoc-claims.js +280 -0
  29. package/lib/parse-jsdoc-prose.js +111 -0
  30. package/lib/parse-markdown-claims.js +242 -0
  31. package/lib/parse-markdown-directives.js +136 -0
  32. package/lib/parse-where-clause.js +707 -0
  33. package/lib/parse-where-clause.types.ts +70 -0
  34. package/lib/patram-cli.js +464 -0
  35. package/lib/patram-config.js +3 -1
  36. package/lib/patram-config.types.ts +2 -1
  37. package/lib/patram.js +6 -0
  38. package/lib/query-graph.js +368 -0
  39. package/lib/query-inspection.js +523 -0
  40. package/lib/render-check-output.js +315 -0
  41. package/lib/render-cli-help.js +419 -0
  42. package/lib/render-json-output.js +161 -0
  43. package/lib/render-output-view.js +222 -0
  44. package/lib/render-plain-output.js +182 -0
  45. package/lib/render-rich-output.js +240 -0
  46. package/lib/render-rich-source.js +1333 -0
  47. package/lib/resolve-check-target.js +190 -0
  48. package/lib/resolve-output-mode.js +60 -0
  49. package/lib/resolve-patram-graph-config.js +88 -0
  50. package/lib/resolve-where-clause.js +66 -0
  51. package/lib/show-document.js +311 -0
  52. package/lib/source-file-defaults.js +28 -0
  53. package/lib/tagged-fenced-block-error.js +17 -0
  54. package/lib/tagged-fenced-block-markdown.js +111 -0
  55. package/lib/tagged-fenced-block-metadata.js +97 -0
  56. package/lib/tagged-fenced-block-parser.js +292 -0
  57. package/lib/tagged-fenced-blocks.js +100 -0
  58. package/lib/tagged-fenced-blocks.types.ts +38 -0
  59. package/lib/write-paged-output.js +87 -0
  60. package/package.json +28 -12
  61. package/bin/patram.test.js +0 -184
  62. package/lib/build-graph.test.js +0 -141
  63. package/lib/check-graph.test.js +0 -103
  64. package/lib/list-source-files.test.js +0 -101
  65. package/lib/load-patram-config.test.js +0 -211
  66. package/lib/parse-claims.test.js +0 -113
  67. 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,270 @@
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
+ * @param {Set<string>} document_paths
81
+ * @returns {{ key: string, path?: string }}
82
+ */
83
+ export function resolveTargetReference(
84
+ target_kind,
85
+ target_type,
86
+ claim,
87
+ document_entity_keys,
88
+ document_paths,
89
+ ) {
90
+ if (target_type === 'value') {
91
+ return resolveValueTargetReference(target_kind, claim);
92
+ }
93
+
94
+ return resolvePathTargetReference(
95
+ target_kind,
96
+ claim,
97
+ document_entity_keys,
98
+ document_paths,
99
+ );
100
+ }
101
+
102
+ /**
103
+ * Attach one canonical path to a non-document graph node.
104
+ *
105
+ * @param {GraphNode} graph_node
106
+ * @param {string | undefined} source_path
107
+ */
108
+ export function setNonDocumentPath(graph_node, source_path) {
109
+ if (!source_path || graph_node.kind === 'document') {
110
+ return;
111
+ }
112
+
113
+ if (!graph_node.path) {
114
+ graph_node.path = source_path;
115
+ return;
116
+ }
117
+
118
+ if (graph_node.path !== source_path) {
119
+ throw new Error(
120
+ `Node "${graph_node.id}" maps to multiple canonical paths.`,
121
+ );
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Normalize one repo-relative source path.
127
+ *
128
+ * @param {string} source_path
129
+ * @returns {string}
130
+ */
131
+ export function normalizeRepoRelativePath(source_path) {
132
+ return posix.normalize(source_path.replaceAll('\\', '/'));
133
+ }
134
+
135
+ /**
136
+ * @param {Record<string, MappingDefinition>} mappings
137
+ * @param {PatramClaim} claim
138
+ * @returns {MappingDefinition | null}
139
+ */
140
+ function resolveMappingDefinition(mappings, claim) {
141
+ if (claim.type === 'directive') {
142
+ return resolveDirectiveMapping(mappings, claim);
143
+ }
144
+
145
+ return mappings[claim.type] ?? null;
146
+ }
147
+
148
+ /**
149
+ * @param {Record<string, MappingDefinition>} mappings
150
+ * @param {PatramClaim} claim
151
+ * @returns {MappingDefinition | null}
152
+ */
153
+ function resolveDirectiveMapping(mappings, claim) {
154
+ if (!claim.parser || !claim.name) {
155
+ return null;
156
+ }
157
+
158
+ return mappings[`${claim.parser}.directive.${claim.name}`] ?? null;
159
+ }
160
+
161
+ /**
162
+ * @param {string} target_kind
163
+ * @param {PatramClaim} claim
164
+ * @returns {{ key: string, path?: string }}
165
+ */
166
+ function resolveValueTargetReference(target_kind, claim) {
167
+ const target_key = getStringClaimValue(claim);
168
+
169
+ if (target_kind === 'document') {
170
+ return {
171
+ key: target_key,
172
+ path: target_key,
173
+ };
174
+ }
175
+
176
+ return {
177
+ key: target_key,
178
+ path: normalizeRepoRelativePath(claim.origin.path),
179
+ };
180
+ }
181
+
182
+ /**
183
+ * @param {string} target_kind
184
+ * @param {PatramClaim} claim
185
+ * @param {Map<string, string>} document_entity_keys
186
+ * @param {Set<string>} document_paths
187
+ * @returns {{ key: string, path?: string }}
188
+ */
189
+ function resolvePathTargetReference(
190
+ target_kind,
191
+ claim,
192
+ document_entity_keys,
193
+ document_paths,
194
+ ) {
195
+ const raw_target = getPathTargetValue(claim);
196
+ const target_path = resolveDirectiveAwareTargetPath(
197
+ claim,
198
+ raw_target,
199
+ document_paths,
200
+ );
201
+
202
+ if (target_kind === 'document') {
203
+ return {
204
+ key: target_path,
205
+ path: target_path,
206
+ };
207
+ }
208
+
209
+ const semantic_target_key = document_entity_keys.get(
210
+ getDocumentEntityMapKey(target_path, target_kind),
211
+ );
212
+
213
+ return {
214
+ key: semantic_target_key ?? target_path,
215
+ path: target_path,
216
+ };
217
+ }
218
+
219
+ /**
220
+ * @param {PatramClaim} claim
221
+ * @param {string} raw_target
222
+ * @param {Set<string>} document_paths
223
+ * @returns {string}
224
+ */
225
+ function resolveDirectiveAwareTargetPath(claim, raw_target, document_paths) {
226
+ const normalized_raw_target = normalizeRepoRelativePath(raw_target);
227
+
228
+ if (claim.type === 'directive' && document_paths.has(normalized_raw_target)) {
229
+ return normalized_raw_target;
230
+ }
231
+
232
+ const source_directory = posix.dirname(
233
+ normalizeRepoRelativePath(claim.origin.path),
234
+ );
235
+
236
+ return normalizeRepoRelativePath(posix.join(source_directory, raw_target));
237
+ }
238
+
239
+ /**
240
+ * @param {PatramClaim} claim
241
+ * @returns {string}
242
+ */
243
+ function getPathTargetValue(claim) {
244
+ if (typeof claim.value === 'string') {
245
+ return claim.value;
246
+ }
247
+
248
+ return claim.value.target;
249
+ }
250
+
251
+ /**
252
+ * @param {PatramClaim} claim
253
+ * @returns {string}
254
+ */
255
+ function getStringClaimValue(claim) {
256
+ if (typeof claim.value === 'string') {
257
+ return claim.value;
258
+ }
259
+
260
+ throw new Error(`Claim "${claim.id}" does not carry a string value.`);
261
+ }
262
+
263
+ /**
264
+ * @param {string} document_path
265
+ * @param {string} kind_name
266
+ * @returns {string}
267
+ */
268
+ function getDocumentEntityMapKey(document_path, kind_name) {
269
+ return `${kind_name}:${document_path}`;
270
+ }