patram 0.10.0 → 0.12.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 (117) hide show
  1. package/bin/patram.js +4 -4
  2. package/lib/cli/arguments.types.d.ts +1 -0
  3. package/lib/cli/commands/check.js +27 -15
  4. package/lib/cli/commands/fields.js +0 -4
  5. package/lib/cli/commands/queries.js +179 -1
  6. package/lib/cli/commands/query.js +1 -8
  7. package/lib/cli/commands/refs.js +3 -10
  8. package/lib/cli/commands/show.js +1 -8
  9. package/lib/cli/help-metadata.js +106 -111
  10. package/lib/cli/main.js +10 -10
  11. package/lib/cli/parse-arguments-helpers.js +416 -66
  12. package/lib/cli/parse-arguments.js +4 -4
  13. package/lib/cli/render-help.js +10 -4
  14. package/lib/config/defaults.js +33 -25
  15. package/lib/config/load-patram-config.d.ts +19 -33
  16. package/lib/config/load-patram-config.js +18 -121
  17. package/lib/config/load-patram-config.types.d.ts +3 -40
  18. package/lib/config/manage-stored-queries-helpers.d.ts +69 -0
  19. package/lib/config/manage-stored-queries-helpers.js +320 -0
  20. package/lib/config/manage-stored-queries-jsonc.d.ts +31 -0
  21. package/lib/config/manage-stored-queries-jsonc.js +95 -0
  22. package/lib/config/manage-stored-queries.d.ts +77 -0
  23. package/lib/config/manage-stored-queries.js +300 -0
  24. package/lib/config/patram-config.d.ts +34 -34
  25. package/lib/config/patram-config.js +3 -3
  26. package/lib/config/patram-config.types.d.ts +5 -11
  27. package/lib/config/resolve-patram-graph-config.d.ts +5 -1
  28. package/lib/config/resolve-patram-graph-config.js +3 -119
  29. package/lib/config/schema.d.ts +158 -269
  30. package/lib/config/schema.js +72 -210
  31. package/lib/config/validate-patram-config-value.d.ts +13 -0
  32. package/lib/config/validate-patram-config-value.js +94 -0
  33. package/lib/config/validation.d.ts +2 -12
  34. package/lib/config/validation.js +125 -483
  35. package/lib/find-close-match.d.ts +4 -1
  36. package/lib/graph/build-graph-identity.d.ts +1 -32
  37. package/lib/graph/build-graph-identity.js +5 -269
  38. package/lib/graph/build-graph.d.ts +13 -4
  39. package/lib/graph/build-graph.js +347 -488
  40. package/lib/graph/build-graph.types.d.ts +8 -9
  41. package/lib/graph/check-directive-metadata-helpers.d.ts +30 -0
  42. package/lib/graph/check-directive-metadata-helpers.js +126 -0
  43. package/lib/graph/check-directive-metadata.d.ts +8 -9
  44. package/lib/graph/check-directive-metadata.js +70 -561
  45. package/lib/graph/check-directive-path-target.d.ts +6 -13
  46. package/lib/graph/check-directive-path-target.js +26 -57
  47. package/lib/graph/check-directive-value.d.ts +1 -5
  48. package/lib/graph/check-directive-value.js +40 -180
  49. package/lib/graph/check-graph.d.ts +5 -5
  50. package/lib/graph/check-graph.js +8 -6
  51. package/lib/graph/document-node-identity.d.ts +23 -7
  52. package/lib/graph/document-node-identity.js +417 -160
  53. package/lib/graph/graph-node.d.ts +42 -0
  54. package/lib/graph/graph-node.js +83 -0
  55. package/lib/graph/inspect-reverse-references.js +16 -11
  56. package/lib/graph/load-project-graph.d.ts +7 -7
  57. package/lib/graph/load-project-graph.js +7 -7
  58. package/lib/graph/parse-where-clause.types.d.ts +3 -2
  59. package/lib/graph/query/cypher-reader.d.ts +59 -0
  60. package/lib/graph/query/cypher-reader.js +151 -0
  61. package/lib/graph/query/cypher-support.d.ts +79 -0
  62. package/lib/graph/query/cypher-support.js +213 -0
  63. package/lib/graph/query/cypher-tokenize.d.ts +13 -0
  64. package/lib/graph/query/cypher-tokenize.js +225 -0
  65. package/lib/graph/query/cypher.types.d.ts +43 -0
  66. package/lib/graph/query/execute.d.ts +7 -7
  67. package/lib/graph/query/execute.js +71 -33
  68. package/lib/graph/query/inspect.js +58 -24
  69. package/lib/graph/query/parse-cypher-patterns.d.ts +27 -0
  70. package/lib/graph/query/parse-cypher-patterns.js +382 -0
  71. package/lib/graph/query/parse-cypher.d.ts +7 -0
  72. package/lib/graph/query/parse-cypher.js +580 -0
  73. package/lib/graph/query/parse-query.d.ts +13 -0
  74. package/lib/graph/query/parse-query.js +97 -0
  75. package/lib/graph/query/resolve.d.ts +6 -0
  76. package/lib/graph/query/resolve.js +81 -24
  77. package/lib/output/command-output.js +12 -5
  78. package/lib/output/compact-layout.js +221 -0
  79. package/lib/output/format-output-item-block.js +31 -1
  80. package/lib/output/format-output-metadata.js +16 -29
  81. package/lib/output/format-stored-query-block.js +95 -0
  82. package/lib/output/layout-incoming-references.js +101 -19
  83. package/lib/output/layout-stored-queries.js +23 -330
  84. package/lib/output/list-queries.js +1 -1
  85. package/lib/output/render-field-discovery.js +11 -2
  86. package/lib/output/render-output-view.js +9 -5
  87. package/lib/output/renderers/json.js +5 -26
  88. package/lib/output/renderers/plain.js +155 -35
  89. package/lib/output/renderers/rich.js +250 -36
  90. package/lib/output/resolve-check-target.js +120 -11
  91. package/lib/output/resolved-link-layout.js +43 -0
  92. package/lib/output/rich-source/render.js +193 -35
  93. package/lib/output/show-document.js +25 -18
  94. package/lib/output/view-model/index.js +124 -103
  95. package/lib/parse/jsdoc/parse-jsdoc-blocks.js +1 -1
  96. package/lib/parse/jsdoc/parse-jsdoc-claims.js +12 -6
  97. package/lib/parse/markdown/parse-markdown-claims.js +99 -62
  98. package/lib/parse/markdown/parse-markdown-directives.d.ts +10 -6
  99. package/lib/parse/markdown/parse-markdown-directives.js +104 -18
  100. package/lib/parse/markdown/parse-markdown-prose.d.ts +27 -0
  101. package/lib/parse/markdown/parse-markdown-prose.js +243 -0
  102. package/lib/parse/parse-claims.d.ts +2 -6
  103. package/lib/parse/parse-claims.js +11 -53
  104. package/lib/parse/tagged-fenced/tagged-fenced-blocks.d.ts +4 -4
  105. package/lib/parse/tagged-fenced/tagged-fenced-blocks.js +4 -4
  106. package/lib/parse/yaml/parse-yaml-claims.js +4 -4
  107. package/lib/patram.d.ts +9 -3
  108. package/lib/patram.js +1 -1
  109. package/lib/scan/discover-fields.js +194 -55
  110. package/lib/scan/list-source-files.d.ts +4 -4
  111. package/lib/scan/list-source-files.js +4 -4
  112. package/package.json +2 -1
  113. package/lib/directive-validation-test-helpers.js +0 -87
  114. package/lib/graph/query/parse.d.ts +0 -75
  115. package/lib/graph/query/parse.js +0 -1064
  116. package/lib/output/derived-summary.js +0 -280
  117. package/lib/output/format-derived-summary-row.js +0 -9
