patram 0.2.0 → 0.4.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 (38) hide show
  1. package/lib/build-graph-identity.js +86 -99
  2. package/lib/build-graph.js +536 -31
  3. package/lib/build-graph.types.ts +6 -2
  4. package/lib/check-directive-metadata.js +534 -0
  5. package/lib/check-directive-value.js +291 -0
  6. package/lib/check-graph.js +23 -5
  7. package/lib/cli-help-metadata.js +56 -16
  8. package/lib/command-output.js +16 -1
  9. package/lib/derived-summary.js +10 -8
  10. package/lib/directive-diagnostics.js +38 -0
  11. package/lib/directive-type-rules.js +133 -0
  12. package/lib/discover-fields.js +435 -0
  13. package/lib/discover-fields.types.ts +52 -0
  14. package/lib/document-node-identity.js +317 -0
  15. package/lib/format-node-header.js +9 -7
  16. package/lib/format-output-metadata.js +15 -23
  17. package/lib/layout-stored-queries.js +124 -85
  18. package/lib/load-patram-config.js +433 -96
  19. package/lib/load-patram-config.types.ts +98 -3
  20. package/lib/load-project-graph.js +4 -1
  21. package/lib/output-view.types.ts +14 -6
  22. package/lib/parse-cli-arguments.types.ts +1 -1
  23. package/lib/parse-where-clause.js +344 -107
  24. package/lib/parse-where-clause.types.ts +25 -8
  25. package/lib/patram-cli.js +68 -4
  26. package/lib/patram-config.js +31 -31
  27. package/lib/patram-config.types.ts +10 -4
  28. package/lib/query-graph.js +269 -40
  29. package/lib/query-inspection.js +440 -60
  30. package/lib/render-field-discovery.js +184 -0
  31. package/lib/render-json-output.js +21 -22
  32. package/lib/render-output-view.js +301 -34
  33. package/lib/render-plain-output.js +1 -1
  34. package/lib/render-rich-output.js +1 -1
  35. package/lib/render-rich-source.js +245 -14
  36. package/lib/resolve-patram-graph-config.js +15 -9
  37. package/lib/show-document.js +66 -9
  38. package/package.json +5 -5
