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.
Files changed (51) hide show
  1. package/bin/patram.js +25 -147
  2. package/lib/build-graph-identity.js +238 -0
  3. package/lib/build-graph.js +143 -77
  4. package/lib/check-graph.js +23 -7
  5. package/lib/claim-helpers.js +55 -0
  6. package/lib/command-output.js +83 -0
  7. package/lib/layout-stored-queries.js +213 -0
  8. package/lib/list-queries.js +18 -0
  9. package/lib/list-source-files.js +50 -15
  10. package/lib/load-patram-config.js +106 -18
  11. package/lib/load-patram-config.types.ts +9 -0
  12. package/lib/load-project-graph.js +124 -0
  13. package/lib/output-view.types.ts +73 -0
  14. package/lib/parse-claims.js +38 -158
  15. package/lib/parse-claims.types.ts +7 -0
  16. package/lib/parse-cli-arguments-helpers.js +273 -0
  17. package/lib/parse-cli-arguments.js +114 -0
  18. package/lib/parse-cli-arguments.types.ts +24 -0
  19. package/lib/parse-cli-color-options.js +44 -0
  20. package/lib/parse-cli-query-pagination.js +49 -0
  21. package/lib/parse-jsdoc-blocks.js +184 -0
  22. package/lib/parse-jsdoc-claims.js +280 -0
  23. package/lib/parse-jsdoc-prose.js +111 -0
  24. package/lib/parse-markdown-claims.js +242 -0
  25. package/lib/parse-markdown-directives.js +136 -0
  26. package/lib/parse-where-clause.js +312 -0
  27. package/lib/patram-cli.js +337 -0
  28. package/lib/patram-config.js +3 -1
  29. package/lib/patram-config.types.ts +2 -1
  30. package/lib/query-graph.js +256 -0
  31. package/lib/render-check-output.js +315 -0
  32. package/lib/render-json-output.js +108 -0
  33. package/lib/render-output-view.js +193 -0
  34. package/lib/render-plain-output.js +237 -0
  35. package/lib/render-rich-output.js +293 -0
  36. package/lib/render-rich-source.js +1333 -0
  37. package/lib/resolve-check-target.js +190 -0
  38. package/lib/resolve-output-mode.js +60 -0
  39. package/lib/resolve-patram-graph-config.js +88 -0
  40. package/lib/resolve-where-clause.js +51 -0
  41. package/lib/show-document.js +311 -0
  42. package/lib/source-file-defaults.js +28 -0
  43. package/lib/write-paged-output.js +87 -0
  44. package/package.json +21 -10
  45. package/bin/patram.test.js +0 -184
  46. package/lib/build-graph.test.js +0 -141
  47. package/lib/check-graph.test.js +0 -103
  48. package/lib/list-source-files.test.js +0 -101
  49. package/lib/load-patram-config.test.js +0 -211
  50. package/lib/parse-claims.test.js +0 -113
  51. package/lib/patram-config.test.js +0 -147