@@ -1,3 +1,4 @@
1
+ /* eslint-disable max-lines */
1
2
  /**
2
3
  * @import { CliColorMode } from '../../cli/arguments.types.ts';
3
4
  * @import { OutputNodeItem, OutputResolvedLinkItem, OutputStoredQueryItem, OutputView, QueryOutputView, RefsOutputView, ShowOutputView } from '../output-view.types.ts';
@@ -6,14 +7,27 @@
6
7
  import { Ansis } from 'ansis';
7
8
 
8
9
  import {
9
- formatOutputNodeMetadataRows,
10
- formatResolvedLinkMetadataRows,
10
+ formatOutputNodeStoredMetadataRow,
11
+ formatResolvedLinkStoredMetadataRow,
11
12
  } from '../format-output-metadata.js';
13
+ import {
14
+ formatCompactMetadataLabel,
15
+ formatCompactTitleRow,
16
+ getCompactLeftTitleWidth,
17
+ wrapCompactBodyText,
18
+ } from '../compact-layout.js';
12
19
  import { formatNodeHeader } from '../format-node-header.js';
13
- import { formatOutputItemBlock } from '../format-output-item-block.js';
14
- import { layoutIncomingReferenceLines } from '../layout-incoming-references.js';
20
+ import { formatStoredQueryBlock } from '../format-stored-query-block.js';
21
+ import {
22
+ collectIncomingRefHeaders,
23
+ createIncomingTreeRenderOptions,
24
+ layoutIncomingReferenceLines,
25
+ } from '../layout-incoming-references.js';
15
26
  import { layoutIncomingSummaryLines } from '../layout-incoming-summary-lines.js';
16
- import { layoutStoredQueries } from '../layout-stored-queries.js';
27
+ import {
28
+ formatResolvedLinkBodyLines,
29
+ formatResolvedLinkHeader,
30
+ } from '../resolved-link-layout.js';
17
31
  import { renderRichSource } from '../rich-source/render.js';
18
32
 
19
33
  const FULL_WIDTH_DIVIDER = ` ${'─'.repeat(78)} `;
@@ -22,22 +36,22 @@ const FULL_WIDTH_DIVIDER = ` ${'─'.repeat(78)} `;
22
36
  * Render styled rich output while preserving the plain layout.
23
37
  *
24
38
  * @param {OutputView} output_view
25
- * @param {{ color_mode: CliColorMode, color_enabled: boolean }} render_options
39
+ * @param {{ color_mode: CliColorMode, color_enabled: boolean, is_tty?: boolean, terminal_width?: number }} render_options
26
40
  * @returns {Promise<string>}
27
41
  */