@@ -0,0 +1,184 @@
1
+ /**
2
+ * @import { ResolvedOutputMode } from './output-view.types.ts';
3
+ * @import { FieldDiscoveryResult, FieldDiscoverySuggestion } from './discover-fields.types.ts';
4
+ */
5
+
6
+ import { Ansis } from 'ansis';
7
+
8
+ const MAX_TEXT_EVIDENCE_ROWS = 5;
9
+
10
+ /**
11
+ * Render field discovery output.
12
+ *
13
+ * @param {FieldDiscoveryResult} discovery_result
14
+ * @param {ResolvedOutputMode} output_mode
15
+ * @returns {string}
16
+ */
17
+ export function renderFieldDiscovery(discovery_result, output_mode) {
18
+ if (output_mode.renderer_name === 'json') {
19
+ return `${JSON.stringify(formatJsonFieldDiscovery(discovery_result), null, 2)}\n`;
20
+ }
21
+
22
+ return renderTextFieldDiscovery(discovery_result, output_mode);
23
+ }
24
+
25
+ /**
26
+ * @param {FieldDiscoveryResult} discovery_result
27
+ * @returns {{ fields: Array<Record<string, unknown>>, summary: FieldDiscoveryResult['summary'] }}
28
+ */
29
+ function formatJsonFieldDiscovery(discovery_result) {
30
+ return {
31
+ fields: discovery_result.fields.map(formatJsonFieldSuggestion),
32
+ summary: discovery_result.summary,
33
+ };
34
+ }
35
+
36
+ /**
37
+ * @param {FieldDiscoverySuggestion} field_suggestion
38
+ * @returns {Record<string, unknown>}
39
+ */
40
+ function formatJsonFieldSuggestion(field_suggestion) {
41
+ return {
42
+ confidence: field_suggestion.confidence,
43
+ conflicting_evidence: field_suggestion.conflicting_evidence,
44
+ evidence_references: field_suggestion.evidence_references,
45
+ likely_class_usage: field_suggestion.likely_class_usage,
46
+ likely_multiplicity: field_suggestion.likely_multiplicity,
47
+ likely_type: field_suggestion.likely_type,
48
+ name: field_suggestion.name,
49
+ };
50
+ }
51
+
52
+ /**
53
+ * @param {FieldDiscoveryResult} discovery_result
54
+ * @param {ResolvedOutputMode} output_mode
55
+ * @returns {string}
56
+ */
57
+ function renderTextFieldDiscovery(discovery_result, output_mode) {
58
+ const ansi = new Ansis(
59
+ output_mode.renderer_name === 'rich' && output_mode.color_enabled ? 3 : 0,
60
+ );
61
+ /** @type {string[]} */
62
+ const output_lines = [];
63
+
64
+ output_lines.push(
65
+ output_mode.renderer_name === 'rich'
66
+ ? ansi.green('Field discovery')
67
+ : 'Field discovery',
68
+ );
69
+
70
+ output_lines.push(
71
+ output_mode.renderer_name === 'rich'
72
+ ? ansi.gray(
73
+ `Found ${discovery_result.summary.count} suggested fields from ${discovery_result.summary.source_file_count} source files.`,
74
+ )
75
+ : `Found ${discovery_result.summary.count} suggested fields from ${discovery_result.summary.source_file_count} source files.`,
76
+ );
77
+
78
+ for (const field_suggestion of discovery_result.fields) {
79
+ output_lines.push(
80
+ '',
81
+ ...formatTextFieldSuggestion(field_suggestion, {
82
+ header: (value) =>
83
+ output_mode.renderer_name === 'rich' ? ansi.green(value) : value,
84
+ label: (value) =>
85
+ output_mode.renderer_name === 'rich' ? ansi.gray(value) : value,
86
+ }),
87
+ );
88
+ }
89
+
90
+ if (discovery_result.fields.length === 0) {
91
+ output_lines.push('', 'No field candidates discovered.');
92
+ }
93
+
94
+ return `${output_lines.join('\n')}\n`;
95
+ }
96
+
97
+ /**
98
+ * @param {FieldDiscoverySuggestion} field_suggestion
99
+ * @param {{ header: (value: string) => string, label: (value: string) => string }} render_options
100
+ * @returns {string[]}
101
+ */
102
+ function formatTextFieldSuggestion(field_suggestion, render_options) {
103
+ /** @type {string[]} */
104
+ const lines = [render_options.header(field_suggestion.name)];
105
+
106
+ lines.push(
107
+ `${render_options.label(' likely type:')} ${field_suggestion.likely_type.name}`,
108
+ `${render_options.label(' likely multiplicity:')} ${field_suggestion.likely_multiplicity.name}`,
109
+ `${render_options.label(' likely class usage:')} ${field_suggestion.likely_class_usage.classes.join(', ')}`,
110
+ `${render_options.label(' confidence:')} ${formatConfidence(field_suggestion.confidence)}`,
111
+ );
112
+
113
+ if (field_suggestion.evidence_references.length > 0) {
114
+ lines.push(
115
+ ...formatTextEvidenceSection(
116
+ ' evidence:',
117
+ field_suggestion.evidence_references,
118
+ render_options,
119
+ ),
120
+ );
121
+ }
122
+
123
+ if (field_suggestion.conflicting_evidence.length > 0) {
124
+ lines.push(
125
+ ...formatTextEvidenceSection(
126
+ ' conflicting evidence:',
127
+ field_suggestion.conflicting_evidence,
128
+ render_options,
129
+ ),
130
+ );
131
+ }
132
+
133
+ return lines;
134
+ }
135
+
136
+ /**
137
+ * @param {string} section_title
138
+ * @param {import('./discover-fields.types.ts').FieldDiscoveryEvidenceReference[]} evidence_references
139
+ * @param {{ header: (value: string) => string, label: (value: string) => string }} render_options
140
+ * @returns {string[]}
141
+ */
142
+ function formatTextEvidenceSection(
143
+ section_title,
144
+ evidence_references,
145
+ render_options,
146
+ ) {
147
+ /** @type {string[]} */
148
+ const lines = [render_options.label(section_title)];
149
+ const visible_evidence_references = evidence_references.slice(
150
+ 0,
151
+ MAX_TEXT_EVIDENCE_ROWS,
152
+ );
153
+
154
+ lines.push(
155
+ ...visible_evidence_references.map(
156
+ (evidence_reference) =>
157
+ `${render_options.label(' ')}${formatEvidenceReference(evidence_reference)}`,
158
+ ),
159
+ );
160
+
161
+ if (evidence_references.length > MAX_TEXT_EVIDENCE_ROWS) {
162
+ const remaining_count = evidence_references.length - MAX_TEXT_EVIDENCE_ROWS;
163
+
164
+ lines.push(render_options.label(` ${remaining_count} more ...`));
165
+ }
166
+
167
+ return lines;
168
+ }
169
+
170
+ /**
171
+ * @param {import('./discover-fields.types.ts').FieldDiscoveryEvidenceReference} evidence_reference
172
+ * @returns {string}
173
+ */
174
+ function formatEvidenceReference(evidence_reference) {
175
+ return `${evidence_reference.path}:${evidence_reference.line}:${evidence_reference.column} ${JSON.stringify(evidence_reference.value)}`;
176
+ }
177
+
178
+ /**
179
+ * @param {number} confidence
180
+ * @returns {string}
181
+ */
182
+ function formatConfidence(confidence) {
183
+ return confidence.toFixed(2);
184
+ }
@@ -55,19 +55,19 @@ export function renderJsonOutput(output_view) {
55
55
 
56
56
  /**
57
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 }}
58
+ * @returns {{ '$class': string, '$id': string, '$path'?: string, derived?: Record<string, boolean | number | string | null>, derived_summary?: string, fields: Record<string, string | string[]>, title: string }}
59
59
  */
60
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 }} */
61
+ /** @type {{ '$class': string, '$id': string, '$path'?: string, derived?: Record<string, boolean | number | string | null>, derived_summary?: string, fields: Record<string, string | string[]>, title: string }} */
62
62
  const query_item = {
63
- id: output_item.id,
64
- kind: output_item.node_kind,
63
+ $class: output_item.node_kind,
64
+ $id: output_item.id,
65
+ fields: output_item.fields,
65
66
  title: output_item.title,
66
- path: output_item.path,
67
67
  };
