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,190 @@
1
+ /**
2
+ * @import { PatramDiagnostic } from './load-patram-config.types.ts';
3
+ */
4
+ import { access, stat } from 'node:fs/promises';
5
+ import { dirname, relative, resolve } from 'node:path';
6
+ import process from 'node:process';
7
+
8
+ const CONFIG_FILE_NAME = '.patram.json';
9
+
10
+ /**
11
+ * @typedef {(
12
+ * | { project_directory: string, target_kind: 'project' }
13
+ * | { project_directory: string, target_kind: 'directory', target_path: string }
14
+ * | { project_directory: string, target_kind: 'file', target_path: string }
15
+ * )} ResolvedCheckTarget
16
+ */
17
+
18
+ /**
19
+ * Resolve the project directory and target scope for `check`.
20
+ *
21
+ * @param {string | undefined} target_argument
22
+ * @returns {Promise<ResolvedCheckTarget>}
23
+ */
24
+ export async function resolveCheckTarget(target_argument) {
25
+ if (target_argument === undefined) {
26
+ return {
27
+ project_directory: process.cwd(),
28
+ target_kind: 'project',
29
+ };
30
+ }
31
+
32
+ const absolute_target_path = resolve(process.cwd(), target_argument);
33
+ const target_stats = await stat(absolute_target_path);
34
+ const target_directory = target_stats.isDirectory()
35
+ ? absolute_target_path
36
+ : dirname(absolute_target_path);
37
+ const project_directory = await findProjectDirectory(target_directory);
38
+
39
+ if (target_stats.isFile()) {
40
+ const target_path = normalizeRepoRelativePath(
41
+ relative(project_directory, absolute_target_path),
42
+ );
43
+
44
+ return {
45
+ project_directory,
46
+ target_kind: 'file',
47
+ target_path,
48
+ };
49
+ }
50
+
51
+ const target_path = normalizeRepoRelativePath(
52
+ relative(project_directory, absolute_target_path),
53
+ );
54
+
55
+ if (target_path.length === 0) {
56
+ return {
57
+ project_directory,
58
+ target_kind: 'project',
59
+ };
60
+ }
61
+
62
+ return {
63
+ project_directory,
64
+ target_kind: 'directory',
65
+ target_path,
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Select the source files covered by one resolved `check` target.
71
+ *
72
+ * @param {string[]} source_file_paths
73
+ * @param {ResolvedCheckTarget} resolved_target
74
+ * @returns {string[]}
75
+ */
76
+ export function selectCheckTargetSourceFiles(
77
+ source_file_paths,
78
+ resolved_target,
79
+ ) {
80
+ if (resolved_target.target_kind === 'project') {
81
+ return source_file_paths;
82
+ }
83
+
84
+ if (resolved_target.target_kind === 'file') {
85
+ return source_file_paths.filter(
86
+ (source_file_path) => source_file_path === resolved_target.target_path,
87
+ );
88
+ }
89
+
90
+ return source_file_paths.filter((source_file_path) =>
91
+ isPathInsideDirectory(source_file_path, resolved_target.target_path),
92
+ );
93
+ }
94
+
95
+ /**
96
+ * Filter diagnostics to one resolved `check` target.
97
+ *
98
+ * @param {PatramDiagnostic[]} diagnostics
99
+ * @param {ResolvedCheckTarget} resolved_target
100
+ * @returns {PatramDiagnostic[]}
101
+ */
102
+ export function selectCheckTargetDiagnostics(diagnostics, resolved_target) {
103
+ if (resolved_target.target_kind === 'project') {
104
+ return diagnostics;
105
+ }
106
+
107
+ if (resolved_target.target_kind === 'file') {
108
+ return diagnostics.filter(
109
+ (diagnostic) => diagnostic.path === resolved_target.target_path,
110
+ );
111
+ }
112
+
113
+ return diagnostics.filter((diagnostic) =>
114
+ isPathInsideDirectory(diagnostic.path, resolved_target.target_path),
115
+ );
116
+ }
117
+
118
+ /**
119
+ * @param {string} start_directory
120
+ * @returns {Promise<string>}
121
+ */
122
+ async function findProjectDirectory(start_directory) {
123
+ let current_directory = start_directory;
124
+
125
+ while (true) {
126
+ if (await hasConfigFile(current_directory)) {
127
+ return current_directory;
128
+ }
129
+
130
+ const parent_directory = dirname(current_directory);
131
+
132
+ if (parent_directory === current_directory) {
133
+ return start_directory;
134
+ }
135
+
136
+ current_directory = parent_directory;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * @param {string} directory_path
142
+ * @returns {Promise<boolean>}
143
+ */
144
+ async function hasConfigFile(directory_path) {
145
+ try {
146
+ await access(resolve(directory_path, CONFIG_FILE_NAME));
147
+ } catch (error) {
148
+ if (isMissingPathError(error)) {
149
+ return false;
150
+ }
151
+
152
+ throw error;
153
+ }
154
+
155
+ return true;
156
+ }
157
+
158
+ /**
159
+ * @param {string} source_path
160
+ * @param {string} directory_path
161
+ * @returns {boolean}
162
+ */
163
+ function isPathInsideDirectory(source_path, directory_path) {
164
+ return (
165
+ source_path === directory_path ||
166
+ source_path.startsWith(`${directory_path}/`)
167
+ );
168
+ }
169
+
170
+ /**
171
+ * @param {string} source_path
172
+ * @returns {string}
173
+ */
174
+ function normalizeRepoRelativePath(source_path) {
175
+ return source_path.replaceAll('\\', '/');
176
+ }
177
+
178
+ /**
179
+ * @param {unknown} error
180
+ * @returns {error is NodeJS.ErrnoException}
181
+ */
182
+ function isMissingPathError(error) {
183
+ if (!(error instanceof Error)) {
184
+ return false;
185
+ }
186
+
187
+ return (
188
+ 'code' in error && (error.code === 'ENOENT' || error.code === 'ENOTDIR')
189
+ );
190
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * @import { ParsedCliArguments } from './parse-cli-arguments.types.ts';
3
+ * @import { ResolvedOutputMode } from './output-view.types.ts';
4
+ */
5
+
6
+ /**
7
+ * Resolve the renderer and color support for one command invocation.
8
+ *
9
+ * @param {ParsedCliArguments} parsed_arguments
10
+ * @param {{ is_tty: boolean, no_color: boolean, term: string | undefined }} output_context
11
+ * @returns {ResolvedOutputMode}
12
+ */
13
+ export function resolveOutputMode(parsed_arguments, output_context) {
14
+ if (parsed_arguments.output_mode === 'json') {
15
+ return {
16
+ color_enabled: false,
17
+ renderer_name: 'json',
18
+ };
19
+ }
20
+
21
+ if (parsed_arguments.output_mode === 'plain') {
22
+ return {
23
+ color_enabled: false,
24
+ renderer_name: 'plain',
25
+ };
26
+ }
27
+
28
+ if (!output_context.is_tty) {
29
+ return {
30
+ color_enabled: false,
31
+ renderer_name: 'plain',
32
+ };
33
+ }
34
+
35
+ return {
36
+ color_enabled: isColorEnabled(parsed_arguments, output_context),
37
+ renderer_name: 'rich',
38
+ };
39
+ }
40
+
41
+ /**
42
+ * @param {ParsedCliArguments} parsed_arguments
43
+ * @param {{ is_tty: boolean, no_color: boolean, term: string | undefined }} output_context
44
+ * @returns {boolean}
45
+ */
46
+ function isColorEnabled(parsed_arguments, output_context) {
47
+ if (parsed_arguments.color_mode === 'always') {
48
+ return true;
49
+ }
50
+
51
+ if (parsed_arguments.color_mode === 'never') {
52
+ return false;
53
+ }
54
+
55
+ if (output_context.no_color || output_context.term === 'dumb') {
56
+ return false;
57
+ }
58
+
59
+ return true;
60
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * @import { PatramRepoConfig } from './load-patram-config.types.ts';
3
+ * @import { PatramConfig } from './patram-config.types.ts';
4
+ */
5
+
6
+ import { parsePatramConfig } from './patram-config.js';
7
+
8
+ /**
9
+ * Built-in graph semantics.
10
+ *
11
+ * Merges repo-defined graph config with Patram's built-in document and link
12
+ * semantics before graph materialization.
13
+ *
14
+ * Kind: config
15
+ * Status: active
16
+ * Tracked in: ../docs/plans/v0/source-anchor-dogfooding.md
17
+ * Decided by: ../docs/decisions/graph-materialization.md
18
+ * @patram
19
+ * @see {@link ./load-patram-config.js}
20
+ * @see {@link ../docs/graph-v0.md}
21
+ */
22
+
23
+ const BUILT_IN_PATRAM_CONFIG = {
24
+ kinds: {
25
+ document: {
26
+ builtin: true,
27
+ label: 'Document',
28
+ },
29
+ },
30
+ mappings: {
31
+ 'document.title': {
32
+ node: {
33
+ field: 'title',
34
+ kind: 'document',
35
+ },
36
+ },
37
+ 'document.description': {
38
+ node: {
39
+ field: 'description',
40
+ kind: 'document',
41
+ },
42
+ },
43
+ 'jsdoc.link': {
44
+ emit: {
45
+ relation: 'links_to',
46
+ target: 'path',
47
+ target_kind: 'document',
48
+ },
49
+ },
50
+ 'markdown.link': {
51
+ emit: {
52
+ relation: 'links_to',
53
+ target: 'path',
54
+ target_kind: 'document',
55
+ },
56
+ },
57
+ },
58
+ relations: {
59
+ links_to: {
60
+ builtin: true,
61
+ from: ['document'],
62
+ to: ['document'],
63
+ },
64
+ },
65
+ };
66
+
67
+ /**
68
+ * Merge built-in Patram graph semantics with repo-defined schema.
69
+ *
70
+ * @param {PatramRepoConfig} repo_config
71
+ * @returns {PatramConfig}
72
+ */
73
+ export function resolvePatramGraphConfig(repo_config) {
74
+ return parsePatramConfig({
75
+ kinds: {
76
+ ...BUILT_IN_PATRAM_CONFIG.kinds,
77
+ ...repo_config.kinds,
78
+ },
79
+ mappings: {
80
+ ...BUILT_IN_PATRAM_CONFIG.mappings,
81
+ ...repo_config.mappings,
82
+ },
83
+ relations: {
84
+ ...BUILT_IN_PATRAM_CONFIG.relations,
85
+ ...repo_config.relations,
86
+ },
87
+ });
88
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * @import { PatramRepoConfig } from './load-patram-config.types.ts';
3
+ */
4
+
5
+ /**
6
+ * @typedef {{ kind: 'ad_hoc' } | { kind: 'stored_query', name: string }} QuerySource
7
+ */
8
+
9
+ /**
10
+ * Resolve an ad hoc or stored query into a where clause.
11
+ *
12
+ * @param {PatramRepoConfig} repo_config
13
+ * @param {string[]} command_arguments
14
+ * @returns {{ success: true, value: { query_source: QuerySource, where_clause: string } } | { success: false, message: string }}
15
+ */
16
+ export function resolveWhereClause(repo_config, command_arguments) {
17
+ if (command_arguments[0] === '--where') {
18
+ const where_clause = command_arguments.slice(1).join(' ').trim();
19
+
20
+ if (where_clause.length === 0) {
21
+ return {
22
+ message: 'Query requires a where clause.',
23
+ success: false,
24
+ };
25
+ }
26
+
27
+ return {
28
+ success: true,
29
+ value: {
30
+ query_source: {
31
+ kind: 'ad_hoc',
32
+ },
33
+ where_clause,
34
+ },
35
+ };
36
+ }
37
+
38
+ const stored_query_name = command_arguments[0];
39
+
40
+ if (!stored_query_name) {
41
+ return {
42
+ message: 'Query requires "--where" or a stored query name.',
43
+ success: false,
44
+ };
45
+ }
46
+
47
+ const stored_query = repo_config.queries[stored_query_name];
48
+
49
+ if (!stored_query) {
50
+ return {
51
+ message: `Stored query "${stored_query_name}" was not found.`,
52
+ success: false,
53
+ };
54
+ }
55
+
56
+ return {
57
+ success: true,
58
+ value: {
59
+ query_source: {
60
+ kind: 'stored_query',
61
+ name: stored_query_name,
62
+ },
63
+ where_clause: stored_query.where,
64
+ },
65
+ };
66
+ }
@@ -0,0 +1,311 @@
1
+ /* eslint-disable max-lines */
2
+ /**
3
+ * @import { GraphNode } from './build-graph.types.ts';
4
+ * @import { PatramClaim } from './parse-claims.types.ts';
5
+ * @import { PatramDiagnostic } from './load-patram-config.types.ts';
6
+ */
7
+
8
+ import { readFile } from 'node:fs/promises';
9
+ import { posix, relative, resolve } from 'node:path';
10
+
11
+ import { parseSourceFile } from './parse-claims.js';
12
+
13
+ /**
14
+ * Show command document rendering.
15
+ *
16
+ * Loads one source file, resolves indexed links, and builds the shared show
17
+ * output model.
18
+ *
19
+ * Kind: output
20
+ * Status: active
21
+ * Tracked in: ../docs/plans/v0/source-anchor-dogfooding.md
22
+ * Decided by: ../docs/decisions/show-output.md
23
+ * Decided by: ../docs/decisions/source-rendering.md
24
+ * @patram
25
+ * @see {@link ./render-output-view.js}
26
+ * @see {@link ../docs/decisions/show-output.md}
27
+ */
28
+
29
+ /**
30
+ * @param {string} requested_file_path
31
+ * @param {string} project_directory
32
+ * @param {import('./build-graph.types.ts').BuildGraphResult} graph
33
+ * @returns {Promise<
34
+ * | {
35
+ * success: true;
36
+ * value: {
37
+ * path: string;
38
+ * rendered_source: string;
39
+ * resolved_links: Array<{
40
+ * label: string;
41
+ * reference: number;
42
+ * target: { kind?: string, path: string, status?: string, title: string };
43
+ * }>;
44
+ * source: string;
45
+ * };
46
+ * }
47
+ * | {
48
+ * diagnostic: PatramDiagnostic;
49
+ * success: false;
50
+ * }
51
+ * >}
52
+ */
53
+ export async function loadShowOutput(
54
+ requested_file_path,
55
+ project_directory,
56
+ graph,
57
+ ) {
58
+ const absolute_source_path = resolve(project_directory, requested_file_path);
59
+ const source_file_path = normalizeRepoRelativePath(
60
+ relative(project_directory, absolute_source_path),
61
+ );
62
+ let source_text;
63
+
64
+ try {
65
+ source_text = await readFile(absolute_source_path, 'utf8');
66
+ } catch (error) {
67
+ if (isFileNotFoundError(error)) {
68
+ return {
69
+ diagnostic: createShowFileNotFoundDiagnostic(source_file_path),
70
+ success: false,
71
+ };
72
+ }
73
+
74
+ throw error;
75
+ }
76
+
77
+ const parse_result = parseSourceFile({
78
+ path: source_file_path,
79
+ source: source_text,
80
+ });
81
+
82
+ if (parse_result.diagnostics.length > 0) {
83
+ return {
84
+ diagnostic: parse_result.diagnostics[0],
85
+ success: false,
86
+ };
87
+ }
88
+
89
+ return {
90
+ success: true,
91
+ value: createShowOutput(
92
+ source_file_path,
93
+ source_text,
94
+ parse_result.claims,
95
+ graph.nodes,
96
+ ),
97
+ };
98
+ }
99
+
100
+ /**
101
+ * @param {string} source_file_path
102
+ * @param {string} source_text
103
+ * @param {PatramClaim[]} claims
104
+ * @param {Record<string, GraphNode>} graph_nodes
105
+ * @returns {{ path: string, rendered_source: string, resolved_links: Array<{ label: string, reference: number, target: { kind?: string, path: string, status?: string, title: string } }>, source: string }}
106
+ */
107
+ function createShowOutput(source_file_path, source_text, claims, graph_nodes) {
108
+ const link_claims = claims.filter(isResolvedLinkClaim);
109
+ const rendered_link_claims = link_claims.filter(isMarkdownLinkClaim);
110
+ const resolved_links = link_claims.map((claim, claim_index) =>
111
+ createResolvedLinkSummary(
112
+ source_file_path,
113
+ claim,
114
+ claim_index + 1,
115
+ graph_nodes,
116
+ ),
117
+ );
118
+
119
+ return {
120
+ path: source_file_path,
121
+ rendered_source: renderResolvedSource(
122
+ source_text,
123
+ rendered_link_claims,
124
+ resolved_links,
125
+ ),
126
+ resolved_links,
127
+ source: source_text,
128
+ };
129
+ }
130
+
131
+ /**
132
+ * @param {string} source_text
133
+ * @param {PatramClaim[]} link_claims
134
+ * @param {Array<{ label: string, reference: number }>} resolved_links
135
+ * @returns {string}
136
+ */
137
+ function renderResolvedSource(source_text, link_claims, resolved_links) {
138
+ const source_lines = source_text.split('\n');
139
+
140
+ return trimTrailingLineBreaks(
141
+ source_lines
142
+ .map((source_line, line_index) =>
143
+ renderResolvedSourceLine(
144
+ source_line,
145
+ line_index + 1,
146
+ link_claims,
147
+ resolved_links,
148
+ ),
149
+ )
150
+ .join('\n'),
151
+ );
152
+ }
153
+
154
+ /**
155
+ * @param {string} source_line
156
+ * @param {number} line_number
157
+ * @param {PatramClaim[]} link_claims
158
+ * @param {Array<{ label: string, reference: number }>} resolved_links
159
+ * @returns {string}
160
+ */
161
+ function renderResolvedSourceLine(
162
+ source_line,
163
+ line_number,
164
+ link_claims,
165
+ resolved_links,
166
+ ) {
167
+ const line_link_claims = link_claims.filter(
168
+ (claim) => claim.origin.line === line_number,
169
+ );
170
+
171
+ if (line_link_claims.length === 0) {
172
+ return source_line;
173
+ }
174
+
175
+ /** @type {string[]} */
176
+ const chunks = [];
177
+ let source_offset = 0;
178
+
179
+ for (const claim of line_link_claims) {
180
+ const claim_index = link_claims.indexOf(claim);
181
+ const claim_value = getLinkClaimValue(claim);
182
+ const rendered_link = resolved_links[claim_index];
183
+ const claim_column = claim.origin.column - 1;
184
+ const raw_link = `[${claim_value.text}](${claim_value.target})`;
185
+
186
+ chunks.push(source_line.slice(source_offset, claim_column));
187
+ chunks.push(`[${rendered_link.label}][${rendered_link.reference}]`);
188
+ source_offset = claim_column + raw_link.length;
189
+ }
190
+
191
+ chunks.push(source_line.slice(source_offset));
192
+
193
+ return chunks.join('');
194
+ }
195
+
196
+ /**
197
+ * @param {string} source_file_path
198
+ * @param {PatramClaim} claim
199
+ * @param {number} reference
200
+ * @param {Record<string, GraphNode>} graph_nodes
201
+ * @returns {{ label: string, reference: number, target: { kind?: string, path: string, status?: string, title: string } }}
202
+ */
203
+ function createResolvedLinkSummary(
204
+ source_file_path,
205
+ claim,
206
+ reference,
207
+ graph_nodes,
208
+ ) {
209
+ const claim_value = getLinkClaimValue(claim);
210
+ const target_path = resolveShowTargetPath(
211
+ source_file_path,
212
+ claim_value.target,
213
+ );
214
+ const target_node = graph_nodes[`doc:${target_path}`];
215
+ const target_title = target_node?.title ?? claim_value.text;
216
+
217
+ return {
218
+ label: claim_value.text,
219
+ reference,
220
+ target: {
221
+ kind: target_node?.kind,
222
+ path: target_node?.path ?? target_path,
223
+ status: target_node?.status,
224
+ title: target_title,
225
+ },
226
+ };
227
+ }
228
+
229
+ /**
230
+ * @param {string} source_file_path
231
+ * @param {string} raw_target
232
+ * @returns {string}
233
+ */
234
+ function resolveShowTargetPath(source_file_path, raw_target) {
235
+ const source_directory = posix.dirname(
236
+ normalizeRepoRelativePath(source_file_path),
237
+ );
238
+
239
+ return normalizeRepoRelativePath(posix.join(source_directory, raw_target));
240
+ }
241
+
242
+ /**
243
+ * @param {PatramClaim} claim
244
+ * @returns {claim is PatramClaim & { type: 'markdown.link', value: { target: string, text: string } }}
245
+ */
246
+ function isMarkdownLinkClaim(claim) {
247
+ return claim.type === 'markdown.link';
248
+ }
249
+
250
+ /**
251
+ * @param {PatramClaim} claim
252
+ * @returns {claim is PatramClaim & { type: 'jsdoc.link' | 'markdown.link', value: { target: string, text: string } }}
253
+ */
254
+ function isResolvedLinkClaim(claim) {
255
+ return claim.type === 'markdown.link' || claim.type === 'jsdoc.link';
256
+ }
257
+
258
+ /**
259
+ * @param {PatramClaim} claim
260
+ * @returns {{ target: string, text: string }}
261
+ */
262
+ function getLinkClaimValue(claim) {
263
+ if (typeof claim.value === 'string') {
264
+ throw new Error(`Expected claim "${claim.id}" to carry a markdown link.`);
265
+ }
266
+
267
+ return claim.value;
268
+ }
269
+
270
+ /**
271
+ * @param {string} source_path
272
+ * @returns {string}
273
+ */
274
+ function normalizeRepoRelativePath(source_path) {
275
+ return posix.normalize(source_path.replaceAll('\\', '/'));
276
+ }
277
+
278
+ /**
279
+ * @param {string} source_file_path
280
+ * @returns {PatramDiagnostic}
281
+ */
282
+ function createShowFileNotFoundDiagnostic(source_file_path) {
283
+ return {
284
+ code: 'show.file_not_found',
285
+ column: 1,
286
+ level: 'error',
287
+ line: 1,
288
+ message: `File "${source_file_path}" was not found.`,
289
+ path: source_file_path,
290
+ };
291
+ }
292
+
293
+ /**
294
+ * @param {string} value
295
+ * @returns {string}
296
+ */
297
+ function trimTrailingLineBreaks(value) {
298
+ return value.replace(/\n+$/du, '');
299
+ }
300
+
301
+ /**
302
+ * @param {unknown} error
303
+ * @returns {error is NodeJS.ErrnoException}
304
+ */
305
+ function isFileNotFoundError(error) {
306
+ if (!(error instanceof Error)) {
307
+ return false;
308
+ }
309
+
310
+ return 'code' in error && error.code === 'ENOENT';
311
+ }