28
42
  export async function renderRichOutput(output_view, render_options) {
29
43
  const ansi = createAnsi(render_options.color_enabled);
30
44
 
31
45
  if (output_view.command === 'query') {
32
- return renderRichQueryOutput(output_view, ansi);
46
+ return renderRichQueryOutput(output_view, render_options, ansi);
33
47
  }
34
48
 
35
49
  if (output_view.command === 'queries') {
36
- return renderRichStoredQueries(output_view, ansi);
50
+ return renderRichStoredQueries(output_view, render_options, ansi);
37
51
  }
38
52
 
39
53
  if (output_view.command === 'refs') {
40
- return renderRichRefsOutput(output_view, ansi);
54
+ return renderRichRefsOutput(output_view, render_options, ansi);
41
55
  }
42
56
  if (output_view.command === 'show') {
43
57
  return renderRichShowOutput(output_view, render_options, ansi);
@@ -48,10 +62,11 @@ export async function renderRichOutput(output_view, render_options) {
48
62
 
49
63
  /**
50
64
  * @param {QueryOutputView} output_view
65
+ * @param {{ is_tty?: boolean, terminal_width?: number }} render_options
51
66
  * @param {Ansis} ansi
52
67
  * @returns {string}
53
68
  */
54
- function renderRichQueryOutput(output_view, ansi) {
69
+ function renderRichQueryOutput(output_view, render_options, ansi) {
55
70
  const footer = renderRichQueryFooter(
56
71
  output_view.summary,
57
72
  output_view.hints,
@@ -62,11 +77,21 @@ function renderRichQueryOutput(output_view, ansi) {
62
77
  return renderRichEmptyQuery(footer, ansi);
63
78
  }
64
79
 
80
+ const left_title_width = getCompactLeftTitleWidth(
81
+ output_view.items.map((output_item) => formatNodeHeader(output_item)),
82
+ );
83
+ const output_blocks = output_view.items.map((output_item) =>
84
+ formatRichNodeItem(output_item, ansi, {
85
+ ...render_options,
86
+ left_title_width,
87
+ }),
88
+ );
89
+
65
90
  if (footer.length === 0) {
66
- return `${output_view.items.map((item) => formatRichNodeItem(item, ansi)).join('\n\n')}\n`;
91
+ return `${output_blocks.join('\n\n')}\n`;
67
92
  }
68
93
 
69
- return `${output_view.items.map((item) => formatRichNodeItem(item, ansi)).join('\n\n')}\n\n${footer}\n`;
94
+ return `${output_blocks.join('\n\n')}\n\n${footer}\n`;
70
95
  }
71
96
 
72
97
  /**
@@ -84,28 +109,29 @@ function renderRichEmptyQuery(footer, ansi) {
84
109
 
85
110
  /**
86
111
  * @param {Extract<OutputView, { command: 'queries' }>} output_view
112
+ * @param {{ is_tty?: boolean, terminal_width?: number }} render_options
87
113
  * @param {Ansis} ansi
88
114
  * @returns {string}
89
115
  */
90
- function renderRichStoredQueries(output_view, ansi) {
116
+ function renderRichStoredQueries(output_view, render_options, ansi) {
91
117
  if (output_view.items.length === 0) {
92
118
  return '';
93
119
  }
94
120
 
95
- const output_lines = layoutStoredQueries(output_view.items).map(
96
- (line_segments) => formatRichStoredQueryLine(line_segments, ansi),
121
+ const output_blocks = output_view.items.map((output_item) =>
122
+ formatRichStoredQueryBlock(output_item, ansi, render_options),
97
123
  );
98
124
 
99
125
  if (output_view.hints.length === 0) {
100
- return `${output_lines.join('\n')}\n`;
126
+ return `${output_blocks.join('\n')}\n`;
101
127
  }
102
128
 
103
- return `${output_lines.join('\n')}\n${output_view.hints.map((hint) => ansi.gray(hint)).join('\n')}\n`;
129
+ return `${output_blocks.join('\n')}\n${output_view.hints.map((hint) => ansi.gray(hint)).join('\n')}\n`;
104
130
  }
105
131
 
106
132
  /**
107
133
  * @param {ShowOutputView} output_view
108
- * @param {{ color_mode: CliColorMode, color_enabled: boolean }} render_options
134
+ * @param {{ color_mode: CliColorMode, color_enabled: boolean, is_tty?: boolean, terminal_width?: number }} render_options
109
135
  * @param {Ansis} ansi
110
136
  * @returns {Promise<string>}
111
137
  */
@@ -119,8 +145,15 @@ async function renderRichShowOutput(output_view, render_options, ansi) {
119
145
  return `${rendered_source}\n`;
120
146
  }
121
147
 
148
+ const left_title_width = getCompactLeftTitleWidth(
149
+ output_view.items.map(formatResolvedLinkHeader),
150
+ );
122
151
  const summary_items = output_view.items.map((item) =>
123
- formatRichResolvedLinkItem(item, ansi),
152
+ formatRichResolvedLinkItem(item, ansi, {
153
+ is_tty: render_options.is_tty,
154
+ left_title_width,
155
+ terminal_width: render_options.terminal_width,
156
+ }),
124
157
  );
125
158
 
126
159
  if (incoming_summary.length > 0) {
@@ -132,14 +165,26 @@ async function renderRichShowOutput(output_view, render_options, ansi) {
132
165
 
133
166
  /**
134
167
  * @param {RefsOutputView} output_view
168
+ * @param {{ is_tty?: boolean, terminal_width?: number }} render_options
135
169
  * @param {Ansis} ansi
136
170
  * @returns {string}
137
171
  */
138
- function renderRichRefsOutput(output_view, ansi) {
139
- const node_summary = formatRichNodeItem(output_view.node, ansi);
172
+ function renderRichRefsOutput(output_view, render_options, ansi) {
173
+ const left_title_width = getCompactLeftTitleWidth(
174
+ collectIncomingRefHeaders(output_view),
175
+ );
176
+ const incoming_render_options =
177
+ createIncomingTreeRenderOptions(render_options);
178
+ const node_summary = formatRichNodeItem(output_view.node, ansi, {
179
+ ...render_options,
180
+ left_title_width,
181
+ });
140
182
  const output_lines = layoutIncomingReferenceLines(output_view.incoming, {
141
- format_node_header(output_item) {
142
- return ansi.green(formatNodeHeader(output_item));
183
+ format_node_block(output_item) {
184
+ return formatRichNodeItem(output_item, ansi, {
185
+ ...incoming_render_options,
186
+ left_title_width,
187
+ });
143
188
  },
144
189
  format_relation_header(relation_name, relation_count) {
145
190
  return `${ansi.bold(relation_name)} ${ansi.gray(`(${relation_count})`)}`;
@@ -154,14 +199,44 @@ function renderRichRefsOutput(output_view, ansi) {
154
199
  /**
155
200
  * @param {OutputNodeItem} output_item
156
201
  * @param {Ansis} ansi
202
+ * @param {{ is_tty?: boolean, left_title_width: number, terminal_width?: number }} render_options
157
203
  * @returns {string}
158
204
  */
159
- function formatRichNodeItem(output_item, ansi) {
160
- return formatOutputItemBlock({
161
- header: ansi.green(formatNodeHeader(output_item)),
162
- metadata_rows: formatOutputNodeMetadataRows(output_item),
163
- title: output_item.title,
164
- });
205
+ function formatRichNodeItem(output_item, ansi, render_options) {
206
+ const metadata_label = formatStoredMetadataLabel(
207
+ formatOutputNodeStoredMetadataRow(output_item),
208
+ );
209
+ /** @type {string[]} */
210
+ const output_lines = [
211
+ formatCompactTitleRow({
212
+ format_left(text) {
213
+ return ansi.green(text);
214
+ },
215
+ format_right(text) {
216
+ return formatRichCompactMetadataLabel(text, ansi);
217
+ },
218
+ is_tty: render_options.is_tty,
219
+ left_title: formatNodeHeader(output_item),
220
+ left_title_width: render_options.left_title_width,
221
+ right_title: metadata_label,
222
+ terminal_width: render_options.terminal_width,
223
+ }),
224
+ ];
225
+
226
+ output_lines.push(` ${output_item.title}`);
227
+
228
+ if (output_item.description) {
229
+ for (const description_line of wrapCompactBodyText(
230
+ output_item.description,
231
+ render_options,
232
+ )) {
233
+ output_lines.push(
234
+ formatRichDescriptionLine(description_line, ' ', ansi),
235
+ );
236
+ }
237
+ }
238
+
239
+ return output_lines.join('\n');
165
240
  }
166
241
 
167
242
  /**
@@ -176,19 +251,72 @@ function formatRichStoredQueryLine(line_segments, ansi) {
176
251
  }
177
252
 
178
253
  /**
179
- * @param {OutputResolvedLinkItem} output_item
254
+ * @param {OutputStoredQueryItem} output_item
180
255
  * @param {Ansis} ansi
256
+ * @param {{ is_tty?: boolean, terminal_width?: number }} render_options
181
257
  * @returns {string}
182
258
  */
183
- function formatRichResolvedLinkItem(output_item, ansi) {
184
- return formatOutputItemBlock({
185
- header: `${ansi.gray(`[${output_item.reference}]`)} ${ansi.green(`${output_item.target.kind} ${output_item.target.path ?? output_item.target.id}`)}`,
186
- metadata_rows: formatResolvedLinkMetadataRows(output_item.target),
187
- metadata_indent: ' ',
188
- title: output_item.target.title,
259
+ function formatRichStoredQueryBlock(output_item, ansi, render_options) {
260
+ return formatStoredQueryBlock(output_item, render_options, {
261
+ format_description(text) {
262
+ return ansi.gray(text);
263
+ },
264
+ format_line(line_segments) {
265
+ return formatRichStoredQueryLine(line_segments, ansi);
266
+ },
267
+ format_name(text) {
268
+ return ansi.green(text);
269
+ },
189
270
  });
190
271
  }
191
272
 
273
+ /**
274
+ * @param {OutputResolvedLinkItem} output_item
275
+ * @param {Ansis} ansi
276
+ * @param {{ is_tty?: boolean, left_title_width: number, terminal_width?: number }} render_options
277
+ * @returns {string}
278
+ */
279
+ function formatRichResolvedLinkItem(output_item, ansi, render_options) {
280
+ const metadata_label = formatStoredMetadataLabel(
281
+ formatResolvedLinkStoredMetadataRow(output_item.target),
282
+ );
283
+ /** @type {string[]} */
284
+ const output_lines = [
285
+ formatCompactTitleRow({
286
+ format_left(text) {
287
+ const closing_bracket_index = text.indexOf(']');
288
+
289
+ if (closing_bracket_index === -1) {
290
+ return ansi.green(text);
291
+ }
292
+
293
+ const reference_token = text.slice(0, closing_bracket_index + 1);
294
+ const header_suffix = text.slice(closing_bracket_index + 1);
295
+
296
+ return `${ansi.gray(reference_token)}${ansi.green(header_suffix)}`;
297
+ },
298
+ format_right(text) {
299
+ return formatRichCompactMetadataLabel(text, ansi);
300
+ },
301
+ is_tty: render_options.is_tty,
302
+ left_title: formatResolvedLinkHeader(output_item),
303
+ left_title_width: render_options.left_title_width,
304
+ right_title: metadata_label,
305
+ terminal_width: render_options.terminal_width,
306
+ }),
307
+ ];
308
+
309
+ output_lines.push(
310
+ ...formatRichResolvedLinkBodyLines(output_item, ansi, {
311
+ body_indent: ' ',
312
+ is_tty: render_options.is_tty,
313
+ terminal_width: render_options.terminal_width,
314
+ }),
315
+ );
316
+
317
+ return output_lines.join('\n');
318
+ }
319
+
192
320
  /**
193
321
  * @param {ShowOutputView} output_view
194
322
  * @param {Ansis} ansi
@@ -223,6 +351,92 @@ function createAnsi(color_enabled) {
223
351
  return new Ansis(color_enabled ? 3 : 0);
224
352
  }
225
353
 
354
+ /**
355
+ * @param {string | undefined} metadata_row
356
+ * @returns {string | undefined}
357
+ */
358
+ function formatStoredMetadataLabel(metadata_row) {
359
+ if (!metadata_row) {
360
+ return undefined;
361
+ }
362
+
363
+ return formatCompactMetadataLabel([metadata_row]);
364
+ }
365
+
366
+ /**
367
+ * @param {string} metadata_label
368
+ * @param {Ansis} ansi
369
+ * @returns {string}
370
+ */
371
+ function formatRichCompactMetadataLabel(metadata_label, ansi) {
372
+ return [...metadata_label]
373
+ .map((character) =>
374
+ character === '(' ||
375
+ character === ')' ||
376
+ character === ',' ||
377
+ character === '='
378
+ ? ansi.gray(character)
379
+ : character,
380
+ )
381
+ .join('');
382
+ }
383
+
384
+ /**
385
+ * @param {string} description_line
386
+ * @param {string} indent
387
+ * @param {Ansis} ansi
388
+ * @returns {string}
389
+ */
390
+ function formatRichDescriptionLine(description_line, indent, ansi) {
391
+ if (description_line.length === 0) {
392
+ return '';
393
+ }
394
+
395
+ return `${indent}${ansi.gray(description_line)}`;
396
+ }
397
+
398
+ /**
399
+ * @param {OutputResolvedLinkItem} output_item
400
+ * @param {Ansis} ansi
401
+ * @param {{ body_indent?: string, is_tty?: boolean, terminal_width?: number }} render_options
402
+ * @returns {string[]}
403
+ */
404
+ function formatRichResolvedLinkBodyLines(output_item, ansi, render_options) {
405
+ const body_indent = render_options.body_indent ?? ' ';
406
+ const body_lines = formatResolvedLinkBodyLines(output_item, render_options);
407
+
408
+ if (body_lines.length <= 1) {
409
+ return body_lines;
410
+ }
411
+
412
+ return [
413
+ body_lines[0],
414
+ ...body_lines
415
+ .slice(1)
416
+ .map((line) =>
417
+ formatRichIndentedDescriptionLine(line, body_indent, ansi),
418
+ ),
419
+ ];
420
+ }
421
+
422
+ /**
423
+ * @param {string} line
424
+ * @param {string} indent
425
+ * @param {Ansis} ansi
426
+ * @returns {string}
427
+ */
428
+ function formatRichIndentedDescriptionLine(line, indent, ansi) {
429
+ if (line.length === 0) {
430
+ return '';
431
+ }
432
+
433
+ if (!line.startsWith(indent)) {
434
+ return ansi.gray(line);
435
+ }
436
+
437
+ return `${indent}${ansi.gray(line.slice(indent.length))}`;
438
+ }
439
+
226
440
  /**
227
441
  * @param {{ kind: 'description' | 'field_name' | 'keyword' | 'literal' | 'name' | 'operator' | 'plain', text: string }} line_segment
228
442
  * @param {Ansis} ansi
@@ -21,7 +21,7 @@ const CONFIG_FILE_NAME = '.patram.json';
21
21
  * @param {string | undefined} target_argument
22
22
  * @returns {Promise<ResolvedCheckTarget>}
23
23
  */
24
- export async function resolveCheckTarget(target_argument) {
24
+ async function resolveCheckTarget(target_argument) {
25
25
  if (target_argument === undefined) {
26
26
  return {
27
27
  project_directory: process.cwd(),
@@ -35,23 +35,19 @@ export async function resolveCheckTarget(target_argument) {
35
35
  ? absolute_target_path
36
36
  : dirname(absolute_target_path);
37
37
  const project_directory = await findProjectDirectory(target_directory);
38
-
39
38
  if (target_stats.isFile()) {
40
39
  const target_path = normalizeRepoRelativePath(
41
40
  relative(project_directory, absolute_target_path),
42
41
  );
43
-
44
42
  return {
45
43
  project_directory,
46
44
  target_kind: 'file',
47
45
  target_path,
48
46
  };
49
47
  }
50
-
51
48
  const target_path = normalizeRepoRelativePath(
52
49
  relative(project_directory, absolute_target_path),
53
50
  );
54
-
55
51
  if (target_path.length === 0) {
56
52
  return {
57
53
  project_directory,
@@ -65,7 +61,36 @@ export async function resolveCheckTarget(target_argument) {
65
61
  target_path,
66
62
  };
67
63
  }
64
+ /**
65
+ * @param {string[]} target_arguments
66
+ * @returns {Promise<ResolvedCheckTarget[]>}
67
+ */
68
+ export async function resolveCheckTargets(target_arguments) {
69
+ if (target_arguments.length === 0) {
70
+ return [await resolveCheckTarget(undefined)];
71
+ }
68
72
 
73
+ return Promise.all(
74
+ target_arguments.map((target_argument) =>
75
+ resolveCheckTarget(target_argument),
76
+ ),
77
+ );
78
+ }
79
+ /** @param {ResolvedCheckTarget[]} resolved_targets
80
+ * @returns {string | null}
81
+ */
82
+ export function resolveCheckTargetProjectDirectory(resolved_targets) {
83
+ const project_directory = resolved_targets[0]?.project_directory;
84
+ if (!project_directory) {
85
+ return null;
86
+ }
87
+ for (const resolved_target of resolved_targets) {
88
+ if (resolved_target.project_directory !== project_directory) {
89
+ return null;
90
+ }
91
+ }
92
+ return project_directory;
93
+ }
69
94
  /**
70
95
  * Select the source files covered by one resolved `check` target.
71
96
  *
@@ -73,10 +98,7 @@ export async function resolveCheckTarget(target_argument) {
73
98
  * @param {ResolvedCheckTarget} resolved_target
74
99
  * @returns {string[]}
75
100
  */
76
- export function selectCheckTargetSourceFiles(
77
- source_file_paths,
78
- resolved_target,
79
- ) {
101
+ function selectCheckTargetSourceFiles(source_file_paths, resolved_target) {
80
102
  if (resolved_target.target_kind === 'project') {
81
103
  return source_file_paths;
82
104
  }
@@ -91,7 +113,22 @@ export function selectCheckTargetSourceFiles(
91
113
  isPathInsideDirectory(source_file_path, resolved_target.target_path),
92
114
  );
93
115
  }
94
-
116
+ /**
117
+ * @param {string[]} source_file_paths
118
+ * @param {ResolvedCheckTarget[]} resolved_targets
119
+ * @returns {string[]}
120
+ */
121
+ export function selectCheckTargetsSourceFiles(
122
+ source_file_paths,
123
+ resolved_targets,
124
+ ) {
125
+ return selectCheckTargetValues(
126
+ source_file_paths,
127
+ resolved_targets,
128
+ selectCheckTargetSourceFiles,
129
+ (source_file_path) => source_file_path,
130
+ );
131
+ }
95
132
  /**
96
133
  * Filter diagnostics to one resolved `check` target.
97
134
  *
@@ -99,7 +136,7 @@ export function selectCheckTargetSourceFiles(
99
136
  * @param {ResolvedCheckTarget} resolved_target
100
137
  * @returns {PatramDiagnostic[]}
101
138
  */
102
- export function selectCheckTargetDiagnostics(diagnostics, resolved_target) {
139
+ function selectCheckTargetDiagnostics(diagnostics, resolved_target) {
103
140
  if (resolved_target.target_kind === 'project') {
104
141
  return diagnostics;
105
142
  }
@@ -115,6 +152,20 @@ export function selectCheckTargetDiagnostics(diagnostics, resolved_target) {
115
152
  );
116
153
  }
117
154
 
155
+ /**
156
+ * @param {PatramDiagnostic[]} diagnostics
157
+ * @param {ResolvedCheckTarget[]} resolved_targets
158
+ * @returns {PatramDiagnostic[]}
159
+ */
160
+ export function selectCheckTargetsDiagnostics(diagnostics, resolved_targets) {
161
+ return selectCheckTargetValues(
162
+ diagnostics,
163
+ resolved_targets,
164
+ selectCheckTargetDiagnostics,
165
+ createDiagnosticKey,
166
+ );
167
+ }
168
+
118
169
  /**
119
170
  * @param {string} start_directory
120
171
  * @returns {Promise<string>}
@@ -155,6 +206,49 @@ async function hasConfigFile(directory_path) {
155
206
  return true;
156
207
  }
157
208
 
209
+ /**
210
+ * @template ValueType
211
+ * @param {ValueType[]} values
212
+ * @param {ResolvedCheckTarget[]} resolved_targets
213
+ * @param {(values: ValueType[], resolved_target: ResolvedCheckTarget) => ValueType[]} select_values
214
+ * @param {(value: ValueType) => string} get_value_key
215
+ * @returns {ValueType[]}
216
+ */
217
+ function selectCheckTargetValues(
218
+ values,
219
+ resolved_targets,
220
+ select_values,
221
+ get_value_key,
222
+ ) {
223
+ if (
224
+ resolved_targets.some(
225
+ (resolved_target) => resolved_target.target_kind === 'project',
226
+ )
227
+ ) {
228
+ return values;
229
+ }
230
+
231
+ /** @type {Set<string>} */
232
+ const selected_keys = new Set();
233
+ /** @type {ValueType[]} */
234
+ const selected_values = [];
235
+
236
+ for (const resolved_target of resolved_targets) {
237
+ for (const value of select_values(values, resolved_target)) {
238
+ const value_key = get_value_key(value);
239
+
240
+ if (selected_keys.has(value_key)) {
241
+ continue;
242
+ }
243
+
244
+ selected_keys.add(value_key);
245
+ selected_values.push(value);
246
+ }
247
+ }
248
+
249
+ return selected_values;
250
+ }
251
+
158
252
  /**
159
253
  * @param {string} source_path
160
254
  * @param {string} directory_path
@@ -175,6 +269,21 @@ function normalizeRepoRelativePath(source_path) {
175
269
  return source_path.replaceAll('\\', '/');
176
270
  }
177
271
 
272
+ /**
273
+ * @param {PatramDiagnostic} diagnostic
274
+ * @returns {string}
275
+ */
276
+ function createDiagnosticKey(diagnostic) {
277
+ return [
278
+ diagnostic.path,
279
+ diagnostic.line,
280
+ diagnostic.column,
281
+ diagnostic.level,
282
+ diagnostic.code,
283
+ diagnostic.message,
284
+ ].join(':');
285
+ }
286
+
178
287
  /**
179
288
  * @param {unknown} error
180
289
  * @returns {error is NodeJS.ErrnoException}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @import { OutputResolvedLinkItem } from './output-view.types.ts';
3
+ */
4
+
5
+ import { wrapCompactBodyText } from './compact-layout.js';
6
+
7
+ /**
8
+ * @param {OutputResolvedLinkItem} output_item
9
+ * @returns {string}
10
+ */
11
+ export function formatResolvedLinkHeader(output_item) {
12
+ return `[${output_item.reference}] ${output_item.target.kind} ${output_item.target.path ?? output_item.target.id}`;
13
+ }
14
+
15
+ /**
16
+ * @param {OutputResolvedLinkItem} output_item
17
+ * @param {{ body_indent?: string, is_tty?: boolean, terminal_width?: number }} render_options
18
+ * @returns {string[]}
19
+ */
20
+ export function formatResolvedLinkBodyLines(output_item, render_options = {}) {
21
+ const body_indent = render_options.body_indent ?? ' ';
22
+ /** @type {string[]} */
23
+ const output_lines = [`${body_indent}${output_item.target.title}`];
24
+
25
+ if (!output_item.target.description) {
26
+ return output_lines;
27
+ }
28
+
29
+ for (const description_line of wrapCompactBodyText(
30
+ output_item.target.description,
31
+ {
32
+ body_indent,
33
+ is_tty: render_options.is_tty,
34
+ terminal_width: render_options.terminal_width,
35
+ },
36
+ )) {
37
+ output_lines.push(
38
+ description_line.length > 0 ? `${body_indent}${description_line}` : '',
39
+ );
40
+ }
41
+
42
+ return output_lines;
43
+ }