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
@@ -0,0 +1,419 @@
1
+ /* eslint-disable max-lines */
2
+ /**
3
+ * @import {
4
+ * CliCommandName,
5
+ * CliHelpTopicName,
6
+ * CliParseError,
7
+ * ParsedCliHelpRequest,
8
+ * } from './parse-cli-arguments.types.ts';
9
+ * @import { PatramDiagnostic } from './load-patram-config.types.ts';
10
+ */
11
+
12
+ import {
13
+ getCommandDefinition,
14
+ getHelpTopicDefinition,
15
+ getRootHelpDefinition,
16
+ listCommandNames,
17
+ listHelpTopicNames,
18
+ } from './cli-help-metadata.js';
19
+
20
+ /**
21
+ * @param {ParsedCliHelpRequest} help_request
22
+ * @returns {string}
23
+ */
24
+ export function renderHelpRequest(help_request) {
25
+ if (help_request.target_kind === 'root') {
26
+ return renderRootHelp();
27
+ }
28
+
29
+ if (help_request.target_kind === 'command') {
30
+ return renderCommandHelp(
31
+ /** @type {CliCommandName} */ (help_request.target_name),
32
+ );
33
+ }
34
+
35
+ return renderHelpTopic(
36
+ /** @type {CliHelpTopicName} */ (help_request.target_name),
37
+ );
38
+ }
39
+
40
+ /**
41
+ * @param {CliParseError} parse_error
42
+ * @returns {string}
43
+ */
44
+ export function renderCliParseError(parse_error) {
45
+ if (parse_error.code === 'unknown_command') {
46
+ return renderUnknownCommandError(parse_error.token, parse_error.suggestion);
47
+ }
48
+
49
+ if (parse_error.code === 'unknown_help_target') {
50
+ return renderUnknownHelpTargetError(
51
+ parse_error.token,
52
+ parse_error.suggestion,
53
+ );
54
+ }
55
+
56
+ if (parse_error.code === 'unknown_option') {
57
+ return renderUnknownOptionError(
58
+ parse_error.token,
59
+ parse_error.command_name,
60
+ parse_error.suggestion,
61
+ );
62
+ }
63
+
64
+ if (parse_error.code === 'option_not_valid_for_command') {
65
+ return renderInvalidCommandOptionError(
66
+ parse_error.command_name,
67
+ parse_error.token,
68
+ );
69
+ }
70
+
71
+ if (parse_error.code === 'missing_required_argument') {
72
+ return renderMissingRequiredArgumentError(
73
+ parse_error.command_name,
74
+ parse_error.argument_label,
75
+ );
76
+ }
77
+
78
+ return `${parse_error.message}\n`;
79
+ }
80
+
81
+ /**
82
+ * @param {PatramDiagnostic} diagnostic
83
+ * @returns {string}
84
+ */
85
+ export function renderInvalidWhereDiagnostic(diagnostic) {
86
+ const diagnostic_line = formatDiagnostic(diagnostic);
87
+
88
+ return joinOutputLines([
89
+ 'Invalid where clause:',
90
+ ` ${diagnostic_line}`,
91
+ '',
92
+ 'Next:',
93
+ ' patram help query-language',
94
+ ]);
95
+ }
96
+
97
+ /**
98
+ * @returns {string}
99
+ */
100
+ function renderRootHelp() {
101
+ const root_help = getRootHelpDefinition();
102
+
103
+ return joinOutputLines([
104
+ 'Usage:',
105
+ ...indentLines(root_help.usage_lines),
106
+ '',
107
+ root_help.summary,
108
+ '',
109
+ 'Commands:',
110
+ ...listCommandNames().map((command_name) =>
111
+ formatSummaryLine(
112
+ command_name,
113
+ getCommandDefinition(command_name).root_summary,
114
+ ),
115
+ ),
116
+ '',
117
+ 'Global options:',
118
+ ...indentLines(root_help.global_options),
119
+ '',
120
+ 'Next:',
121
+ ' patram help <command>',
122
+ ]);
123
+ }
124
+
125
+ /**
126
+ * @param {CliCommandName} command_name
127
+ * @returns {string}
128
+ */
129
+ function renderCommandHelp(command_name) {
130
+ const command_definition = getCommandDefinition(command_name);
131
+ /** @type {string[]} */
132
+ const output_lines = [
133
+ 'Usage:',
134
+ ...indentLines(command_definition.usage_lines),
135
+ '',
136
+ command_definition.summary,
137
+ ];
138
+
139
+ if (
140
+ command_definition.syntax_lines &&
141
+ command_definition.syntax_lines.length
142
+ ) {
143
+ output_lines.push(
144
+ '',
145
+ 'Where clause:',
146
+ ...indentLines(command_definition.syntax_lines),
147
+ );
148
+ }
149
+
150
+ output_lines.push(
151
+ '',
152
+ 'Options:',
153
+ ...command_definition.options.map((option) =>
154
+ formatSummaryLine(
155
+ option.label,
156
+ option.description,
157
+ command_definition.option_column_width,
158
+ ),
159
+ ),
160
+ '',
161
+ 'Examples:',
162
+ ...indentLines(command_definition.examples),
163
+ '',
164
+ 'Related:',
165
+ ...indentLines(
166
+ command_definition.related.map(
167
+ (related_name) => `patram ${related_name}`,
168
+ ),
169
+ ),
170
+ );
171
+
172
+ if (command_definition.help_topics.length > 0) {
173
+ output_lines.push(
174
+ '',
175
+ 'Help topics:',
176
+ ...indentLines(
177
+ command_definition.help_topics.map(
178
+ (help_topic_name) => `patram help ${help_topic_name}`,
179
+ ),
180
+ ),
181
+ );
182
+ }
183
+
184
+ return joinOutputLines(output_lines);
185
+ }
186
+
187
+ /**
188
+ * @param {CliHelpTopicName} help_topic_name
189
+ * @returns {string}
190
+ */
191
+ function renderHelpTopic(help_topic_name) {
192
+ const help_topic = getHelpTopicDefinition(help_topic_name);
193
+
194
+ return joinOutputLines([
195
+ help_topic.lead,
196
+ '',
197
+ 'Usage:',
198
+ ...indentLines(help_topic.usage_lines),
199
+ '',
200
+ 'Fields:',
201
+ ...indentLines(help_topic.terms),
202
+ '',
203
+ 'Relations:',
204
+ ...help_topic.relation_terms.map((relation_term) =>
205
+ formatSummaryLine(
206
+ relation_term.label,
207
+ relation_term.description,
208
+ getSummaryColumnWidth(help_topic.relation_terms, 23),
209
+ ),
210
+ ),
211
+ '',
212
+ 'Operators:',
213
+ ...help_topic.operators.map((operator) =>
214
+ formatSummaryLine(
215
+ operator.label,
216
+ operator.description,
217
+ getSummaryColumnWidth(help_topic.operators, 7),
218
+ ),
219
+ ),
220
+ '',
221
+ 'Examples:',
222
+ ...indentLines(help_topic.examples),
223
+ ]);
224
+ }
225
+
226
+ /**
227
+ * @param {string} invalid_token
228
+ * @param {CliCommandName | undefined} suggestion
229
+ * @returns {string}
230
+ */
231
+ function renderUnknownCommandError(invalid_token, suggestion) {
232
+ if (suggestion) {
233
+ return joinOutputLines([
234
+ `Unknown command: ${invalid_token}`,
235
+ '',
236
+ 'Did you mean:',
237
+ ` ${suggestion}`,
238
+ '',
239
+ 'Next:',
240
+ ` patram help ${suggestion}`,
241
+ ]);
242
+ }
243
+
244
+ return joinOutputLines([
245
+ `Unknown command: ${invalid_token}`,
246
+ '',
247
+ 'Commands:',
248
+ ...indentLines(listCommandNames()),
249
+ '',
250
+ 'Next:',
251
+ ' patram --help',
252
+ ]);
253
+ }
254
+
255
+ /**
256
+ * @param {string} invalid_token
257
+ * @param {CliCommandName | undefined} command_name
258
+ * @param {string | undefined} suggestion
259
+ * @returns {string}
260
+ */
261
+ function renderUnknownOptionError(invalid_token, command_name, suggestion) {
262
+ if (suggestion) {
263
+ return joinOutputLines([
264
+ `Unknown option: ${invalid_token}`,
265
+ '',
266
+ 'Did you mean:',
267
+ ` ${suggestion}`,
268
+ '',
269
+ 'Next:',
270
+ ` ${renderCommandHelpPath(command_name)}`,
271
+ ]);
272
+ }
273
+
274
+ /** @type {string[]} */
275
+ const output_lines = [`Unknown option: ${invalid_token}`];
276
+
277
+ if (command_name) {
278
+ output_lines.push(
279
+ '',
280
+ 'Usage:',
281
+ ...indentLines(getCommandDefinition(command_name).usage_lines),
282
+ '',
283
+ 'Next:',
284
+ ` ${renderCommandHelpPath(command_name)}`,
285
+ );
286
+ } else {
287
+ output_lines.push('', 'Next:', ' patram --help');
288
+ }
289
+
290
+ return joinOutputLines(output_lines);
291
+ }
292
+
293
+ /**
294
+ * @param {CliCommandName} command_name
295
+ * @param {string} invalid_token
296
+ * @returns {string}
297
+ */
298
+ function renderInvalidCommandOptionError(command_name, invalid_token) {
299
+ return joinOutputLines([
300
+ `Option not valid for command: ${invalid_token}`,
301
+ '',
302
+ 'Usage:',
303
+ ...indentLines(getCommandDefinition(command_name).usage_lines),
304
+ '',
305
+ 'Next:',
306
+ ` ${renderCommandHelpPath(command_name)}`,
307
+ ]);
308
+ }
309
+
310
+ /**
311
+ * @param {'query' | 'show'} command_name
312
+ * @param {string} argument_label
313
+ * @returns {string}
314
+ */
315
+ function renderMissingRequiredArgumentError(command_name, argument_label) {
316
+ const command_definition = getCommandDefinition(command_name);
317
+
318
+ return joinOutputLines([
319
+ `Missing required argument: ${argument_label}`,
320
+ '',
321
+ 'Usage:',
322
+ ...indentLines(command_definition.missing_usage_lines),
323
+ '',
324
+ 'Examples:',
325
+ ...indentLines(command_definition.missing_argument_examples),
326
+ ]);
327
+ }
328
+
329
+ /**
330
+ * @param {string} invalid_token
331
+ * @param {CliCommandName | CliHelpTopicName | undefined} suggestion
332
+ * @returns {string}
333
+ */
334
+ function renderUnknownHelpTargetError(invalid_token, suggestion) {
335
+ if (suggestion) {
336
+ return joinOutputLines([
337
+ `Unknown help topic or command: ${invalid_token}`,
338
+ '',
339
+ 'Did you mean:',
340
+ ` ${suggestion}`,
341
+ '',
342
+ 'Next:',
343
+ ` patram help ${suggestion}`,
344
+ ]);
345
+ }
346
+
347
+ return joinOutputLines([
348
+ `Unknown help topic or command: ${invalid_token}`,
349
+ '',
350
+ 'Help topics:',
351
+ ...indentLines(listHelpTopicNames()),
352
+ '',
353
+ 'Commands:',
354
+ ...indentLines(listCommandNames()),
355
+ '',
356
+ 'Next:',
357
+ ' patram help query',
358
+ ]);
359
+ }
360
+
361
+ /**
362
+ * @param {PatramDiagnostic} diagnostic
363
+ * @returns {string}
364
+ */
365
+ function formatDiagnostic(diagnostic) {
366
+ return `${diagnostic.path}:${diagnostic.line}:${diagnostic.column} ${diagnostic.level} ${diagnostic.code} ${diagnostic.message}`;
367
+ }
368
+
369
+ /**
370
+ * @param {string | undefined} command_name
371
+ * @returns {string}
372
+ */
373
+ function renderCommandHelpPath(command_name) {
374
+ if (!command_name) {
375
+ return 'patram --help';
376
+ }
377
+
378
+ return `patram help ${command_name}`;
379
+ }
380
+
381
+ /**
382
+ * @param {string[]} lines
383
+ * @returns {string[]}
384
+ */
385
+ function indentLines(lines) {
386
+ return lines.map((line) => ` ${line}`);
387
+ }
388
+
389
+ /**
390
+ * @param {string} label
391
+ * @param {string} description
392
+ * @param {number} width
393
+ * @returns {string}
394
+ */
395
+ function formatSummaryLine(label, description, width = 9) {
396
+ return ` ${label.padEnd(width)}${description}`;
397
+ }
398
+
399
+ /**
400
+ * @param {{ label: string }[]} items
401
+ * @param {number} minimum_width
402
+ * @returns {number}
403
+ */
404
+ function getSummaryColumnWidth(items, minimum_width) {
405
+ const longest_label = items.reduce(
406
+ (current_width, item) => Math.max(current_width, item.label.length),
407
+ 0,
408
+ );
409
+
410
+ return Math.max(minimum_width, longest_label + 2);
411
+ }
412
+
413
+ /**
414
+ * @param {string[]} output_lines
415
+ * @returns {string}
416
+ */
417
+ function joinOutputLines(output_lines) {
418
+ return `${output_lines.join('\n')}\n`;
419
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * @import { OutputNodeItem, OutputResolvedLinkItem, OutputStoredQueryItem, OutputView } from './output-view.types.ts';
3
+ */
4
+
5
+ /**
6
+ * Render structured JSON output for one output view.
7
+ *
8
+ * @param {OutputView} output_view
9
+ * @returns {string}
10
+ */
11
+ export function renderJsonOutput(output_view) {
12
+ if (output_view.command === 'query') {
13
+ return `${JSON.stringify(
14
+ {
15
+ results: output_view.items.map(formatJsonQueryItem),
16
+ summary: {
17
+ shown_count: output_view.summary.count,
18
+ total_count: output_view.summary.total_count,
19
+ offset: output_view.summary.offset,
20
+ limit: output_view.summary.limit,
21
+ },
22
+ hints: output_view.hints,
23
+ },
24
+ null,
25
+ 2,
26
+ )}\n`;
27
+ }
28
+
29
+ if (output_view.command === 'queries') {
30
+ return `${JSON.stringify(
31
+ {
32
+ queries: output_view.items.map(formatJsonStoredQuery),
33
+ },
34
+ null,
35
+ 2,
36
+ )}\n`;
37
+ }
38
+
39
+ if (output_view.command === 'show') {
40
+ return `${JSON.stringify(
41
+ {
42
+ document: output_view.document
43
+ ? formatJsonShowDocument(output_view.document)
44
+ : undefined,
45
+ source: output_view.source,
46
+ resolved_links: output_view.items.map(formatJsonResolvedLink),
47
+ },
48
+ null,
49
+ 2,
50
+ )}\n`;
51
+ }
52
+
53
+ throw new Error('Unsupported output view command.');
54
+ }
55
+
56
+ /**
57
+ * @param {OutputNodeItem} output_item
58
+ * @returns {{ derived?: Record<string, boolean | number | string | null>, derived_summary?: string, id: string, kind: string, title: string, path: string, status?: string }}
59
+ */
60
+ function formatJsonQueryItem(output_item) {
61
+ /** @type {{ derived?: Record<string, boolean | number | string | null>, derived_summary?: string, id: string, kind: string, title: string, path: string, status?: string }} */
62
+ const query_item = {
63
+ id: output_item.id,
64
+ kind: output_item.node_kind,
65
+ title: output_item.title,
66
+ path: output_item.path,
67
+ };
68
+
69
+ if (output_item.status) {
70
+ query_item.status = output_item.status;
71
+ }
72
+
73
+ if (output_item.derived_summary) {
74
+ query_item.derived_summary = output_item.derived_summary.name;
75
+ query_item.derived = Object.fromEntries(
76
+ output_item.derived_summary.fields.map((field) => [
77
+ field.name,
78
+ field.value,
79
+ ]),
80
+ );
81
+ }
82
+
83
+ return query_item;
84
+ }
85
+
86
+ /**
87
+ * @param {OutputStoredQueryItem} output_item
88
+ * @returns {{ name: string, where: string }}
89
+ */
90
+ function formatJsonStoredQuery(output_item) {
91
+ return {
92
+ name: output_item.name,
93
+ where: output_item.where,
94
+ };
95
+ }
96
+
97
+ /**
98
+ * @param {OutputResolvedLinkItem} output_item
99
+ * @returns {{ label: string, reference: number, target: { derived?: Record<string, boolean | number | string | null>, derived_summary?: string, kind?: string, path: string, status?: string, title: string } }}
100
+ */
101
+ function formatJsonResolvedLink(output_item) {
102
+ /** @type {{ label: string, reference: number, target: { derived?: Record<string, boolean | number | string | null>, derived_summary?: string, kind?: string, path: string, status?: string, title: string } }} */
103
+ const resolved_link = {
104
+ reference: output_item.reference,
105
+ label: output_item.label,
106
+ target: {
107
+ title: output_item.target.title,
108
+ path: output_item.target.path,
109
+ },
110
+ };
111
+
112
+ if (output_item.target.kind) {
113
+ resolved_link.target.kind = output_item.target.kind;
114
+ }
115
+
116
+ if (output_item.target.status) {
117
+ resolved_link.target.status = output_item.target.status;
118
+ }
119
+
120
+ if (output_item.target.derived_summary) {
121
+ resolved_link.target.derived_summary =
122
+ output_item.target.derived_summary.name;
123
+ resolved_link.target.derived = Object.fromEntries(
124
+ output_item.target.derived_summary.fields.map((field) => [
125
+ field.name,
126
+ field.value,
127
+ ]),
128
+ );
129
+ }
130
+
131
+ return resolved_link;
132
+ }
133
+
134
+ /**
135
+ * @param {OutputNodeItem} output_item
136
+ * @returns {{ derived?: Record<string, boolean | number | string | null>, derived_summary?: string, kind: string, title: string, path: string, status?: string }}
137
+ */
138
+ function formatJsonShowDocument(output_item) {
139
+ /** @type {{ derived?: Record<string, boolean | number | string | null>, derived_summary?: string, kind: string, title: string, path: string, status?: string }} */
140
+ const document_summary = {
141
+ kind: output_item.node_kind,
142
+ path: output_item.path,
143
+ title: output_item.title,
144
+ };
145
+
146
+ if (output_item.status) {
147
+ document_summary.status = output_item.status;
148
+ }
149
+
150
+ if (output_item.derived_summary) {
151
+ document_summary.derived_summary = output_item.derived_summary.name;
152
+ document_summary.derived = Object.fromEntries(
153
+ output_item.derived_summary.fields.map((field) => [
154
+ field.name,
155
+ field.value,
156
+ ]),
157
+ );
158
+ }
159
+
160
+ return document_summary;
161
+ }