68
68
 
69
- if (output_item.status) {
70
- query_item.status = output_item.status;
69
+ if (output_item.path) {
70
+ query_item.$path = output_item.path;
71
71
  }
72
72
 
73
73
  if (output_item.derived_summary) {
@@ -96,25 +96,23 @@ function formatJsonStoredQuery(output_item) {
96
96
 
97
97
  /**
98
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 } }}
99
+ * @returns {{ label: string, reference: number, target: { '$class': string, '$id': string, '$path'?: string, derived?: Record<string, boolean | number | string | null>, derived_summary?: string, fields: Record<string, string | string[]>, title: string } }}
100
100
  */
101
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 } }} */
102
+ /** @type {{ label: string, reference: number, target: { '$class': string, '$id': string, '$path'?: string, derived?: Record<string, boolean | number | string | null>, derived_summary?: string, fields: Record<string, string | string[]>, title: string } }} */
103
103
  const resolved_link = {
104
104
  reference: output_item.reference,
105
105
  label: output_item.label,
106
106
  target: {
107
+ $class: output_item.target.kind,
108
+ $id: output_item.target.id,
109
+ fields: output_item.target.fields,
107
110
  title: output_item.target.title,
108
- path: output_item.target.path,
109
111
  },
110
112
  };
111
113
 
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;
114
+ if (output_item.target.path) {
115
+ resolved_link.target.$path = output_item.target.path;
118
116
  }
