patram 0.7.0 → 0.8.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/cli-help-metadata.js +48 -9
- package/lib/command-output.js +1 -0
- package/lib/inspect-reverse-references.js +184 -0
- package/lib/layout-incoming-references.js +105 -0
- package/lib/layout-incoming-summary-lines.js +21 -0
- package/lib/output-view.types.d.ts +10 -2
- package/lib/output-view.types.ts +19 -2
- package/lib/parse-cli-arguments-helpers.js +11 -4
- package/lib/parse-cli-arguments.types.ts +8 -2
- package/lib/patram-cli.js +66 -1
- package/lib/render-cli-help.js +1 -1
- package/lib/render-json-output.js +91 -62
- package/lib/render-output-view.js +58 -3
- package/lib/render-plain-output.js +46 -3
- package/lib/render-rich-output.js +50 -5
- package/lib/reverse-reference-test-helpers.js +76 -0
- package/lib/show-document.js +44 -6
- package/package.json +1 -1
package/lib/cli-help-metadata.js
CHANGED
|
@@ -50,6 +50,7 @@ export const COMMAND_NAMES = /** @type {const} */ ([
|
|
|
50
50
|
'fields',
|
|
51
51
|
'query',
|
|
52
52
|
'queries',
|
|
53
|
+
'refs',
|
|
53
54
|
'show',
|
|
54
55
|
]);
|
|
55
56
|
|
|
@@ -146,12 +147,12 @@ const COMMAND_DEFINITIONS = {
|
|
|
146
147
|
]),
|
|
147
148
|
examples: [
|
|
148
149
|
'patram query active-plans',
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
"patram query --where 'tracked_in=doc:docs/plans/v0/worktracking-agent-guidance.md'",
|
|
151
|
+
"patram query --where 'status not in [done, dropped, superseded]'",
|
|
152
|
+
"patram query --where '$class=plan and none(in:tracked_in, $class=decision)'",
|
|
153
|
+
"patram query --where 'count(in:decided_by, $class=task) = 0'",
|
|
153
154
|
'patram query ready-tasks --explain',
|
|
154
|
-
|
|
155
|
+
"patram query --where '$class=decision and status=accepted and count(in:decided_by, $class=task) = 0' --lint",
|
|
155
156
|
'patram query active-plans --limit 10 --offset 20',
|
|
156
157
|
],
|
|
157
158
|
extra_positionals_message:
|
|
@@ -161,12 +162,12 @@ const COMMAND_DEFINITIONS = {
|
|
|
161
162
|
min_positionals: 0,
|
|
162
163
|
missing_argument_examples: [
|
|
163
164
|
'patram query active-plans',
|
|
164
|
-
|
|
165
|
+
"patram query --where 'tracked_in=doc:docs/plans/v0/worktracking-agent-guidance.md'",
|
|
165
166
|
],
|
|
166
|
-
missing_argument_label:
|
|
167
|
+
missing_argument_label: "<name> or --where '<clause>'",
|
|
167
168
|
missing_usage_lines: [
|
|
168
169
|
'patram query <name> [options]',
|
|
169
|
-
|
|
170
|
+
"patram query --where '<clause>' [options]",
|
|
170
171
|
],
|
|
171
172
|
option_column_width: 19,
|
|
172
173
|
options: [
|
|
@@ -214,7 +215,7 @@ const COMMAND_DEFINITIONS = {
|
|
|
214
215
|
],
|
|
215
216
|
usage_lines: [
|
|
216
217
|
'patram query <name> [options]',
|
|
217
|
-
|
|
218
|
+
"patram query --where '<clause>' [options]",
|
|
218
219
|
],
|
|
219
220
|
},
|
|
220
221
|
queries: {
|
|
@@ -243,6 +244,44 @@ const COMMAND_DEFINITIONS = {
|
|
|
243
244
|
summary: 'List the stored queries defined in the project configuration.',
|
|
244
245
|
usage_lines: ['patram queries [options]'],
|
|
245
246
|
},
|
|
247
|
+
refs: {
|
|
248
|
+
allowed_option_names: new Set(['where']),
|
|
249
|
+
examples: [
|
|
250
|
+
'patram refs docs/decisions/query-language.md',
|
|
251
|
+
"patram refs docs/decisions/query-language.md --where '$class=document'",
|
|
252
|
+
'patram refs docs/decisions/query-language.md --json',
|
|
253
|
+
],
|
|
254
|
+
extra_positionals_message: 'Refs accepts exactly one file path.',
|
|
255
|
+
help_topics: ['query-language'],
|
|
256
|
+
max_positionals: 1,
|
|
257
|
+
min_positionals: 1,
|
|
258
|
+
missing_argument_examples: [
|
|
259
|
+
'patram refs docs/decisions/query-language.md',
|
|
260
|
+
"patram refs docs/patram.md --where '$class=document'",
|
|
261
|
+
],
|
|
262
|
+
missing_argument_label: '<file>',
|
|
263
|
+
missing_usage_lines: ['patram refs <file>'],
|
|
264
|
+
option_column_width: 19,
|
|
265
|
+
options: [
|
|
266
|
+
{
|
|
267
|
+
description: 'Filter incoming source nodes with a where clause',
|
|
268
|
+
label: '--where <clause>',
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
description: 'Print plain text output',
|
|
272
|
+
label: '--plain',
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
description: 'Print JSON output',
|
|
276
|
+
label: '--json',
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
related: ['show', 'query'],
|
|
280
|
+
root_summary: 'Inspect incoming graph references for one file',
|
|
281
|
+
summary:
|
|
282
|
+
'Inspect incoming graph references for one file, grouped by relation.',
|
|
283
|
+
usage_lines: ['patram refs <file> [options]'],
|
|
284
|
+
},
|
|
246
285
|
show: {
|
|
247
286
|
allowed_option_names: new Set(),
|
|
248
287
|
examples: ['patram show docs/patram.md', 'patram show lib/patram-cli.js'],
|
package/lib/command-output.js
CHANGED
|
@@ -78,6 +78,7 @@ export function shouldPageCommandOutput(parsed_command, output_stream) {
|
|
|
78
78
|
output_stream.isTTY === true &&
|
|
79
79
|
(parsed_command.command_name === 'fields' ||
|
|
80
80
|
parsed_command.command_name === 'query' ||
|
|
81
|
+
parsed_command.command_name === 'refs' ||
|
|
81
82
|
parsed_command.command_name === 'show')
|
|
82
83
|
);
|
|
83
84
|
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { BuildGraphResult, GraphNode } from './build-graph.types.ts';
|
|
3
|
+
* @import { PatramDiagnostic, PatramRepoConfig } from './load-patram-config.types.ts';
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
normalizeRepoRelativePath,
|
|
8
|
+
resolveDocumentNodeId,
|
|
9
|
+
} from './document-node-identity.js';
|
|
10
|
+
import { queryGraph } from './query-graph.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Inspect incoming graph edges for one canonical target file.
|
|
14
|
+
*
|
|
15
|
+
* @param {BuildGraphResult} graph
|
|
16
|
+
* @param {string} target_file_path
|
|
17
|
+
* @param {PatramRepoConfig | undefined} repo_config
|
|
18
|
+
* @param {string | undefined} where_clause
|
|
19
|
+
* @returns {{
|
|
20
|
+
* diagnostics: PatramDiagnostic[],
|
|
21
|
+
* incoming: Record<string, GraphNode[]>,
|
|
22
|
+
* node: GraphNode,
|
|
23
|
+
* }}
|
|
24
|
+
*/
|
|
25
|
+
export function inspectReverseReferences(
|
|
26
|
+
graph,
|
|
27
|
+
target_file_path,
|
|
28
|
+
repo_config,
|
|
29
|
+
where_clause,
|
|
30
|
+
) {
|
|
31
|
+
const normalized_target_path = normalizeRepoRelativePath(target_file_path);
|
|
32
|
+
const target_node_id = resolveDocumentNodeId(
|
|
33
|
+
graph.document_node_ids,
|
|
34
|
+
normalized_target_path,
|
|
35
|
+
);
|
|
36
|
+
const target_node =
|
|
37
|
+
graph.nodes[target_node_id] ??
|
|
38
|
+
createFallbackTargetNode(target_node_id, normalized_target_path);
|
|
39
|
+
const allowed_source_node_ids = where_clause
|
|
40
|
+
? resolveAllowedSourceNodeIds(graph, where_clause, repo_config)
|
|
41
|
+
: null;
|
|
42
|
+
|
|
43
|
+
if (allowed_source_node_ids && 'diagnostics' in allowed_source_node_ids) {
|
|
44
|
+
return {
|
|
45
|
+
diagnostics: allowed_source_node_ids.diagnostics,
|
|
46
|
+
incoming: {},
|
|
47
|
+
node: target_node,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
diagnostics: [],
|
|
53
|
+
incoming: collectIncomingGroups(
|
|
54
|
+
graph,
|
|
55
|
+
target_node.id,
|
|
56
|
+
allowed_source_node_ids,
|
|
57
|
+
),
|
|
58
|
+
node: target_node,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {BuildGraphResult} graph
|
|
64
|
+
* @param {string} target_node_id
|
|
65
|
+
* @param {Set<string> | null} allowed_source_node_ids
|
|
66
|
+
* @returns {Record<string, GraphNode[]>}
|
|
67
|
+
*/
|
|
68
|
+
function collectIncomingGroups(graph, target_node_id, allowed_source_node_ids) {
|
|
69
|
+
/** @type {Map<string, Map<string, GraphNode>>} */
|
|
70
|
+
const grouped_incoming = new Map();
|
|
71
|
+
|
|
72
|
+
for (const graph_edge of graph.edges) {
|
|
73
|
+
if (graph_edge.to !== target_node_id) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (
|
|
78
|
+
allowed_source_node_ids &&
|
|
79
|
+
!allowed_source_node_ids.has(graph_edge.from)
|
|
80
|
+
) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const source_node = graph.nodes[graph_edge.from];
|
|
85
|
+
|
|
86
|
+
if (!source_node) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let relation_sources = grouped_incoming.get(graph_edge.relation);
|
|
91
|
+
|
|
92
|
+
if (!relation_sources) {
|
|
93
|
+
relation_sources = new Map();
|
|
94
|
+
grouped_incoming.set(graph_edge.relation, relation_sources);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
relation_sources.set(source_node.id, source_node);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return formatIncomingGroups(grouped_incoming);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @param {BuildGraphResult} graph
|
|
105
|
+
* @param {string} where_clause
|
|
106
|
+
* @param {PatramRepoConfig | undefined} repo_config
|
|
107
|
+
* @returns {{ diagnostics: PatramDiagnostic[] } | Set<string>}
|
|
108
|
+
*/
|
|
109
|
+
function resolveAllowedSourceNodeIds(graph, where_clause, repo_config) {
|
|
110
|
+
const query_result = queryGraph(
|
|
111
|
+
graph,
|
|
112
|
+
where_clause,
|
|
113
|
+
repo_config ?? {
|
|
114
|
+
fields: {},
|
|
115
|
+
include: [],
|
|
116
|
+
queries: {},
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
if (query_result.diagnostics.length > 0) {
|
|
121
|
+
return {
|
|
122
|
+
diagnostics: query_result.diagnostics,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return new Set(query_result.nodes.map((graph_node) => graph_node.id));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @param {Map<string, Map<string, GraphNode>>} grouped_incoming
|
|
131
|
+
* @returns {Record<string, GraphNode[]>}
|
|
132
|
+
*/
|
|
133
|
+
function formatIncomingGroups(grouped_incoming) {
|
|
134
|
+
/** @type {Record<string, GraphNode[]>} */
|
|
135
|
+
const incoming = {};
|
|
136
|
+
|
|
137
|
+
for (const relation_name of [...grouped_incoming.keys()].sort(
|
|
138
|
+
compareStrings,
|
|
139
|
+
)) {
|
|
140
|
+
const relation_sources = /** @type {Map<string, GraphNode>} */ (
|
|
141
|
+
grouped_incoming.get(relation_name)
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
incoming[relation_name] = [...relation_sources.values()].sort(
|
|
145
|
+
compareGraphNodes,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return incoming;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @param {string} node_id
|
|
154
|
+
* @param {string} target_path
|
|
155
|
+
* @returns {GraphNode}
|
|
156
|
+
*/
|
|
157
|
+
function createFallbackTargetNode(node_id, target_path) {
|
|
158
|
+
return {
|
|
159
|
+
$class: 'document',
|
|
160
|
+
$id: node_id,
|
|
161
|
+
$path: target_path,
|
|
162
|
+
id: node_id,
|
|
163
|
+
path: target_path,
|
|
164
|
+
title: target_path,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @param {GraphNode} left_node
|
|
170
|
+
* @param {GraphNode} right_node
|
|
171
|
+
* @returns {number}
|
|
172
|
+
*/
|
|
173
|
+
function compareGraphNodes(left_node, right_node) {
|
|
174
|
+
return compareStrings(left_node.id, right_node.id);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* @param {string} left_value
|
|
179
|
+
* @param {string} right_value
|
|
180
|
+
* @returns {number}
|
|
181
|
+
*/
|
|
182
|
+
function compareStrings(left_value, right_value) {
|
|
183
|
+
return left_value.localeCompare(right_value, 'en');
|
|
184
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { OutputNodeItem } from './output-view.types.ts';
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { formatOutputNodeMetadataRows } from './format-output-metadata.js';
|
|
6
|
+
import { formatNodeHeader } from './format-node-header.js';
|
|
7
|
+
import { formatOutputItemBlock } from './format-output-item-block.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Layout grouped incoming references as plain text lines.
|
|
11
|
+
*
|
|
12
|
+
* @param {Record<string, OutputNodeItem[]>} incoming
|
|
13
|
+
* @param {{
|
|
14
|
+
* format_node_header?: (output_item: OutputNodeItem) => string,
|
|
15
|
+
* format_relation_header?: (relation_name: string, relation_count: number) => string,
|
|
16
|
+
* }} layout_options
|
|
17
|
+
* @returns {string[]}
|
|
18
|
+
*/
|
|
19
|
+
export function layoutIncomingReferenceLines(incoming, layout_options = {}) {
|
|
20
|
+
const format_node_header =
|
|
21
|
+
layout_options.format_node_header ?? defaultNodeHeaderFormatter;
|
|
22
|
+
const format_relation_header =
|
|
23
|
+
layout_options.format_relation_header ?? defaultRelationHeaderFormatter;
|
|
24
|
+
/** @type {string[]} */
|
|
25
|
+
const output_lines = [];
|
|
26
|
+
|
|
27
|
+
for (const relation_name of Object.keys(incoming)) {
|
|
28
|
+
if (output_lines.length > 0) {
|
|
29
|
+
output_lines.push('');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const relation_sources = incoming[relation_name];
|
|
33
|
+
|
|
34
|
+
output_lines.push(
|
|
35
|
+
format_relation_header(relation_name, relation_sources.length),
|
|
36
|
+
);
|
|
37
|
+
output_lines.push(
|
|
38
|
+
...layoutIncomingRelationSources(relation_sources, format_node_header),
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return output_lines;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {OutputNodeItem} output_item
|
|
47
|
+
* @returns {string}
|
|
48
|
+
*/
|
|
49
|
+
function defaultNodeHeaderFormatter(output_item) {
|
|
50
|
+
return formatNodeHeader(output_item);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {string} relation_name
|
|
55
|
+
* @param {number} relation_count
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
function defaultRelationHeaderFormatter(relation_name, relation_count) {
|
|
59
|
+
return `${relation_name} (${relation_count})`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {OutputNodeItem[]} relation_sources
|
|
64
|
+
* @param {(output_item: OutputNodeItem) => string} format_node_header
|
|
65
|
+
* @returns {string[]}
|
|
66
|
+
*/
|
|
67
|
+
function layoutIncomingRelationSources(relation_sources, format_node_header) {
|
|
68
|
+
/** @type {string[]} */
|
|
69
|
+
const output_lines = [];
|
|
70
|
+
|
|
71
|
+
for (const [item_index, output_item] of relation_sources.entries()) {
|
|
72
|
+
if (item_index > 0) {
|
|
73
|
+
output_lines.push('');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
output_lines.push(
|
|
77
|
+
...indentIncomingNodeBlock(
|
|
78
|
+
formatIncomingNodeBlock(output_item, format_node_header),
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return output_lines;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {OutputNodeItem} output_item
|
|
88
|
+
* @param {(output_item: OutputNodeItem) => string} format_node_header
|
|
89
|
+
* @returns {string}
|
|
90
|
+
*/
|
|
91
|
+
function formatIncomingNodeBlock(output_item, format_node_header) {
|
|
92
|
+
return formatOutputItemBlock({
|
|
93
|
+
header: format_node_header(output_item),
|
|
94
|
+
metadata_rows: formatOutputNodeMetadataRows(output_item),
|
|
95
|
+
title: output_item.title,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {string} block
|
|
101
|
+
* @returns {string[]}
|
|
102
|
+
*/
|
|
103
|
+
function indentIncomingNodeBlock(block) {
|
|
104
|
+
return block.split('\n').map((line) => (line.length > 0 ? ` ${line}` : ''));
|
|
105
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout compact incoming-summary lines for `show`.
|
|
3
|
+
*
|
|
4
|
+
* @param {Record<string, number>} incoming_summary
|
|
5
|
+
* @returns {string[]}
|
|
6
|
+
*/
|
|
7
|
+
export function layoutIncomingSummaryLines(incoming_summary) {
|
|
8
|
+
const relation_names = Object.keys(incoming_summary);
|
|
9
|
+
const output_lines = ['incoming refs:'];
|
|
10
|
+
|
|
11
|
+
if (relation_names.length === 0) {
|
|
12
|
+
output_lines.push(' none');
|
|
13
|
+
return output_lines;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
for (const relation_name of relation_names) {
|
|
17
|
+
output_lines.push(` ${relation_name}: ${incoming_summary[relation_name]}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return output_lines;
|
|
21
|
+
}
|
|
@@ -13,7 +13,7 @@ export interface OutputMetadataField {
|
|
|
13
13
|
}
|
|
14
14
|
export interface OutputViewSummary {
|
|
15
15
|
count: number;
|
|
16
|
-
kind: 'resolved_link_list' | 'result_list' | 'stored_query_list';
|
|
16
|
+
kind: 'incoming_reference_list' | 'resolved_link_list' | 'result_list' | 'stored_query_list';
|
|
17
17
|
}
|
|
18
18
|
export interface QueryOutputViewSummary extends OutputViewSummary {
|
|
19
19
|
kind: 'result_list';
|
|
@@ -67,13 +67,21 @@ export interface ShowOutputView {
|
|
|
67
67
|
command: 'show';
|
|
68
68
|
document?: OutputNodeItem;
|
|
69
69
|
hints: string[];
|
|
70
|
+
incoming_summary: Record<string, number>;
|
|
70
71
|
items: OutputResolvedLinkItem[];
|
|
71
72
|
path: string;
|
|
72
73
|
rendered_source: string;
|
|
73
74
|
source: string;
|
|
74
75
|
summary: OutputViewSummary;
|
|
75
76
|
}
|
|
76
|
-
export
|
|
77
|
+
export interface RefsOutputView {
|
|
78
|
+
command: 'refs';
|
|
79
|
+
hints: string[];
|
|
80
|
+
incoming: Record<string, OutputNodeItem[]>;
|
|
81
|
+
node: OutputNodeItem;
|
|
82
|
+
summary: OutputViewSummary;
|
|
83
|
+
}
|
|
84
|
+
export type OutputView = QueryOutputView | QueriesOutputView | RefsOutputView | ShowOutputView;
|
|
77
85
|
export interface ResolvedOutputMode {
|
|
78
86
|
color_enabled: boolean;
|
|
79
87
|
renderer_name: 'json' | 'plain' | 'rich';
|
package/lib/output-view.types.ts
CHANGED
|
@@ -19,7 +19,11 @@ export interface OutputMetadataField {
|
|
|
19
19
|
|
|
20
20
|
export interface OutputViewSummary {
|
|
21
21
|
count: number;
|
|
22
|
-
kind:
|
|
22
|
+
kind:
|
|
23
|
+
| 'incoming_reference_list'
|
|
24
|
+
| 'resolved_link_list'
|
|
25
|
+
| 'result_list'
|
|
26
|
+
| 'stored_query_list';
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
export interface QueryOutputViewSummary extends OutputViewSummary {
|
|
@@ -81,6 +85,7 @@ export interface ShowOutputView {
|
|
|
81
85
|
command: 'show';
|
|
82
86
|
document?: OutputNodeItem;
|
|
83
87
|
hints: string[];
|
|
88
|
+
incoming_summary: Record<string, number>;
|
|
84
89
|
items: OutputResolvedLinkItem[];
|
|
85
90
|
path: string;
|
|
86
91
|
rendered_source: string;
|
|
@@ -88,7 +93,19 @@ export interface ShowOutputView {
|
|
|
88
93
|
summary: OutputViewSummary;
|
|
89
94
|
}
|
|
90
95
|
|
|
91
|
-
export
|
|
96
|
+
export interface RefsOutputView {
|
|
97
|
+
command: 'refs';
|
|
98
|
+
hints: string[];
|
|
99
|
+
incoming: Record<string, OutputNodeItem[]>;
|
|
100
|
+
node: OutputNodeItem;
|
|
101
|
+
summary: OutputViewSummary;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type OutputView =
|
|
105
|
+
| QueryOutputView
|
|
106
|
+
| QueriesOutputView
|
|
107
|
+
| RefsOutputView
|
|
108
|
+
| ShowOutputView;
|
|
92
109
|
|
|
93
110
|
export interface ResolvedOutputMode {
|
|
94
111
|
color_enabled: boolean;
|
|
@@ -160,6 +160,10 @@ export function buildCommandArguments(
|
|
|
160
160
|
return ['--where', parsed_values.where];
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
if (command_name === 'refs' && parsed_values.where !== undefined) {
|
|
164
|
+
return [command_positionals[0], '--where', parsed_values.where];
|
|
165
|
+
}
|
|
166
|
+
|
|
163
167
|
return command_positionals;
|
|
164
168
|
}
|
|
165
169
|
|
|
@@ -305,7 +309,7 @@ function findMissingOptionValue(option_tokens) {
|
|
|
305
309
|
for (const token of option_tokens) {
|
|
306
310
|
if (token.name === 'where' && typeof token.value !== 'string') {
|
|
307
311
|
return {
|
|
308
|
-
argument_label:
|
|
312
|
+
argument_label: "<name> or --where '<clause>'",
|
|
309
313
|
code: 'missing_required_argument',
|
|
310
314
|
command_name: 'query',
|
|
311
315
|
};
|
|
@@ -401,11 +405,14 @@ function validateCommandPositionals(command_name, command_positionals) {
|
|
|
401
405
|
};
|
|
402
406
|
}
|
|
403
407
|
|
|
404
|
-
if (
|
|
408
|
+
if (
|
|
409
|
+
(command_name === 'refs' || command_name === 'show') &&
|
|
410
|
+
command_definition.missing_argument_label
|
|
411
|
+
) {
|
|
405
412
|
return {
|
|
406
413
|
argument_label: command_definition.missing_argument_label,
|
|
407
414
|
code: 'missing_required_argument',
|
|
408
|
-
command_name
|
|
415
|
+
command_name,
|
|
409
416
|
};
|
|
410
417
|
}
|
|
411
418
|
|
|
@@ -422,7 +429,7 @@ function validateCommandPositionals(command_name, command_positionals) {
|
|
|
422
429
|
|
|
423
430
|
if (command_name === 'query' && command_positionals.length === 0) {
|
|
424
431
|
return {
|
|
425
|
-
argument_label:
|
|
432
|
+
argument_label: "<name> or --where '<clause>'",
|
|
426
433
|
code: 'missing_required_argument',
|
|
427
434
|
command_name: 'query',
|
|
428
435
|
};
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
export type CliCommandName =
|
|
1
|
+
export type CliCommandName =
|
|
2
|
+
| 'check'
|
|
3
|
+
| 'fields'
|
|
4
|
+
| 'query'
|
|
5
|
+
| 'queries'
|
|
6
|
+
| 'refs'
|
|
7
|
+
| 'show';
|
|
2
8
|
export type CliHelpTopicName = 'query-language';
|
|
3
9
|
export type CliHelpTargetKind = 'root' | 'command' | 'topic';
|
|
4
10
|
|
|
@@ -34,7 +40,7 @@ export type CliParseError =
|
|
|
34
40
|
| {
|
|
35
41
|
code: 'missing_required_argument';
|
|
36
42
|
argument_label: string;
|
|
37
|
-
command_name: 'query' | 'show';
|
|
43
|
+
command_name: 'query' | 'refs' | 'show';
|
|
38
44
|
}
|
|
39
45
|
| {
|
|
40
46
|
code: 'option_not_valid_for_command';
|
package/lib/patram-cli.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
writeRenderedCommandOutput,
|
|
13
13
|
} from './command-output.js';
|
|
14
14
|
import { discoverFields } from './discover-fields.js';
|
|
15
|
+
import { inspectReverseReferences } from './inspect-reverse-references.js';
|
|
15
16
|
import { listRepoFiles } from './list-source-files.js';
|
|
16
17
|
import { listQueries } from './list-queries.js';
|
|
17
18
|
import { loadPatramConfig } from './load-patram-config.js';
|
|
@@ -36,6 +37,7 @@ import {
|
|
|
36
37
|
} from './resolve-check-target.js';
|
|
37
38
|
import {
|
|
38
39
|
createOutputView,
|
|
40
|
+
createRefsOutputView,
|
|
39
41
|
createShowOutputView,
|
|
40
42
|
} from './render-output-view.js';
|
|
41
43
|
import { renderFieldDiscovery } from './render-field-discovery.js';
|
|
@@ -54,6 +56,7 @@ import { loadShowOutput } from './show-document.js';
|
|
|
54
56
|
* Implements Command: ../docs/reference/commands/check.md
|
|
55
57
|
* Implements Command: ../docs/reference/commands/query.md
|
|
56
58
|
* Implements Command: ../docs/reference/commands/queries.md
|
|
59
|
+
* Implements Command: ../docs/reference/commands/refs.md
|
|
57
60
|
* Implements Command: ../docs/reference/commands/show.md
|
|
58
61
|
* Tracked in: ../docs/plans/v0/source-anchor-dogfooding.md
|
|
59
62
|
* Decided by: ../docs/decisions/cli-output-architecture.md
|
|
@@ -105,6 +108,10 @@ export async function main(cli_arguments, io_context) {
|
|
|
105
108
|
return runQueriesCommand(parsed_command, io_context);
|
|
106
109
|
}
|
|
107
110
|
|
|
111
|
+
if (parsed_command.command_name === 'refs') {
|
|
112
|
+
return runRefsCommand(parsed_command, io_context);
|
|
113
|
+
}
|
|
114
|
+
|
|
108
115
|
if (parsed_command.command_name === 'show') {
|
|
109
116
|
return runShowCommand(parsed_command, io_context);
|
|
110
117
|
}
|
|
@@ -371,6 +378,52 @@ async function runQueriesCommand(parsed_command, io_context) {
|
|
|
371
378
|
return 0;
|
|
372
379
|
}
|
|
373
380
|
|
|
381
|
+
/**
|
|
382
|
+
* @param {ParsedCliCommandRequest} parsed_command
|
|
383
|
+
* @param {{ stderr: { write(chunk: string): boolean }, stdout: { isTTY?: boolean, write(chunk: string): boolean }, write_paged_output?: (output_text: string) => Promise<void> }} io_context
|
|
384
|
+
* @returns {Promise<number>}
|
|
385
|
+
*/
|
|
386
|
+
async function runRefsCommand(parsed_command, io_context) {
|
|
387
|
+
const project_graph_result = await loadProjectGraph(process.cwd());
|
|
388
|
+
|
|
389
|
+
if (project_graph_result.diagnostics.length > 0) {
|
|
390
|
+
writeDiagnostics(io_context.stderr, project_graph_result.diagnostics);
|
|
391
|
+
|
|
392
|
+
return 1;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const refs_output = inspectReverseReferences(
|
|
396
|
+
project_graph_result.graph,
|
|
397
|
+
parsed_command.command_arguments[0],
|
|
398
|
+
project_graph_result.config,
|
|
399
|
+
resolveRefsWhereClause(parsed_command.command_arguments),
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
if (refs_output.diagnostics.length > 0) {
|
|
403
|
+
io_context.stderr.write(
|
|
404
|
+
renderInvalidWhereDiagnostic(refs_output.diagnostics[0]),
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
return 1;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const derived_summary_evaluator = createDerivedSummaryEvaluator(
|
|
411
|
+
project_graph_result.config,
|
|
412
|
+
project_graph_result.graph,
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
await writeCommandOutput(
|
|
416
|
+
io_context,
|
|
417
|
+
parsed_command,
|
|
418
|
+
createRefsOutputView(refs_output, {
|
|
419
|
+
derived_summary_evaluator,
|
|
420
|
+
repo_config: project_graph_result.config,
|
|
421
|
+
}),
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
return 0;
|
|
425
|
+
}
|
|
426
|
+
|
|
374
427
|
/**
|
|
375
428
|
* @param {ParsedCliCommandRequest} parsed_command
|
|
376
429
|
* @param {{ stderr: { write(chunk: string): boolean }, stdout: { isTTY?: boolean, write(chunk: string): boolean }, write_paged_output?: (output_text: string) => Promise<void> }} io_context
|
|
@@ -441,7 +494,7 @@ function createQueryOutputOptions(parsed_command, query_result, use_pager) {
|
|
|
441
494
|
const offset = parsed_command.query_offset ?? 0;
|
|
442
495
|
|
|
443
496
|
if (query_result.total_count === 0) {
|
|
444
|
-
hints.push(
|
|
497
|
+
hints.push("Try: patram query --where '$class=task'");
|
|
445
498
|
}
|
|
446
499
|
|
|
447
500
|
if (
|
|
@@ -500,6 +553,18 @@ function createQueryExecutionOptions(parsed_command, use_pager) {
|
|
|
500
553
|
};
|
|
501
554
|
}
|
|
502
555
|
|
|
556
|
+
/**
|
|
557
|
+
* @param {string[]} command_arguments
|
|
558
|
+
* @returns {string | undefined}
|
|
559
|
+
*/
|
|
560
|
+
function resolveRefsWhereClause(command_arguments) {
|
|
561
|
+
if (command_arguments[1] !== '--where') {
|
|
562
|
+
return undefined;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return command_arguments.slice(2).join(' ').trim();
|
|
566
|
+
}
|
|
567
|
+
|
|
503
568
|
/**
|
|
504
569
|
* @param {import('./load-patram-config.types.ts').PatramDiagnostic} diagnostic
|
|
505
570
|
* @returns {string}
|
package/lib/render-cli-help.js
CHANGED
|
@@ -308,7 +308,7 @@ function renderInvalidCommandOptionError(command_name, invalid_token) {
|
|
|
308
308
|
}
|
|
309
309
|
|
|
310
310
|
/**
|
|
311
|
-
* @param {'query' | 'show'} command_name
|
|
311
|
+
* @param {'query' | 'refs' | 'show'} command_name
|
|
312
312
|
* @param {string} argument_label
|
|
313
313
|
* @returns {string}
|
|
314
314
|
*/
|
|
@@ -10,54 +10,113 @@
|
|
|
10
10
|
*/
|
|
11
11
|
export function renderJsonOutput(output_view) {
|
|
12
12
|
if (output_view.command === 'query') {
|
|
13
|
-
return
|
|
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`;
|
|
13
|
+
return renderJsonQueryOutput(output_view);
|
|
27
14
|
}
|
|
28
15
|
|
|
29
16
|
if (output_view.command === 'queries') {
|
|
30
|
-
return
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
2,
|
|
36
|
-
)}\n`;
|
|
17
|
+
return renderJsonQueriesOutput(output_view.items);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (output_view.command === 'refs') {
|
|
21
|
+
return renderJsonRefsOutput(output_view);
|
|
37
22
|
}
|
|
38
23
|
|
|
39
24
|
if (output_view.command === 'show') {
|
|
40
|
-
return
|
|
41
|
-
{
|
|
42
|
-
document: output_view.document
|
|
43
|
-
? formatJsonShowDocument(output_view.document)
|
|
44
|
-
: undefined,
|
|
45
|
-
source: output_view.source,
|
|
46
|
-
resolved_links: output_view.items.map(formatJsonResolvedLink),
|
|
47
|
-
},
|
|
48
|
-
null,
|
|
49
|
-
2,
|
|
50
|
-
)}\n`;
|
|
25
|
+
return renderJsonShowOutput(output_view);
|
|
51
26
|
}
|
|
52
27
|
|
|
53
28
|
throw new Error('Unsupported output view command.');
|
|
54
29
|
}
|
|
55
30
|
|
|
31
|
+
/**
|
|
32
|
+
* @param {Extract<OutputView, { command: 'query' }>} output_view
|
|
33
|
+
* @returns {string}
|
|
34
|
+
*/
|
|
35
|
+
function renderJsonQueryOutput(output_view) {
|
|
36
|
+
return `${JSON.stringify(
|
|
37
|
+
{
|
|
38
|
+
results: output_view.items.map(formatJsonQueryItem),
|
|
39
|
+
summary: {
|
|
40
|
+
shown_count: output_view.summary.count,
|
|
41
|
+
total_count: output_view.summary.total_count,
|
|
42
|
+
offset: output_view.summary.offset,
|
|
43
|
+
limit: output_view.summary.limit,
|
|
44
|
+
},
|
|
45
|
+
hints: output_view.hints,
|
|
46
|
+
},
|
|
47
|
+
null,
|
|
48
|
+
2,
|
|
49
|
+
)}\n`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {OutputStoredQueryItem[]} output_items
|
|
54
|
+
* @returns {string}
|
|
55
|
+
*/
|
|
56
|
+
function renderJsonQueriesOutput(output_items) {
|
|
57
|
+
return `${JSON.stringify(
|
|
58
|
+
{
|
|
59
|
+
queries: output_items.map(formatJsonStoredQuery),
|
|
60
|
+
},
|
|
61
|
+
null,
|
|
62
|
+
2,
|
|
63
|
+
)}\n`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {Extract<OutputView, { command: 'refs' }>} output_view
|
|
68
|
+
* @returns {string}
|
|
69
|
+
*/
|
|
70
|
+
function renderJsonRefsOutput(output_view) {
|
|
71
|
+
return `${JSON.stringify(
|
|
72
|
+
{
|
|
73
|
+
node: formatJsonNodeItem(output_view.node),
|
|
74
|
+
incoming: Object.fromEntries(
|
|
75
|
+
Object.entries(output_view.incoming).map(
|
|
76
|
+
([relation_name, output_items]) => [
|
|
77
|
+
relation_name,
|
|
78
|
+
output_items.map(formatJsonNodeItem),
|
|
79
|
+
],
|
|
80
|
+
),
|
|
81
|
+
),
|
|
82
|
+
},
|
|
83
|
+
null,
|
|
84
|
+
2,
|
|
85
|
+
)}\n`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {Extract<OutputView, { command: 'show' }>} output_view
|
|
90
|
+
* @returns {string}
|
|
91
|
+
*/
|
|
92
|
+
function renderJsonShowOutput(output_view) {
|
|
93
|
+
return `${JSON.stringify(
|
|
94
|
+
{
|
|
95
|
+
document: output_view.document
|
|
96
|
+
? formatJsonNodeItem(output_view.document)
|
|
97
|
+
: undefined,
|
|
98
|
+
incoming_summary: output_view.incoming_summary,
|
|
99
|
+
source: output_view.source,
|
|
100
|
+
resolved_links: output_view.items.map(formatJsonResolvedLink),
|
|
101
|
+
},
|
|
102
|
+
null,
|
|
103
|
+
2,
|
|
104
|
+
)}\n`;
|
|
105
|
+
}
|
|
106
|
+
|
|
56
107
|
/**
|
|
57
108
|
* @param {OutputNodeItem} output_item
|
|
58
109
|
* @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
110
|
*/
|
|
60
111
|
function formatJsonQueryItem(output_item) {
|
|
112
|
+
return formatJsonNodeItem(output_item);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @param {OutputNodeItem} output_item
|
|
117
|
+
* @returns {{ '$class': string, '$id': string, '$path'?: string, derived?: Record<string, boolean | number | string | null>, derived_summary?: string, fields: Record<string, string | string[]>, title: string }}
|
|
118
|
+
*/
|
|
119
|
+
function formatJsonNodeItem(output_item) {
|
|
61
120
|
/** @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
121
|
const query_item = {
|
|
63
122
|
$class: output_item.node_kind,
|
|
@@ -128,33 +187,3 @@ function formatJsonResolvedLink(output_item) {
|
|
|
128
187
|
|
|
129
188
|
return resolved_link;
|
|
130
189
|
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* @param {OutputNodeItem} output_item
|
|
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 }}
|
|
135
|
-
*/
|
|
136
|
-
function formatJsonShowDocument(output_item) {
|
|
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 }} */
|
|
138
|
-
const document_summary = {
|
|
139
|
-
$class: output_item.node_kind,
|
|
140
|
-
$id: output_item.id,
|
|
141
|
-
fields: output_item.fields,
|
|
142
|
-
title: output_item.title,
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
if (output_item.path) {
|
|
146
|
-
document_summary.$path = output_item.path;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (output_item.derived_summary) {
|
|
150
|
-
document_summary.derived_summary = output_item.derived_summary.name;
|
|
151
|
-
document_summary.derived = Object.fromEntries(
|
|
152
|
-
output_item.derived_summary.fields.map((field) => [
|
|
153
|
-
field.name,
|
|
154
|
-
field.value,
|
|
155
|
-
]),
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return document_summary;
|
|
160
|
-
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @import { DerivedSummaryEvaluator } from './derived-summary.js';
|
|
4
4
|
* @import { PatramRepoConfig } from './load-patram-config.types.ts';
|
|
5
5
|
* @import { ParsedCliArguments } from './parse-cli-arguments.types.ts';
|
|
6
|
-
* @import { OutputDerivedSummary, OutputMetadataField, OutputNodeItem, OutputResolvedLinkItem, OutputResolvedLinkTarget, OutputStoredQueryItem, OutputView, ResolvedOutputMode, ShowOutputView } from './output-view.types.ts';
|
|
6
|
+
* @import { OutputDerivedSummary, OutputMetadataField, OutputNodeItem, OutputResolvedLinkItem, OutputResolvedLinkTarget, OutputStoredQueryItem, OutputView, RefsOutputView, ResolvedOutputMode, ShowOutputView } from './output-view.types.ts';
|
|
7
7
|
*/
|
|
8
8
|
/* eslint-disable max-lines */
|
|
9
9
|
|
|
@@ -55,7 +55,7 @@ export function createOutputView(command_name, command_items, command_options) {
|
|
|
55
55
|
/**
|
|
56
56
|
* Create a shared output view for the show command.
|
|
57
57
|
*
|
|
58
|
-
* @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
|
|
58
|
+
* @param {{ incoming_summary: Record<string, number>, 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
|
|
59
59
|
* @param {{ derived_summary_evaluator?: DerivedSummaryEvaluator, document_node_ids?: BuildGraphResult['document_node_ids'], graph_nodes?: BuildGraphResult['nodes'], repo_config?: PatramRepoConfig }=} command_options
|
|
60
60
|
* @returns {ShowOutputView}
|
|
61
61
|
*/
|
|
@@ -78,6 +78,7 @@ export function createShowOutputView(show_output, command_options = {}) {
|
|
|
78
78
|
)
|
|
79
79
|
: undefined,
|
|
80
80
|
hints: [],
|
|
81
|
+
incoming_summary: show_output.incoming_summary,
|
|
81
82
|
items: show_output.resolved_links.map((resolved_link) =>
|
|
82
83
|
createResolvedLinkOutputItem(resolved_link, command_options),
|
|
83
84
|
),
|
|
@@ -91,6 +92,46 @@ export function createShowOutputView(show_output, command_options = {}) {
|
|
|
91
92
|
};
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Create a shared output view for the refs command.
|
|
97
|
+
*
|
|
98
|
+
* @param {{ incoming: Record<string, GraphNode[]>, node: GraphNode }} refs_output
|
|
99
|
+
* @param {{ derived_summary_evaluator?: DerivedSummaryEvaluator, repo_config?: PatramRepoConfig }=} command_options
|
|
100
|
+
* @returns {RefsOutputView}
|
|
101
|
+
*/
|
|
102
|
+
export function createRefsOutputView(refs_output, command_options = {}) {
|
|
103
|
+
/** @type {Record<string, OutputNodeItem[]>} */
|
|
104
|
+
const incoming = {};
|
|
105
|
+
|
|
106
|
+
for (const relation_name of Object.keys(refs_output.incoming)) {
|
|
107
|
+
incoming[relation_name] = refs_output.incoming[relation_name].map(
|
|
108
|
+
(graph_node) =>
|
|
109
|
+
createOutputNodeItem(
|
|
110
|
+
graph_node,
|
|
111
|
+
command_options.derived_summary_evaluator?.evaluate(graph_node) ??
|
|
112
|
+
null,
|
|
113
|
+
command_options.repo_config?.fields ?? {},
|
|
114
|
+
),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
command: 'refs',
|
|
120
|
+
hints: [],
|
|
121
|
+
incoming,
|
|
122
|
+
node: createOutputNodeItem(
|
|
123
|
+
refs_output.node,
|
|
124
|
+
command_options.derived_summary_evaluator?.evaluate(refs_output.node) ??
|
|
125
|
+
null,
|
|
126
|
+
command_options.repo_config?.fields ?? {},
|
|
127
|
+
),
|
|
128
|
+
summary: {
|
|
129
|
+
count: countIncomingReferenceItems(incoming),
|
|
130
|
+
kind: 'incoming_reference_list',
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
94
135
|
/**
|
|
95
136
|
* Render one shared output view through the resolved renderer.
|
|
96
137
|
*
|
|
@@ -130,7 +171,7 @@ function createQueryOutputView(graph_nodes, command_options = {}) {
|
|
|
130
171
|
command: 'query',
|
|
131
172
|
hints:
|
|
132
173
|
command_options.hints ??
|
|
133
|
-
(total_count === 0 ? [
|
|
174
|
+
(total_count === 0 ? ["Try: patram query --where '$class=task'"] : []),
|
|
134
175
|
items: graph_nodes.map((graph_node) =>
|
|
135
176
|
createOutputNodeItem(
|
|
136
177
|
graph_node,
|
|
@@ -168,6 +209,20 @@ function createStoredQueriesOutputView(stored_queries) {
|
|
|
168
209
|
};
|
|
169
210
|
}
|
|
170
211
|
|
|
212
|
+
/**
|
|
213
|
+
* @param {Record<string, OutputNodeItem[]>} incoming
|
|
214
|
+
* @returns {number}
|
|
215
|
+
*/
|
|
216
|
+
function countIncomingReferenceItems(incoming) {
|
|
217
|
+
let count = 0;
|
|
218
|
+
|
|
219
|
+
for (const output_items of Object.values(incoming)) {
|
|
220
|
+
count += output_items.length;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return count;
|
|
224
|
+
}
|
|
225
|
+
|
|
171
226
|
/**
|
|
172
227
|
* @param {GraphNode} graph_node
|
|
173
228
|
* @param {OutputDerivedSummary | null} derived_summary
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @import { OutputNodeItem, OutputResolvedLinkItem, OutputStoredQueryItem, OutputView, QueryOutputView, ShowOutputView } from './output-view.types.ts';
|
|
2
|
+
* @import { OutputNodeItem, OutputResolvedLinkItem, OutputStoredQueryItem, OutputView, QueryOutputView, RefsOutputView, ShowOutputView } from './output-view.types.ts';
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import {
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
} from './format-output-metadata.js';
|
|
9
9
|
import { formatNodeHeader } from './format-node-header.js';
|
|
10
10
|
import { formatOutputItemBlock } from './format-output-item-block.js';
|
|
11
|
+
import { layoutIncomingReferenceLines } from './layout-incoming-references.js';
|
|
12
|
+
import { layoutIncomingSummaryLines } from './layout-incoming-summary-lines.js';
|
|
11
13
|
import { layoutStoredQueries } from './layout-stored-queries.js';
|
|
12
14
|
|
|
13
15
|
/**
|
|
@@ -25,6 +27,10 @@ export function renderPlainOutput(output_view) {
|
|
|
25
27
|
return renderPlainStoredQueries(output_view.items);
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
if (output_view.command === 'refs') {
|
|
31
|
+
return renderPlainRefsOutput(output_view);
|
|
32
|
+
}
|
|
33
|
+
|
|
28
34
|
if (output_view.command === 'show') {
|
|
29
35
|
return renderPlainShowOutput(output_view);
|
|
30
36
|
}
|
|
@@ -85,8 +91,13 @@ function renderPlainShowOutput(output_view) {
|
|
|
85
91
|
const document_summary = output_view.document
|
|
86
92
|
? formatPlainNodeItem(output_view.document)
|
|
87
93
|
: '';
|
|
94
|
+
const incoming_summary = renderPlainIncomingSummary(output_view);
|
|
88
95
|
|
|
89
|
-
if (
|
|
96
|
+
if (
|
|
97
|
+
document_summary.length === 0 &&
|
|
98
|
+
output_view.items.length === 0 &&
|
|
99
|
+
incoming_summary.length === 0
|
|
100
|
+
) {
|
|
90
101
|
return `${rendered_source}\n`;
|
|
91
102
|
}
|
|
92
103
|
|
|
@@ -99,7 +110,28 @@ function renderPlainShowOutput(output_view) {
|
|
|
99
110
|
|
|
100
111
|
summary_items.push(...output_view.items.map(formatPlainResolvedLinkItem));
|
|
101
112
|
|
|
102
|
-
|
|
113
|
+
const summary_blocks = summary_items.filter((summary_item) => summary_item);
|
|
114
|
+
|
|
115
|
+
if (incoming_summary.length > 0) {
|
|
116
|
+
summary_blocks.push(incoming_summary);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return `${rendered_source}\n\n----------------\n${summary_blocks.join('\n\n')}\n`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @param {RefsOutputView} output_view
|
|
124
|
+
* @returns {string}
|
|
125
|
+
*/
|
|
126
|
+
function renderPlainRefsOutput(output_view) {
|
|
127
|
+
const node_summary = formatPlainNodeItem(output_view.node);
|
|
128
|
+
const output_lines = layoutIncomingReferenceLines(output_view.incoming);
|
|
129
|
+
|
|
130
|
+
if (output_lines.length === 0) {
|
|
131
|
+
return `${node_summary}\n\nNo incoming references.\n`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return `${node_summary}\n\n${output_lines.join('\n')}\n`;
|
|
103
135
|
}
|
|
104
136
|
|
|
105
137
|
/**
|
|
@@ -135,6 +167,17 @@ function formatPlainResolvedLinkItem(output_item) {
|
|
|
135
167
|
});
|
|
136
168
|
}
|
|
137
169
|
|
|
170
|
+
/**
|
|
171
|
+
* @param {ShowOutputView} output_view
|
|
172
|
+
* @returns {string}
|
|
173
|
+
*/
|
|
174
|
+
function renderPlainIncomingSummary(output_view) {
|
|
175
|
+
const output_lines = layoutIncomingSummaryLines(output_view.incoming_summary);
|
|
176
|
+
output_lines.push('', `Hint: patram refs ${output_view.path}`);
|
|
177
|
+
|
|
178
|
+
return output_lines.join('\n');
|
|
179
|
+
}
|
|
180
|
+
|
|
138
181
|
/**
|
|
139
182
|
* @param {string} value
|
|
140
183
|
* @returns {string}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @import { CliColorMode } from './parse-cli-arguments.types.ts';
|
|
3
|
-
* @import { OutputNodeItem, OutputResolvedLinkItem, OutputStoredQueryItem, OutputView, QueryOutputView, ShowOutputView } from './output-view.types.ts';
|
|
3
|
+
* @import { OutputNodeItem, OutputResolvedLinkItem, OutputStoredQueryItem, OutputView, QueryOutputView, RefsOutputView, ShowOutputView } from './output-view.types.ts';
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { Ansis } from 'ansis';
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
} from './format-output-metadata.js';
|
|
12
12
|
import { formatNodeHeader } from './format-node-header.js';
|
|
13
13
|
import { formatOutputItemBlock } from './format-output-item-block.js';
|
|
14
|
+
import { layoutIncomingReferenceLines } from './layout-incoming-references.js';
|
|
15
|
+
import { layoutIncomingSummaryLines } from './layout-incoming-summary-lines.js';
|
|
14
16
|
import { layoutStoredQueries } from './layout-stored-queries.js';
|
|
15
17
|
import { renderRichSource } from './render-rich-source.js';
|
|
16
18
|
|
|
@@ -34,6 +36,9 @@ export async function renderRichOutput(output_view, render_options) {
|
|
|
34
36
|
return renderRichStoredQueries(output_view.items, ansi);
|
|
35
37
|
}
|
|
36
38
|
|
|
39
|
+
if (output_view.command === 'refs') {
|
|
40
|
+
return renderRichRefsOutput(output_view, ansi);
|
|
41
|
+
}
|
|
37
42
|
if (output_view.command === 'show') {
|
|
38
43
|
return renderRichShowOutput(output_view, render_options, ansi);
|
|
39
44
|
}
|
|
@@ -105,10 +110,7 @@ async function renderRichShowOutput(output_view, render_options, ansi) {
|
|
|
105
110
|
const document_summary = output_view.document
|
|
106
111
|
? formatRichNodeItem(output_view.document, ansi)
|
|
107
112
|
: '';
|
|
108
|
-
|
|
109
|
-
if (document_summary.length === 0 && output_view.items.length === 0) {
|
|
110
|
-
return `${rendered_source}\n`;
|
|
111
|
-
}
|
|
113
|
+
const incoming_summary = renderRichIncomingSummary(output_view, ansi);
|
|
112
114
|
|
|
113
115
|
/** @type {string[]} */
|
|
114
116
|
const summary_items = [];
|
|
@@ -121,9 +123,34 @@ async function renderRichShowOutput(output_view, render_options, ansi) {
|
|
|
121
123
|
...output_view.items.map((item) => formatRichResolvedLinkItem(item, ansi)),
|
|
122
124
|
);
|
|
123
125
|
|
|
126
|
+
if (incoming_summary.length > 0) {
|
|
127
|
+
summary_items.push(incoming_summary);
|
|
128
|
+
}
|
|
129
|
+
|
|
124
130
|
return `${rendered_source}\n\n${ansi.gray(FULL_WIDTH_DIVIDER)}\n\n${summary_items.join('\n\n')}\n`;
|
|
125
131
|
}
|
|
126
132
|
|
|
133
|
+
/**
|
|
134
|
+
* @param {RefsOutputView} output_view
|
|
135
|
+
* @param {Ansis} ansi
|
|
136
|
+
* @returns {string}
|
|
137
|
+
*/
|
|
138
|
+
function renderRichRefsOutput(output_view, ansi) {
|
|
139
|
+
const node_summary = formatRichNodeItem(output_view.node, ansi);
|
|
140
|
+
const output_lines = layoutIncomingReferenceLines(output_view.incoming, {
|
|
141
|
+
format_node_header(output_item) {
|
|
142
|
+
return ansi.green(formatNodeHeader(output_item));
|
|
143
|
+
},
|
|
144
|
+
format_relation_header(relation_name, relation_count) {
|
|
145
|
+
return `${ansi.bold(relation_name)} ${ansi.gray(`(${relation_count})`)}`;
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return output_lines.length === 0
|
|
150
|
+
? `${node_summary}\n\n${ansi.yellow('No incoming references.')}\n`
|
|
151
|
+
: `${node_summary}\n\n${output_lines.join('\n')}\n`;
|
|
152
|
+
}
|
|
153
|
+
|
|
127
154
|
/**
|
|
128
155
|
* @param {OutputNodeItem} output_item
|
|
129
156
|
* @param {Ansis} ansi
|
|
@@ -162,6 +189,24 @@ function formatRichResolvedLinkItem(output_item, ansi) {
|
|
|
162
189
|
});
|
|
163
190
|
}
|
|
164
191
|
|
|
192
|
+
/**
|
|
193
|
+
* @param {ShowOutputView} output_view
|
|
194
|
+
* @param {Ansis} ansi
|
|
195
|
+
* @returns {string}
|
|
196
|
+
*/
|
|
197
|
+
function renderRichIncomingSummary(output_view, ansi) {
|
|
198
|
+
const output_lines = layoutIncomingSummaryLines(output_view.incoming_summary);
|
|
199
|
+
output_lines[0] = ansi.bold(output_lines[0]);
|
|
200
|
+
|
|
201
|
+
if (Object.keys(output_view.incoming_summary).length === 0) {
|
|
202
|
+
output_lines[1] = ` ${ansi.gray('none')}`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
output_lines.push('', ansi.gray(`Hint: patram refs ${output_view.path}`));
|
|
206
|
+
|
|
207
|
+
return output_lines.join('\n');
|
|
208
|
+
}
|
|
209
|
+
|
|
165
210
|
/**
|
|
166
211
|
* @param {boolean} color_enabled
|
|
167
212
|
* @returns {Ansis}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared reverse-reference graph fixtures for tests.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function createDecisionNode() {
|
|
6
|
+
return {
|
|
7
|
+
$class: 'decision',
|
|
8
|
+
$id: 'decision:query-language',
|
|
9
|
+
$path: 'docs/decisions/query-language.md',
|
|
10
|
+
id: 'decision:query-language',
|
|
11
|
+
path: 'docs/decisions/query-language.md',
|
|
12
|
+
status: 'accepted',
|
|
13
|
+
title: 'Query Language',
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createReconcileNode() {
|
|
18
|
+
return {
|
|
19
|
+
$class: 'document',
|
|
20
|
+
$id: 'doc:lib/reconcile.js',
|
|
21
|
+
$path: 'lib/reconcile.js',
|
|
22
|
+
id: 'doc:lib/reconcile.js',
|
|
23
|
+
path: 'lib/reconcile.js',
|
|
24
|
+
title: 'Reconciler entrypoint.',
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createResumeNode() {
|
|
29
|
+
return {
|
|
30
|
+
$class: 'document',
|
|
31
|
+
$id: 'doc:lib/resume.js',
|
|
32
|
+
$path: 'lib/resume.js',
|
|
33
|
+
id: 'doc:lib/resume.js',
|
|
34
|
+
path: 'lib/resume.js',
|
|
35
|
+
title: 'Resume entrypoint.',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createTaskNode() {
|
|
40
|
+
return {
|
|
41
|
+
$class: 'task',
|
|
42
|
+
$id: 'task:reverse-reference-inspection',
|
|
43
|
+
$path: 'docs/tasks/v0/reverse-reference-inspection.md',
|
|
44
|
+
id: 'task:reverse-reference-inspection',
|
|
45
|
+
path: 'docs/tasks/v0/reverse-reference-inspection.md',
|
|
46
|
+
status: 'ready',
|
|
47
|
+
title: 'Implement reverse reference inspection',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {string} edge_id
|
|
53
|
+
* @param {string} from_id
|
|
54
|
+
* @param {string} origin_path
|
|
55
|
+
* @param {string} relation_name
|
|
56
|
+
* @param {string} to_id
|
|
57
|
+
*/
|
|
58
|
+
export function createGraphEdge(
|
|
59
|
+
edge_id,
|
|
60
|
+
from_id,
|
|
61
|
+
origin_path,
|
|
62
|
+
relation_name,
|
|
63
|
+
to_id,
|
|
64
|
+
) {
|
|
65
|
+
return {
|
|
66
|
+
from: from_id,
|
|
67
|
+
id: edge_id,
|
|
68
|
+
origin: {
|
|
69
|
+
column: 1,
|
|
70
|
+
line: 1,
|
|
71
|
+
path: origin_path,
|
|
72
|
+
},
|
|
73
|
+
relation: relation_name,
|
|
74
|
+
to: to_id,
|
|
75
|
+
};
|
|
76
|
+
}
|
package/lib/show-document.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* eslint-disable max-lines */
|
|
2
2
|
/**
|
|
3
|
-
* @import { GraphNode } from './build-graph.types.ts';
|
|
3
|
+
* @import { BuildGraphResult, GraphNode } from './build-graph.types.ts';
|
|
4
4
|
* @import { PatramClaim } from './parse-claims.types.ts';
|
|
5
5
|
* @import { PatramDiagnostic } from './load-patram-config.types.ts';
|
|
6
6
|
*/
|
|
@@ -9,6 +9,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
9
9
|
import { posix, relative, resolve } from 'node:path';
|
|
10
10
|
|
|
11
11
|
import { resolveDocumentNodeId } from './build-graph-identity.js';
|
|
12
|
+
import { inspectReverseReferences } from './inspect-reverse-references.js';
|
|
12
13
|
import { parseSourceFile } from './parse-claims.js';
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -30,11 +31,12 @@ import { parseSourceFile } from './parse-claims.js';
|
|
|
30
31
|
/**
|
|
31
32
|
* @param {string} requested_file_path
|
|
32
33
|
* @param {string} project_directory
|
|
33
|
-
* @param {
|
|
34
|
+
* @param {BuildGraphResult} graph
|
|
34
35
|
* @returns {Promise<
|
|
35
36
|
* | {
|
|
36
37
|
* success: true;
|
|
37
38
|
* value: {
|
|
39
|
+
* incoming_summary: Record<string, number>;
|
|
38
40
|
* path: string;
|
|
39
41
|
* rendered_source: string;
|
|
40
42
|
* resolved_links: Array<{
|
|
@@ -93,6 +95,7 @@ export async function loadShowOutput(
|
|
|
93
95
|
source_file_path,
|
|
94
96
|
source_text,
|
|
95
97
|
parse_result.claims,
|
|
98
|
+
graph,
|
|
96
99
|
graph.document_node_ids,
|
|
97
100
|
graph.nodes,
|
|
98
101
|
),
|
|
@@ -103,14 +106,16 @@ export async function loadShowOutput(
|
|
|
103
106
|
* @param {string} source_file_path
|
|
104
107
|
* @param {string} source_text
|
|
105
108
|
* @param {PatramClaim[]} claims
|
|
109
|
+
* @param {BuildGraphResult} graph
|
|
106
110
|
* @param {import('./build-graph.types.ts').BuildGraphResult['document_node_ids']} document_node_ids
|
|
107
111
|
* @param {Record<string, GraphNode>} graph_nodes
|
|
108
|
-
* @returns {{ path: string, rendered_source: string, resolved_links: Array<{ label: string, reference: number, target: { kind?: string, path: string, status?: string, title: string } }>, source: string }}
|
|
112
|
+
* @returns {{ incoming_summary: Record<string, number>, path: string, rendered_source: string, resolved_links: Array<{ label: string, reference: number, target: { kind?: string, path: string, status?: string, title: string } }>, source: string }}
|
|
109
113
|
*/
|
|
110
114
|
function createShowOutput(
|
|
111
115
|
source_file_path,
|
|
112
116
|
source_text,
|
|
113
117
|
claims,
|
|
118
|
+
graph,
|
|
114
119
|
document_node_ids,
|
|
115
120
|
graph_nodes,
|
|
116
121
|
) {
|
|
@@ -125,8 +130,15 @@ function createShowOutput(
|
|
|
125
130
|
graph_nodes,
|
|
126
131
|
),
|
|
127
132
|
);
|
|
133
|
+
const reverse_references = inspectReverseReferences(
|
|
134
|
+
graph,
|
|
135
|
+
source_file_path,
|
|
136
|
+
undefined,
|
|
137
|
+
undefined,
|
|
138
|
+
);
|
|
128
139
|
|
|
129
140
|
return {
|
|
141
|
+
incoming_summary: summarizeIncomingReferences(reverse_references.incoming),
|
|
130
142
|
path: source_file_path,
|
|
131
143
|
rendered_source: renderResolvedSource(
|
|
132
144
|
source_text,
|
|
@@ -269,12 +281,23 @@ function getScalarGraphField(field_value) {
|
|
|
269
281
|
* @returns {{ kind?: string, path: string, status?: string, title: string }}
|
|
270
282
|
*/
|
|
271
283
|
function createResolvedLinkTarget(target_node, target_path, fallback_title) {
|
|
272
|
-
|
|
273
|
-
|
|
284
|
+
/** @type {{ kind?: string, path: string, status?: string, title: string }} */
|
|
285
|
+
const resolved_target = {
|
|
274
286
|
path: getResolvedLinkTargetPath(target_node, target_path),
|
|
275
|
-
status: getScalarGraphField(target_node?.status),
|
|
276
287
|
title: getScalarGraphField(target_node?.title) ?? fallback_title,
|
|
277
288
|
};
|
|
289
|
+
const target_kind = getResolvedLinkTargetKind(target_node);
|
|
290
|
+
const target_status = getScalarGraphField(target_node?.status);
|
|
291
|
+
|
|
292
|
+
if (target_kind) {
|
|
293
|
+
resolved_target.kind = target_kind;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (target_status) {
|
|
297
|
+
resolved_target.status = target_status;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return resolved_target;
|
|
278
301
|
}
|
|
279
302
|
|
|
280
303
|
/**
|
|
@@ -355,6 +378,21 @@ function trimTrailingLineBreaks(value) {
|
|
|
355
378
|
return value.replace(/\n+$/du, '');
|
|
356
379
|
}
|
|
357
380
|
|
|
381
|
+
/**
|
|
382
|
+
* @param {Record<string, GraphNode[]>} incoming
|
|
383
|
+
* @returns {Record<string, number>}
|
|
384
|
+
*/
|
|
385
|
+
function summarizeIncomingReferences(incoming) {
|
|
386
|
+
/** @type {Record<string, number>} */
|
|
387
|
+
const incoming_summary = {};
|
|
388
|
+
|
|
389
|
+
for (const relation_name of Object.keys(incoming)) {
|
|
390
|
+
incoming_summary[relation_name] = incoming[relation_name].length;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return incoming_summary;
|
|
394
|
+
}
|
|
395
|
+
|
|
358
396
|
/**
|
|
359
397
|
* @param {unknown} error
|
|
360
398
|
* @returns {error is NodeJS.ErrnoException}
|