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.
- package/lib/build-graph-identity.js +86 -99
- package/lib/build-graph.js +536 -31
- package/lib/build-graph.types.ts +6 -2
- package/lib/check-directive-metadata.js +534 -0
- package/lib/check-directive-value.js +291 -0
- package/lib/check-graph.js +23 -5
- package/lib/cli-help-metadata.js +56 -16
- package/lib/command-output.js +16 -1
- package/lib/derived-summary.js +10 -8
- package/lib/directive-diagnostics.js +38 -0
- package/lib/directive-type-rules.js +133 -0
- package/lib/discover-fields.js +435 -0
- package/lib/discover-fields.types.ts +52 -0
- package/lib/document-node-identity.js +317 -0
- package/lib/format-node-header.js +9 -7
- package/lib/format-output-metadata.js +15 -23
- package/lib/layout-stored-queries.js +124 -85
- package/lib/load-patram-config.js +433 -96
- package/lib/load-patram-config.types.ts +98 -3
- package/lib/load-project-graph.js +4 -1
- package/lib/output-view.types.ts +14 -6
- package/lib/parse-cli-arguments.types.ts +1 -1
- package/lib/parse-where-clause.js +344 -107
- package/lib/parse-where-clause.types.ts +25 -8
- package/lib/patram-cli.js +68 -4
- package/lib/patram-config.js +31 -31
- package/lib/patram-config.types.ts +10 -4
- package/lib/query-graph.js +269 -40
- package/lib/query-inspection.js +440 -60
- package/lib/render-field-discovery.js +184 -0
- package/lib/render-json-output.js +21 -22
- package/lib/render-output-view.js +301 -34
- package/lib/render-plain-output.js +1 -1
- package/lib/render-rich-output.js +1 -1
- package/lib/render-rich-source.js +245 -14
- package/lib/resolve-patram-graph-config.js +15 -9
- package/lib/show-document.js +66 -9
- 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,
|
|
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,
|
|
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
|
-
|
|
64
|
-
|
|
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.
|
|
70
|
-
query_item
|
|
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,
|
|
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,
|
|
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.
|
|
113
|
-
resolved_link.target
|
|
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,
|
|
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,
|
|
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
|
-
|
|
142
|
-
|
|
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.
|
|
147
|
-
document_summary
|
|
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
|
|
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
|
-
|
|
77
|
-
|
|
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 "
|
|
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
|
-
|
|
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 || !
|
|
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
|
-
|
|
193
|
+
fields,
|
|
194
|
+
id: getOutputNodeId(graph_node),
|
|
192
195
|
kind: 'node',
|
|
193
|
-
node_kind:
|
|
194
|
-
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(
|
|
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
|
-
|
|
214
|
-
|
|
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
|
-
|
|
218
|
-
|
|
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
|
|
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
|
}
|