119
117
 
120
118
  if (output_item.target.derived_summary) {
@@ -133,18 +131,19 @@ function formatJsonResolvedLink(output_item) {
133
131
 
134
132
  /**
135
133
  * @param {OutputNodeItem} output_item
136
- * @returns {{ derived?: Record<string, boolean | number | string | null>, derived_summary?: string, kind: string, title: string, path: string, status?: string }}
134
+ * @returns {{ '$class': string, '$id': string, '$path'?: string, derived?: Record<string, boolean | number | string | null>, derived_summary?: string, fields: Record<string, string | string[]>, title: string }}
137
135
  */
138
136
  function formatJsonShowDocument(output_item) {
139
- /** @type {{ derived?: Record<string, boolean | number | string | null>, derived_summary?: string, kind: string, title: string, path: string, status?: string }} */
137
+ /** @type {{ '$class': string, '$id': string, '$path'?: string, derived?: Record<string, boolean | number | string | null>, derived_summary?: string, fields: Record<string, string | string[]>, title: string }} */
140
138
  const document_summary = {
141
- kind: output_item.node_kind,
142
- path: output_item.path,
139
+ $class: output_item.node_kind,
140
+ $id: output_item.id,
141
+ fields: output_item.fields,
143
142
  title: output_item.title,
144
143
  };
145
144
 
146
- if (output_item.status) {
147
- document_summary.status = output_item.status;
145
+ if (output_item.path) {
146
+ document_summary.$path = output_item.path;
148
147
  }
149
148
 
150
149
  if (output_item.derived_summary) {
@@ -1,14 +1,17 @@
1
1
  /** @import * as $k$$l$output$j$view$k$types$k$ts from './output-view.types.ts'; */
2
+ /* eslint-disable max-lines */
2
3
  /**
3
4
  * @import { BuildGraphResult, GraphNode } from './build-graph.types.ts';
4
5
  * @import { DerivedSummaryEvaluator } from './derived-summary.js';
6
+ * @import { PatramRepoConfig } from './load-patram-config.types.ts';
5
7
  * @import { ParsedCliArguments } from './parse-cli-arguments.types.ts';
6
- * @import { OutputStoredQueryItem, OutputView, ResolvedOutputMode, ShowOutputView } from './output-view.types.ts';
8
+ * @import { OutputMetadataField, OutputStoredQueryItem, OutputView, ResolvedOutputMode, ShowOutputView } from './output-view.types.ts';
7
9
  */
8
10
 
9
11
  import { renderJsonOutput } from './render-json-output.js';
10
12
  import { renderPlainOutput } from './render-plain-output.js';
11
13
  import { renderRichOutput } from './render-rich-output.js';
14
+ import { resolveDocumentNodeId } from './build-graph-identity.js';
12
15
 
13
16
  /**
14
17
  * Shared command output views.
@@ -30,7 +33,7 @@ import { renderRichOutput } from './render-rich-output.js';
30
33
  *
31
34
  * @param {'query' | 'queries'} command_name
32
35
  * @param {GraphNode[] | { name: string, where: string }[]} command_items
33
- * @param {{ derived_summary_evaluator?: DerivedSummaryEvaluator, hints?: string[], limit?: number, offset?: number, total_count?: number }=} command_options
36
+ * @param {{ derived_summary_evaluator?: DerivedSummaryEvaluator, hints?: string[], limit?: number, offset?: number, repo_config?: PatramRepoConfig, total_count?: number }=} command_options
34
37
  * @returns {OutputView}
35
38
  */
36
39
  export function createOutputView(command_name, command_items, command_options) {
@@ -54,12 +57,15 @@ export function createOutputView(command_name, command_items, command_options) {
54
57
  * Create a shared output view for the show command.
55
58
  *
56
59
  * @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
57
- * @param {{ derived_summary_evaluator?: DerivedSummaryEvaluator, graph_nodes?: BuildGraphResult['nodes'] }=} command_options
60
+ * @param {{ derived_summary_evaluator?: DerivedSummaryEvaluator, document_node_ids?: BuildGraphResult['document_node_ids'], graph_nodes?: BuildGraphResult['nodes'], repo_config?: PatramRepoConfig }=} command_options
58
61
  * @returns {ShowOutputView}
59
62
  */
60
63
  export function createShowOutputView(show_output, command_options = {}) {
61
- const shown_document_node =
62
- command_options.graph_nodes?.[`doc:${show_output.path}`];
64
+ const shown_document_node = resolveDocumentGraphNode(
65
+ command_options.graph_nodes,
66
+ command_options.document_node_ids,
67
+ show_output.path,
68
+ );
63
69
 
64
70
  return {
65
71
  command: 'show',
@@ -69,22 +75,13 @@ export function createShowOutputView(show_output, command_options = {}) {
69
75
  command_options.derived_summary_evaluator?.evaluate(
70
76
  shown_document_node,
71
77
  ) ?? null,
78
+ command_options.repo_config?.fields ?? {},
72
79
  )
73
80
  : undefined,
74
81
  hints: [],
75
- items: show_output.resolved_links.map((resolved_link) => ({
76
- kind: 'resolved_link',
77
- label: resolved_link.label,
78
- reference: resolved_link.reference,
79
- target: createResolvedLinkTarget(
80
- resolved_link.target,
81
- command_options.graph_nodes?.[`doc:${resolved_link.target.path}`]
82
- ? (command_options.derived_summary_evaluator?.evaluate(
83
- command_options.graph_nodes[`doc:${resolved_link.target.path}`],
84
- ) ?? null)
85
- : null,
86
- ),
87
- })),
82
+ items: show_output.resolved_links.map((resolved_link) =>
83
+ createResolvedLinkOutputItem(resolved_link, command_options),
84
+ ),
88
85
  path: show_output.path,
89
86
  rendered_source: show_output.rendered_source,
90
87
  source: show_output.source,
@@ -124,7 +121,7 @@ export async function renderOutputView(
124
121
 
125
122
  /**
126
123
  * @param {GraphNode[]} graph_nodes
127
- * @param {{ derived_summary_evaluator?: DerivedSummaryEvaluator, hints?: string[], limit?: number, offset?: number, total_count?: number }=} command_options
124
+ * @param {{ derived_summary_evaluator?: DerivedSummaryEvaluator, hints?: string[], limit?: number, offset?: number, repo_config?: PatramRepoConfig, total_count?: number }=} command_options
128
125
  * @returns {OutputView}
129
126
  */
130
127
  function createQueryOutputView(graph_nodes, command_options = {}) {
@@ -134,11 +131,12 @@ function createQueryOutputView(graph_nodes, command_options = {}) {
134
131
  command: 'query',
135
132
  hints:
136
133
  command_options.hints ??
137
- (total_count === 0 ? ['Try: patram query --where "kind=task"'] : []),
134
+ (total_count === 0 ? ['Try: patram query --where "$class=task"'] : []),
138
135
  items: graph_nodes.map((graph_node) =>
139
136
  createOutputNodeItem(
140
137
  graph_node,
141
138
  command_options.derived_summary_evaluator?.evaluate(graph_node) ?? null,
139
+ command_options.repo_config?.fields ?? {},
142
140
  ),
143
141
  ),
144
142
  summary: {
@@ -174,13 +172,17 @@ function createStoredQueriesOutputView(stored_queries) {
174
172
  /**
175
173
  * @param {GraphNode} graph_node
176
174
  * @param {import('./output-view.types.ts').OutputDerivedSummary | null} derived_summary
175
+ * @param {NonNullable<PatramRepoConfig['fields']>} field_definitions
177
176
  * @returns {$k$$l$output$j$view$k$types$k$ts.OutputNodeItem}
178
177
  */
179
- function createOutputNodeItem(graph_node, derived_summary) {
180
- const title =
181
- graph_node.title ?? graph_node.label ?? graph_node.path ?? graph_node.key;
178
+ function createOutputNodeItem(graph_node, derived_summary, field_definitions) {
179
+ const title = getOutputNodeTitle(graph_node);
180
+ const path = getOutputNodePath(graph_node);
181
+ const node_class = getOutputNodeClass(graph_node);
182
+ const fields = collectOutputFields(graph_node, field_definitions);
183
+ const visible_fields = createVisibleOutputFields(fields, field_definitions);
182
184
 
183
- if (!title || !graph_node.path) {
185
+ if (!title || !node_class) {
184
186
  throw new Error(
185
187
  `Expected graph node "${graph_node.id}" to have a title and path.`,
186
188
  );
@@ -188,35 +190,300 @@ function createOutputNodeItem(graph_node, derived_summary) {
188
190
 
189
191
  return {
190
192
  derived_summary: derived_summary ?? undefined,
191
- id: graph_node.id,
193
+ fields,
194
+ id: getOutputNodeId(graph_node),
192
195
  kind: 'node',
193
- node_kind: graph_node.kind,
194
- path: graph_node.path,
195
- status: graph_node.status,
196
+ node_kind: node_class,
197
+ path,
196
198
  title,
199
+ visible_fields,
197
200
  };
198
201
  }
199
202
 
200
203
  /**
201
204
  * @param {{ kind?: string, path: string, status?: string, title: string }} target
205
+ * @param {NonNullable<PatramRepoConfig['fields']>} field_definitions
202
206
  * @param {import('./output-view.types.ts').OutputDerivedSummary | null} derived_summary
207
+ * @param {GraphNode | undefined} graph_node
203
208
  * @returns {$k$$l$output$j$view$k$types$k$ts.OutputResolvedLinkTarget}
204
209
  */
205
- function createResolvedLinkTarget(target, derived_summary) {
210
+ function createResolvedLinkTarget(
211
+ target,
212
+ field_definitions,
213
+ derived_summary,
214
+ graph_node,
215
+ ) {
216
+ /** @type {Record<string, string | string[]>} */
217
+ const fields = {};
218
+
219
+ if (target.status) {
220
+ fields.status = target.status;
221
+ }
222
+
206
223
  /** @type {$k$$l$output$j$view$k$types$k$ts.OutputResolvedLinkTarget} */
207
224
  const resolved_target = {
208
225
  derived_summary: derived_summary ?? undefined,
226
+ fields,
227
+ id: graph_node ? getOutputNodeId(graph_node) : `doc:${target.path}`,
228
+ kind: target.kind ?? 'document',
209
229
  path: target.path,
210
230
  title: target.title,
231
+ visible_fields: createVisibleOutputFields(fields, field_definitions),
211
232
  };
212
233
 
213
- if (target.kind && target.kind !== 'document') {
214
- resolved_target.kind = target.kind;
234
+ return resolved_target;
235
+ }
236
+
237
+ /**
238
+ * @param {string | string[] | undefined} field_value
239
+ * @returns {string | undefined}
240
+ */
241
+ function getScalarGraphNodeField(field_value) {
242
+ if (Array.isArray(field_value)) {
243
+ return field_value[0];
215
244
  }
216
245
 
217
- if (target.status) {
218
- resolved_target.status = target.status;
246
+ return field_value;
247
+ }
248
+
249
+ /**
250
+ * @param {GraphNode} graph_node
251
+ * @returns {string | undefined}
252
+ */
253
+ function getOutputNodeTitle(graph_node) {
254
+ return (
255
+ getScalarGraphNodeField(graph_node.title) ??
256
+ getScalarGraphNodeField(graph_node.label) ??
257
+ getOutputNodePath(graph_node) ??
258
+ getScalarGraphNodeField(graph_node.key)
259
+ );
260
+ }
261
+
262
+ /**
263
+ * @param {GraphNode} graph_node
264
+ * @returns {string | undefined}
265
+ */
266
+ function getOutputNodePath(graph_node) {
267
+ return getScalarGraphNodeField(graph_node.$path ?? graph_node.path);
268
+ }
269
+
270
+ /**
271
+ * @param {GraphNode} graph_node
272
+ * @returns {string | undefined}
273
+ */
274
+ function getOutputNodeClass(graph_node) {
275
+ return getScalarGraphNodeField(graph_node.$class ?? graph_node.kind);
276
+ }
277
+
278
+ /**
279
+ * @param {GraphNode} graph_node
280
+ * @returns {string}
281
+ */
282
+ function getOutputNodeId(graph_node) {
283
+ return (
284
+ getScalarGraphNodeField(graph_node.$id ?? graph_node.id) ?? graph_node.id
285
+ );
286
+ }
287
+
288
+ /**
289
+ * @param {{ label: string, reference: number, target: { kind?: string, path: string, status?: string, title: string } }} resolved_link
290
+ * @param {{ derived_summary_evaluator?: DerivedSummaryEvaluator, document_node_ids?: BuildGraphResult['document_node_ids'], graph_nodes?: BuildGraphResult['nodes'], repo_config?: PatramRepoConfig }} command_options
291
+ * @returns {$k$$l$output$j$view$k$types$k$ts.OutputResolvedLinkItem}
292
+ */
293
+ function createResolvedLinkOutputItem(resolved_link, command_options) {
294
+ const target_graph_node = resolveDocumentGraphNode(
295
+ command_options.graph_nodes,
296
+ command_options.document_node_ids,
297
+ resolved_link.target.path,
298
+ );
299
+
300
+ return {
301
+ kind: 'resolved_link',
302
+ label: resolved_link.label,
303
+ reference: resolved_link.reference,
304
+ target: createResolvedLinkTarget(
305
+ resolved_link.target,
306
+ command_options.repo_config?.fields ?? {},
307
+ target_graph_node
308
+ ? (command_options.derived_summary_evaluator?.evaluate(
309
+ target_graph_node,
310
+ ) ?? null)
311
+ : null,
312
+ target_graph_node,
313
+ ),
314
+ };
315
+ }
316
+
317
+ /**
318
+ * @param {BuildGraphResult['nodes'] | undefined} graph_nodes
319
+ * @param {BuildGraphResult['document_node_ids'] | undefined} document_node_ids
320
+ * @param {string} document_path
321
+ * @returns {GraphNode | undefined}
322
+ */
323
+ function resolveDocumentGraphNode(
324
+ graph_nodes,
325
+ document_node_ids,
326
+ document_path,
327
+ ) {
328
+ if (!graph_nodes) {
329
+ return undefined;
219
330
  }
220
331
 
221
- return resolved_target;
332
+ return graph_nodes[resolveDocumentNodeId(document_node_ids, document_path)];
333
+ }
334
+
335
+ /**
336
+ * @param {GraphNode} graph_node
337
+ * @param {NonNullable<PatramRepoConfig['fields']>} field_definitions
338
+ * @returns {Record<string, string | string[]>}
339
+ */
340
+ function collectOutputFields(graph_node, field_definitions) {
341
+ /** @type {Record<string, string | string[]>} */
342
+ const fields = {};
343
+
344
+ for (const [field_name, field_value] of Object.entries(graph_node)) {
345
+ const normalized_value = getCollectedOutputFieldValue(
346
+ graph_node,
347
+ field_name,
348
+ field_value,
349
+ );
350
+
351
+ if (normalized_value === undefined) {
352
+ continue;
353
+ }
354
+
355
+ fields[field_name] = normalized_value;
356
+ }
357
+
358
+ for (const field_name of Object.keys(field_definitions)) {
359
+ if (fields[field_name] !== undefined) {
360
+ continue;
361
+ }
362
+
363
+ const field_value = normalizeOutputFieldValue(graph_node[field_name]);
364
+
365
+ if (field_value !== undefined) {
366
+ fields[field_name] = field_value;
367
+ }
368
+ }
369
+
370
+ return fields;
371
+ }
372
+
373
+ /**
374
+ * @param {GraphNode} graph_node
375
+ * @param {string} field_name
376
+ * @param {unknown} field_value
377
+ * @returns {string | string[] | undefined}
378
+ */
379
+ function getCollectedOutputFieldValue(graph_node, field_name, field_value) {
380
+ if (isInternalOutputField(field_name)) {
381
+ return undefined;
382
+ }
383
+
384
+ const normalized_value = normalizeOutputFieldValue(field_value);
385
+
386
+ if (normalized_value === undefined) {
387
+ return undefined;
388
+ }
389
+
390
+ if (isLegacyMirrorOutputField(graph_node, field_name, normalized_value)) {
391
+ return undefined;
392
+ }
393
+
394
+ return normalized_value;
395
+ }
396
+
397
+ /**
398
+ * @param {GraphNode} graph_node
399
+ * @param {string} field_name
400
+ * @param {string | string[]} normalized_value
401
+ * @returns {boolean}
402
+ */
403
+ function isLegacyMirrorOutputField(graph_node, field_name, normalized_value) {
404
+ if (Array.isArray(normalized_value)) {
405
+ return false;
406
+ }
407
+
408
+ if (field_name === 'kind') {
409
+ return normalized_value === graph_node.$class;
410
+ }
411
+
412
+ if (field_name === 'path') {
413
+ return normalized_value === graph_node.$path;
414
+ }
415
+
416
+ if (field_name === 'id') {
417
+ return normalized_value === graph_node.$id;
418
+ }
419
+
420
+ return false;
421
+ }
422
+
423
+ /**
424
+ * @param {Record<string, string | string[]>} fields
425
+ * @param {NonNullable<PatramRepoConfig['fields']>} field_definitions
426
+ * @returns {OutputMetadataField[]}
427
+ */
428
+ function createVisibleOutputFields(fields, field_definitions) {
429
+ return Object.entries(fields)
430
+ .filter(
431
+ ([field_name]) => field_definitions[field_name]?.display?.hidden !== true,
432
+ )
433
+ .sort(([left_name], [right_name]) =>
434
+ compareOutputFieldNames(left_name, right_name, field_definitions),
435
+ )
436
+ .map(([name, value]) => ({ name, value }));
437
+ }
438
+
439
+ /**
440
+ * @param {string} field_name
441
+ * @returns {boolean}
442
+ */
443
+ function isInternalOutputField(field_name) {
444
+ return (
445
+ field_name === '$class' ||
446
+ field_name === '$id' ||
447
+ field_name === '$path' ||
448
+ field_name === 'id' ||
449
+ field_name === 'key' ||
450
+ field_name === 'label' ||
451
+ field_name === 'path' ||
452
+ field_name === 'title'
453
+ );
454
+ }
455
+
456
+ /**
457
+ * @param {string} left_name
458
+ * @param {string} right_name
459
+ * @param {NonNullable<PatramRepoConfig['fields']>} field_definitions
460
+ * @returns {number}
461
+ */
462
+ function compareOutputFieldNames(left_name, right_name, field_definitions) {
463
+ const left_order =
464
+ field_definitions[left_name]?.display?.order ?? Number.MAX_SAFE_INTEGER;
465
+ const right_order =
466
+ field_definitions[right_name]?.display?.order ?? Number.MAX_SAFE_INTEGER;
467
+
468
+ if (left_order !== right_order) {
469
+ return left_order - right_order;
470
+ }
471
+
472
+ return left_name.localeCompare(right_name, 'en');
473
+ }
474
+
475
+ /**
476
+ * @param {unknown} field_value
477
+ * @returns {string | string[] | undefined}
478
+ */
479
+ function normalizeOutputFieldValue(field_value) {
480
+ if (Array.isArray(field_value)) {
481
+ const string_values = field_value.flatMap((value) =>
482
+ typeof value === 'string' ? [value] : [],
483
+ );
484
+
485
+ return string_values.length > 0 ? string_values : undefined;
486
+ }
487
+
488
+ return typeof field_value === 'string' ? field_value : undefined;
222
489
  }