@@ -0,0 +1,315 @@
1
+ /**
2
+ * @import { PatramDiagnostic } from './load-patram-config.types.ts';
3
+ * @import { ResolvedOutputMode } from './output-view.types.ts';
4
+ */
5
+
6
+ import { Ansis } from 'ansis';
7
+ import stringWidth from 'string-width';
8
+
9
+ /**
10
+ * Render check diagnostics for one output mode.
11
+ *
12
+ * @param {PatramDiagnostic[]} diagnostics
13
+ * @param {ResolvedOutputMode} output_mode
14
+ * @returns {string}
15
+ */
16
+ export function renderCheckDiagnostics(diagnostics, output_mode) {
17
+ if (output_mode.renderer_name === 'json') {
18
+ return `${JSON.stringify(
19
+ {
20
+ diagnostics: diagnostics.map(formatJsonDiagnostic),
21
+ },
22
+ null,
23
+ 2,
24
+ )}\n`;
25
+ }
26
+
27
+ if (output_mode.renderer_name === 'plain') {
28
+ return renderPlainCheckDiagnostics(diagnostics);
29
+ }
30
+
31
+ return renderRichCheckDiagnostics(diagnostics, createAnsi(output_mode));
32
+ }
33
+
34
+ /**
35
+ * Render the success summary for one check run.
36
+ *
37
+ * @param {number} source_file_count
38
+ * @param {ResolvedOutputMode} output_mode
39
+ * @returns {string}
40
+ */
41
+ export function renderCheckSuccess(source_file_count, output_mode) {
42
+ if (output_mode.renderer_name === 'json') {
43
+ return `${JSON.stringify({ diagnostics: [] }, null, 2)}\n`;
44
+ }
45
+
46
+ if (output_mode.renderer_name === 'plain') {
47
+ return formatCheckSuccess(source_file_count);
48
+ }
49
+
50
+ const ansi = createAnsi(output_mode);
51
+ return `${ansi.green('Check passed.')}\n${ansi.gray(`Scanned ${source_file_count} ${pluralize('file', source_file_count)}. Found 0 errors.`)}\n`;
52
+ }
53
+
54
+ /**
55
+ * @param {PatramDiagnostic[]} diagnostics
56
+ * @returns {string}
57
+ */
58
+ function renderPlainCheckDiagnostics(diagnostics) {
59
+ const grouped_diagnostics = groupDiagnosticsByPath(diagnostics);
60
+ const diagnostic_prefix_width = measureMaxDiagnosticPrefixWidth(diagnostics);
61
+ return `${grouped_diagnostics
62
+ .map((diagnostic_group) =>
63
+ formatPlainDiagnosticGroup(diagnostic_group, diagnostic_prefix_width),
64
+ )
65
+ .join('\n\n')}\n\n${formatPlainSummary(diagnostics)}\n`;
66
+ }
67
+
68
+ /**
69
+ * @param {PatramDiagnostic[]} diagnostics
70
+ * @param {Ansis} ansi
71
+ * @returns {string}
72
+ */
73
+ function renderRichCheckDiagnostics(diagnostics, ansi) {
74
+ const grouped_diagnostics = groupDiagnosticsByPath(diagnostics);
75
+ const diagnostic_prefix_width = measureMaxDiagnosticPrefixWidth(diagnostics);
76
+ return `${grouped_diagnostics
77
+ .map((diagnostic_group) =>
78
+ formatRichDiagnosticGroup(
79
+ diagnostic_group,
80
+ diagnostic_prefix_width,
81
+ ansi,
82
+ ),
83
+ )
84
+ .join('\n\n')}\n\n${formatRichSummary(diagnostics, ansi)}\n`;
85
+ }
86
+
87
+ /**
88
+ * @param {PatramDiagnostic[]} diagnostics
89
+ * @returns {Array<{ diagnostics: PatramDiagnostic[], path: string }>}
90
+ */
91
+ function groupDiagnosticsByPath(diagnostics) {
92
+ /** @type {Array<{ diagnostics: PatramDiagnostic[], path: string }>} */
93
+ const grouped_diagnostics = [];
94
+ /** @type {Map<string, { diagnostics: PatramDiagnostic[], path: string }>} */
95
+ const grouped_diagnostics_by_path = new Map();
96
+
97
+ for (const diagnostic of diagnostics) {
98
+ let diagnostic_group = grouped_diagnostics_by_path.get(diagnostic.path);
99
+
100
+ if (!diagnostic_group) {
101
+ diagnostic_group = {
102
+ diagnostics: [],
103
+ path: diagnostic.path,
104
+ };
105
+ grouped_diagnostics_by_path.set(diagnostic.path, diagnostic_group);
106
+ grouped_diagnostics.push(diagnostic_group);
107
+ }
108
+
109
+ diagnostic_group.diagnostics.push(diagnostic);
110
+ }
111
+
112
+ return grouped_diagnostics;
113
+ }
114
+
115
+ /**
116
+ * @param {{ diagnostics: PatramDiagnostic[], path: string }} diagnostic_group
117
+ * @param {number} diagnostic_prefix_width
118
+ * @returns {string}
119
+ */
120
+ function formatPlainDiagnosticGroup(diagnostic_group, diagnostic_prefix_width) {
121
+ return `${formatDiagnosticGroupHeader(diagnostic_group.path)}\n${diagnostic_group.diagnostics
122
+ .map((diagnostic) =>
123
+ formatPlainDiagnosticRow(diagnostic, diagnostic_prefix_width),
124
+ )
125
+ .join('\n')}`;
126
+ }
127
+
128
+ /**
129
+ * @param {PatramDiagnostic} diagnostic
130
+ * @param {number} diagnostic_prefix_width
131
+ * @returns {string}
132
+ */
133
+ function formatPlainDiagnosticRow(diagnostic, diagnostic_prefix_width) {
134
+ const diagnostic_prefix = formatDiagnosticPrefix(diagnostic);
135
+ return `${diagnostic_prefix}${createDiagnosticCodePadding(diagnostic_prefix, diagnostic_prefix_width)} ${diagnostic.code}`;
136
+ }
137
+
138
+ /**
139
+ * @param {PatramDiagnostic[]} diagnostics
140
+ * @returns {string}
141
+ */
142
+ function formatPlainSummary(diagnostics) {
143
+ const error_count = countDiagnosticsByLevel(diagnostics, 'error');
144
+ const warning_count = countDiagnosticsByLevel(diagnostics, 'warning');
145
+ const problem_count = diagnostics.length;
146
+
147
+ return `\u2716 ${problem_count} ${pluralize('problem', problem_count)} (${error_count} ${pluralize('error', error_count)}, ${warning_count} ${pluralize('warning', warning_count)})`;
148
+ }
149
+
150
+ /**
151
+ * @param {{ diagnostics: PatramDiagnostic[], path: string }} diagnostic_group
152
+ * @param {number} diagnostic_prefix_width
153
+ * @param {Ansis} ansi
154
+ * @returns {string}
155
+ */
156
+ function formatRichDiagnosticGroup(
157
+ diagnostic_group,
158
+ diagnostic_prefix_width,
159
+ ansi,
160
+ ) {
161
+ return `${ansi.green(formatDiagnosticGroupHeader(diagnostic_group.path))}\n${diagnostic_group.diagnostics
162
+ .map((diagnostic) =>
163
+ formatRichDiagnosticRow(diagnostic, diagnostic_prefix_width, ansi),
164
+ )
165
+ .join('\n')}`;
166
+ }
167
+
168
+ /**
169
+ * @param {PatramDiagnostic} diagnostic
170
+ * @param {number} diagnostic_prefix_width
171
+ * @param {Ansis} ansi
172
+ * @returns {string}
173
+ */
174
+ function formatRichDiagnosticRow(diagnostic, diagnostic_prefix_width, ansi) {
175
+ const location = `${diagnostic.line}:${diagnostic.column}`;
176
+ const diagnostic_prefix = formatDiagnosticPrefix(diagnostic);
177
+ return ` ${ansi.gray(location)} ${formatRichDiagnosticLevel(diagnostic.level, ansi)} ${diagnostic.message}${createDiagnosticCodePadding(diagnostic_prefix, diagnostic_prefix_width)} ${ansi.gray(diagnostic.code)}`;
178
+ }
179
+
180
+ /**
181
+ * @param {PatramDiagnostic[]} diagnostics
182
+ * @param {Ansis} ansi
183
+ * @returns {string}
184
+ */
185
+ function formatRichSummary(diagnostics, ansi) {
186
+ return ansi.red(formatPlainSummary(diagnostics));
187
+ }
188
+
189
+ /**
190
+ * @param {PatramDiagnostic} diagnostic
191
+ * @returns {{ code: string, column: number, level: 'error', line: number, message: string, path: string }}
192
+ */
193
+ function formatJsonDiagnostic(diagnostic) {
194
+ return {
195
+ path: diagnostic.path,
196
+ line: diagnostic.line,
197
+ column: diagnostic.column,
198
+ level: diagnostic.level,
199
+ code: diagnostic.code,
200
+ message: diagnostic.message,
201
+ };
202
+ }
203
+
204
+ /**
205
+ * @param {number} source_file_count
206
+ * @returns {string}
207
+ */
208
+ function formatCheckSuccess(source_file_count) {
209
+ return `Check passed.\nScanned ${source_file_count} ${pluralize('file', source_file_count)}. Found 0 errors.\n`;
210
+ }
211
+
212
+ /**
213
+ * @param {PatramDiagnostic} diagnostic
214
+ */
215
+ function formatDiagnosticPrefix(diagnostic) {
216
+ return ` ${diagnostic.line}:${diagnostic.column} ${diagnostic.level} ${diagnostic.message}`;
217
+ }
218
+
219
+ /**
220
+ * @param {PatramDiagnostic[]} diagnostics
221
+ */
222
+ function measureMaxDiagnosticPrefixWidth(diagnostics) {
223
+ let max_width = 0;
224
+
225
+ for (const diagnostic of diagnostics) {
226
+ const diagnostic_prefix_width = stringWidth(
227
+ formatDiagnosticPrefix(diagnostic),
228
+ );
229
+
230
+ if (diagnostic_prefix_width > max_width) {
231
+ max_width = diagnostic_prefix_width;
232
+ }
233
+ }
234
+
235
+ return max_width;
236
+ }
237
+
238
+ /**
239
+ * @param {string} diagnostic_prefix
240
+ * @param {number} diagnostic_prefix_width
241
+ */
242
+ function createDiagnosticCodePadding(
243
+ diagnostic_prefix,
244
+ diagnostic_prefix_width,
245
+ ) {
246
+ return ' '.repeat(
247
+ Math.max(0, diagnostic_prefix_width - stringWidth(diagnostic_prefix)),
248
+ );
249
+ }
250
+
251
+ /**
252
+ * @param {PatramDiagnostic[]} diagnostics
253
+ * @param {'error' | 'warning'} level
254
+ */
255
+ function countDiagnosticsByLevel(diagnostics, level) {
256
+ let count = 0;
257
+
258
+ for (const diagnostic of diagnostics) {
259
+ if (diagnostic.level === level) {
260
+ count += 1;
261
+ }
262
+ }
263
+
264
+ return count;
265
+ }
266
+
267
+ /**
268
+ * @param {'error' | 'warning'} level
269
+ * @param {Ansis} ansi
270
+ * @returns {string}
271
+ */
272
+ function formatRichDiagnosticLevel(level, ansi) {
273
+ if (level === 'warning') {
274
+ return ansi.yellow(level);
275
+ }
276
+
277
+ return ansi.red(level);
278
+ }
279
+
280
+ /**
281
+ * @param {string} noun
282
+ * @param {number} count
283
+ */
284
+ function pluralize(noun, count) {
285
+ if (count === 1) {
286
+ return noun;
287
+ }
288
+
289
+ return `${noun}s`;
290
+ }
291
+
292
+ /**
293
+ * @param {ResolvedOutputMode} output_mode
294
+ */
295
+ function createAnsi(output_mode) {
296
+ return new Ansis(output_mode.color_enabled ? 3 : 0);
297
+ }
298
+
299
+ /**
300
+ * @param {string} path
301
+ */
302
+ function formatDiagnosticGroupHeader(path) {
303
+ return `${resolveDiagnosticPathType(path)} ${path}`;
304
+ }
305
+
306
+ /**
307
+ * @param {string} path
308
+ */
309
+ function resolveDiagnosticPathType(path) {
310
+ if (path.endsWith('.json')) {
311
+ return 'file';
312
+ }
313
+
314
+ return 'document';
315
+ }
@@ -0,0 +1,108 @@
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
+ source: output_view.source,
43
+ resolved_links: output_view.items.map(formatJsonResolvedLink),
44
+ },
45
+ null,
46
+ 2,
47
+ )}\n`;
48
+ }
49
+
50
+ throw new Error('Unsupported output view command.');
51
+ }
52
+
53
+ /**
54
+ * @param {OutputNodeItem} output_item
55
+ * @returns {{ id: string, kind: string, title: string, path: string, status?: string }}
56
+ */
57
+ function formatJsonQueryItem(output_item) {
58
+ /** @type {{ id: string, kind: string, title: string, path: string, status?: string }} */
59
+ const query_item = {
60
+ id: output_item.id,
61
+ kind: output_item.node_kind,
62
+ title: output_item.title,
63
+ path: output_item.path,
64
+ };
65
+
66
+ if (output_item.status) {
67
+ query_item.status = output_item.status;
68
+ }
69
+
70
+ return query_item;
71
+ }
72
+
73
+ /**
74
+ * @param {OutputStoredQueryItem} output_item
75
+ * @returns {{ name: string, where: string }}
76
+ */
77
+ function formatJsonStoredQuery(output_item) {
78
+ return {
79
+ name: output_item.name,
80
+ where: output_item.where,
81
+ };
82
+ }
83
+
84
+ /**
85
+ * @param {OutputResolvedLinkItem} output_item
86
+ * @returns {{ label: string, reference: number, target: { kind?: string, path: string, status?: string, title: string } }}
87
+ */
88
+ function formatJsonResolvedLink(output_item) {
89
+ /** @type {{ label: string, reference: number, target: { kind?: string, path: string, status?: string, title: string } }} */
90
+ const resolved_link = {
91
+ reference: output_item.reference,
92
+ label: output_item.label,
93
+ target: {
94
+ title: output_item.target.title,
95
+ path: output_item.target.path,
96
+ },
97
+ };
98
+
99
+ if (output_item.target.kind) {
100
+ resolved_link.target.kind = output_item.target.kind;
101
+ }
102
+
103
+ if (output_item.target.status) {
104
+ resolved_link.target.status = output_item.target.status;
105
+ }
106
+
107
+ return resolved_link;
108
+ }
@@ -0,0 +1,193 @@
1
+ /** @import * as $k$$l$output$j$view$k$types$k$ts from './output-view.types.ts'; */
2
+ /**
3
+ * @import { GraphNode } from './build-graph.types.ts';
4
+ * @import { ParsedCliArguments } from './parse-cli-arguments.types.ts';
5
+ * @import { OutputStoredQueryItem, OutputView, ResolvedOutputMode, ShowOutputView } from './output-view.types.ts';
6
+ */
7
+
8
+ import { renderJsonOutput } from './render-json-output.js';
9
+ import { renderPlainOutput } from './render-plain-output.js';
10
+ import { renderRichOutput } from './render-rich-output.js';
11
+
12
+ /**
13
+ * Shared command output views.
14
+ *
15
+ * Normalizes `query`, `queries`, and `show` results into renderer-specific
16
+ * output models.
17
+ *
18
+ * Kind: output
19
+ * Status: active
20
+ * Tracked in: ../docs/plans/v0/source-anchor-dogfooding.md
21
+ * Decided by: ../docs/decisions/cli-output-architecture.md
22
+ * @patram
23
+ * @see {@link ./show-document.js}
24
+ * @see {@link ../docs/decisions/cli-output-architecture.md}
25
+ */
26
+
27
+ /**
28
+ * Create a shared output view from one command result.
29
+ *
30
+ * @param {'query' | 'queries'} command_name
31
+ * @param {GraphNode[] | { name: string, where: string }[]} command_items
32
+ * @param {{ hints?: string[], limit?: number, offset?: number, total_count?: number }=} command_options
33
+ * @returns {OutputView}
34
+ */
35
+ export function createOutputView(command_name, command_items, command_options) {
36
+ if (command_name === 'query') {
37
+ return createQueryOutputView(
38
+ /** @type {GraphNode[]} */ (command_items),
39
+ command_options,
40
+ );
41
+ }
42
+
43
+ if (command_name === 'queries') {
44
+ return createStoredQueriesOutputView(
45
+ /** @type {OutputStoredQueryItem[]} */ (command_items),
46
+ );
47
+ }
48
+
49
+ throw new Error(`Unsupported output view command "${command_name}".`);
50
+ }
51
+
52
+ /**
53
+ * Create a shared output view for the show command.
54
+ *
55
+ * @param {{ path: string, rendered_source: string, resolved_links: Array<{ label: string, reference: number, target: { kind?: string, path: string, status?: string, title: string } }>, source: string }} show_output
56
+ * @returns {ShowOutputView}
57
+ */
58
+ export function createShowOutputView(show_output) {
59
+ return {
60
+ command: 'show',
61
+ hints: [],
62
+ items: show_output.resolved_links.map((resolved_link) => ({
63
+ kind: 'resolved_link',
64
+ label: resolved_link.label,
65
+ reference: resolved_link.reference,
66
+ target: createResolvedLinkTarget(resolved_link.target),
67
+ })),
68
+ path: show_output.path,
69
+ rendered_source: show_output.rendered_source,
70
+ source: show_output.source,
71
+ summary: {
72
+ count: show_output.resolved_links.length,
73
+ kind: 'resolved_link_list',
74
+ },
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Render one shared output view through the resolved renderer.
80
+ *
81
+ * @param {OutputView} output_view
82
+ * @param {ResolvedOutputMode} output_mode
83
+ * @param {ParsedCliArguments} parsed_arguments
84
+ * @returns {Promise<string>}
85
+ */
86
+ export async function renderOutputView(
87
+ output_view,
88
+ output_mode,
89
+ parsed_arguments,
90
+ ) {
91
+ if (output_mode.renderer_name === 'json') {
92
+ return renderJsonOutput(output_view);
93
+ }
94
+
95
+ if (output_mode.renderer_name === 'plain') {
96
+ return renderPlainOutput(output_view);
97
+ }
98
+
99
+ return renderRichOutput(output_view, {
100
+ color_enabled: output_mode.color_enabled,
101
+ color_mode: parsed_arguments.color_mode,
102
+ });
103
+ }
104
+
105
+ /**
106
+ * @param {GraphNode[]} graph_nodes
107
+ * @param {{ hints?: string[], limit?: number, offset?: number, total_count?: number }=} command_options
108
+ * @returns {OutputView}
109
+ */
110
+ function createQueryOutputView(graph_nodes, command_options = {}) {
111
+ const total_count = command_options.total_count ?? graph_nodes.length;
112
+
113
+ return {
114
+ command: 'query',
115
+ hints:
116
+ command_options.hints ??
117
+ (total_count === 0 ? ['Try: patram query --where "kind=task"'] : []),
118
+ items: graph_nodes.map(createOutputNodeItem),
119
+ summary: {
120
+ count: graph_nodes.length,
121
+ kind: 'result_list',
122
+ limit: command_options.limit ?? graph_nodes.length,
123
+ offset: command_options.offset ?? 0,
124
+ total_count,
125
+ },
126
+ };
127
+ }
128
+
129
+ /**
130
+ * @param {{ name: string, where: string }[]} stored_queries
131
+ * @returns {OutputView}
132
+ */
133
+ function createStoredQueriesOutputView(stored_queries) {
134
+ return {
135
+ command: 'queries',
136
+ hints: [],
137
+ items: stored_queries.map((stored_query) => ({
138
+ kind: 'stored_query',
139
+ name: stored_query.name,
140
+ where: stored_query.where,
141
+ })),
142
+ summary: {
143
+ count: stored_queries.length,
144
+ kind: 'stored_query_list',
145
+ },
146
+ };
147
+ }
148
+
149
+ /**
150
+ * @param {GraphNode} graph_node
151
+ * @returns {$k$$l$output$j$view$k$types$k$ts.OutputNodeItem}
152
+ */
153
+ function createOutputNodeItem(graph_node) {
154
+ const title =
155
+ graph_node.title ?? graph_node.label ?? graph_node.path ?? graph_node.key;
156
+
157
+ if (!title || !graph_node.path) {
158
+ throw new Error(
159
+ `Expected graph node "${graph_node.id}" to have a title and path.`,
160
+ );
161
+ }
162
+
163
+ return {
164
+ id: graph_node.id,
165
+ kind: 'node',
166
+ node_kind: graph_node.kind,
167
+ path: graph_node.path,
168
+ status: graph_node.status,
169
+ title,
170
+ };
171
+ }
172
+
173
+ /**
174
+ * @param {{ kind?: string, path: string, status?: string, title: string }} target
175
+ * @returns {$k$$l$output$j$view$k$types$k$ts.OutputResolvedLinkTarget}
176
+ */
177
+ function createResolvedLinkTarget(target) {
178
+ /** @type {$k$$l$output$j$view$k$types$k$ts.OutputResolvedLinkTarget} */
179
+ const resolved_target = {
180
+ path: target.path,
181
+ title: target.title,
182
+ };
183
+
184
+ if (target.kind && target.kind !== 'document') {
185
+ resolved_target.kind = target.kind;
186
+ }
187
+
188
+ if (target.status) {
189
+ resolved_target.status = target.status;
190
+ }
191
+
192
+ return resolved_target;
193
+ }