patram 0.0.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/patram.js +25 -147
- package/lib/build-graph-identity.js +270 -0
- package/lib/build-graph.js +156 -77
- package/lib/check-graph.js +23 -7
- package/lib/claim-helpers.js +55 -0
- package/lib/cli-help-metadata.js +552 -0
- package/lib/command-output.js +83 -0
- package/lib/derived-summary.js +278 -0
- package/lib/format-derived-summary-row.js +9 -0
- package/lib/format-node-header.js +19 -0
- package/lib/format-output-item-block.js +22 -0
- package/lib/format-output-metadata.js +62 -0
- package/lib/layout-stored-queries.js +361 -0
- package/lib/list-queries.js +18 -0
- package/lib/list-source-files.js +50 -15
- package/lib/load-patram-config.js +505 -18
- package/lib/load-patram-config.types.ts +40 -0
- package/lib/load-project-graph.js +124 -0
- package/lib/output-view.types.ts +88 -0
- package/lib/parse-claims.js +38 -158
- package/lib/parse-claims.types.ts +7 -0
- package/lib/parse-cli-arguments-helpers.js +446 -0
- package/lib/parse-cli-arguments.js +266 -0
- package/lib/parse-cli-arguments.types.ts +69 -0
- package/lib/parse-cli-color-options.js +44 -0
- package/lib/parse-cli-query-pagination.js +49 -0
- package/lib/parse-jsdoc-blocks.js +184 -0
- package/lib/parse-jsdoc-claims.js +280 -0
- package/lib/parse-jsdoc-prose.js +111 -0
- package/lib/parse-markdown-claims.js +242 -0
- package/lib/parse-markdown-directives.js +136 -0
- package/lib/parse-where-clause.js +707 -0
- package/lib/parse-where-clause.types.ts +70 -0
- package/lib/patram-cli.js +464 -0
- package/lib/patram-config.js +3 -1
- package/lib/patram-config.types.ts +2 -1
- package/lib/patram.js +6 -0
- package/lib/query-graph.js +368 -0
- package/lib/query-inspection.js +523 -0
- package/lib/render-check-output.js +315 -0
- package/lib/render-cli-help.js +419 -0
- package/lib/render-json-output.js +161 -0
- package/lib/render-output-view.js +222 -0
- package/lib/render-plain-output.js +182 -0
- package/lib/render-rich-output.js +240 -0
- package/lib/render-rich-source.js +1333 -0
- package/lib/resolve-check-target.js +190 -0
- package/lib/resolve-output-mode.js +60 -0
- package/lib/resolve-patram-graph-config.js +88 -0
- package/lib/resolve-where-clause.js +66 -0
- package/lib/show-document.js +311 -0
- package/lib/source-file-defaults.js +28 -0
- package/lib/tagged-fenced-block-error.js +17 -0
- package/lib/tagged-fenced-block-markdown.js +111 -0
- package/lib/tagged-fenced-block-metadata.js +97 -0
- package/lib/tagged-fenced-block-parser.js +292 -0
- package/lib/tagged-fenced-blocks.js +100 -0
- package/lib/tagged-fenced-blocks.types.ts +38 -0
- package/lib/write-paged-output.js +87 -0
- package/package.json +28 -12
- package/bin/patram.test.js +0 -184
- package/lib/build-graph.test.js +0 -141
- package/lib/check-graph.test.js +0 -103
- package/lib/list-source-files.test.js +0 -101
- package/lib/load-patram-config.test.js +0 -211
- package/lib/parse-claims.test.js +0 -113
- package/lib/patram-config.test.js +0 -147
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { BuildGraphResult, GraphNode } from './build-graph.types.ts';
|
|
3
|
+
* @import { OutputDerivedSummary } from './output-view.types.ts';
|
|
4
|
+
* @import { DerivedSummaryConfig, DerivedSummaryFieldConfig, DerivedSummaryScalar, PatramRepoConfig } from './load-patram-config.types.ts';
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { queryGraph } from './query-graph.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Derived summary evaluation.
|
|
11
|
+
*
|
|
12
|
+
* Evaluates repo-configured output-only metadata for graph nodes without
|
|
13
|
+
* mutating graph state.
|
|
14
|
+
*
|
|
15
|
+
* Kind: output
|
|
16
|
+
* Status: active
|
|
17
|
+
* Tracked in: ../docs/plans/v0/declarative-derived-summaries.md
|
|
18
|
+
* Decided by: ../docs/decisions/declarative-derived-summary-config.md
|
|
19
|
+
* Decided by: ../docs/decisions/declarative-derived-summary-side-effects.md
|
|
20
|
+
* @patram
|
|
21
|
+
* @see {@link ./load-patram-config.js}
|
|
22
|
+
* @see {@link ./render-output-view.js}
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {{
|
|
27
|
+
* evaluate: (graph_node: GraphNode) => OutputDerivedSummary | null,
|
|
28
|
+
* }} DerivedSummaryEvaluator
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {PatramRepoConfig} repo_config
|
|
33
|
+
* @param {BuildGraphResult} graph
|
|
34
|
+
* @returns {DerivedSummaryEvaluator}
|
|
35
|
+
*/
|
|
36
|
+
export function createDerivedSummaryEvaluator(repo_config, graph) {
|
|
37
|
+
const summary_by_kind = createSummaryByKind(repo_config.derived_summaries);
|
|
38
|
+
/** @type {Map<string, Set<string>>} */
|
|
39
|
+
const matching_node_id_cache = new Map();
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
evaluate(graph_node) {
|
|
43
|
+
const configured_summary = summary_by_kind.get(graph_node.kind);
|
|
44
|
+
|
|
45
|
+
if (!configured_summary) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
fields: configured_summary.definition.fields.map(
|
|
51
|
+
(field_definition) => ({
|
|
52
|
+
name: field_definition.name,
|
|
53
|
+
value: evaluateFieldValue(
|
|
54
|
+
field_definition,
|
|
55
|
+
configured_summary.definition,
|
|
56
|
+
graph,
|
|
57
|
+
graph_node,
|
|
58
|
+
matching_node_id_cache,
|
|
59
|
+
),
|
|
60
|
+
}),
|
|
61
|
+
),
|
|
62
|
+
name: configured_summary.name,
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {DerivedSummaryFieldConfig} field_definition
|
|
70
|
+
* @param {DerivedSummaryConfig} summary_definition
|
|
71
|
+
* @param {BuildGraphResult} graph
|
|
72
|
+
* @param {GraphNode} graph_node
|
|
73
|
+
* @param {Map<string, Set<string>>} matching_node_id_cache
|
|
74
|
+
* @returns {DerivedSummaryScalar}
|
|
75
|
+
*/
|
|
76
|
+
function evaluateFieldValue(
|
|
77
|
+
field_definition,
|
|
78
|
+
summary_definition,
|
|
79
|
+
graph,
|
|
80
|
+
graph_node,
|
|
81
|
+
matching_node_id_cache,
|
|
82
|
+
) {
|
|
83
|
+
if ('count' in field_definition) {
|
|
84
|
+
return countMatchingTraversalNodes(
|
|
85
|
+
graph,
|
|
86
|
+
graph_node,
|
|
87
|
+
field_definition.count.traversal,
|
|
88
|
+
field_definition.count.where,
|
|
89
|
+
matching_node_id_cache,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return selectFieldValue(
|
|
94
|
+
graph,
|
|
95
|
+
graph_node,
|
|
96
|
+
summary_definition,
|
|
97
|
+
field_definition,
|
|
98
|
+
matching_node_id_cache,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {BuildGraphResult} graph
|
|
104
|
+
* @param {GraphNode} graph_node
|
|
105
|
+
* @param {string} traversal_text
|
|
106
|
+
* @param {string} where_clause
|
|
107
|
+
* @param {Map<string, Set<string>>} matching_node_id_cache
|
|
108
|
+
* @returns {number}
|
|
109
|
+
*/
|
|
110
|
+
function countMatchingTraversalNodes(
|
|
111
|
+
graph,
|
|
112
|
+
graph_node,
|
|
113
|
+
traversal_text,
|
|
114
|
+
where_clause,
|
|
115
|
+
matching_node_id_cache,
|
|
116
|
+
) {
|
|
117
|
+
const target_node_ids = getTraversedNodeIds(
|
|
118
|
+
graph,
|
|
119
|
+
graph_node.id,
|
|
120
|
+
traversal_text,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
if (target_node_ids.size === 0) {
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const matching_node_ids = getMatchingNodeIds(
|
|
128
|
+
graph,
|
|
129
|
+
where_clause,
|
|
130
|
+
matching_node_id_cache,
|
|
131
|
+
);
|
|
132
|
+
let matching_count = 0;
|
|
133
|
+
|
|
134
|
+
for (const target_node_id of target_node_ids) {
|
|
135
|
+
if (matching_node_ids.has(target_node_id)) {
|
|
136
|
+
matching_count += 1;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return matching_count;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @param {BuildGraphResult} graph
|
|
145
|
+
* @param {GraphNode} graph_node
|
|
146
|
+
* @param {DerivedSummaryConfig} summary_definition
|
|
147
|
+
* @param {Extract<DerivedSummaryFieldConfig, { select: unknown }>} field_definition
|
|
148
|
+
* @param {Map<string, Set<string>>} matching_node_id_cache
|
|
149
|
+
* @returns {DerivedSummaryScalar}
|
|
150
|
+
*/
|
|
151
|
+
function selectFieldValue(
|
|
152
|
+
graph,
|
|
153
|
+
graph_node,
|
|
154
|
+
summary_definition,
|
|
155
|
+
field_definition,
|
|
156
|
+
matching_node_id_cache,
|
|
157
|
+
) {
|
|
158
|
+
for (const select_case of field_definition.select) {
|
|
159
|
+
const matching_node_ids = getMatchingNodeIds(
|
|
160
|
+
graph,
|
|
161
|
+
select_case.when,
|
|
162
|
+
matching_node_id_cache,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
if (matching_node_ids.has(graph_node.id)) {
|
|
166
|
+
return select_case.value;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return field_definition.default;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @param {BuildGraphResult} graph
|
|
175
|
+
* @param {string} where_clause
|
|
176
|
+
* @param {Map<string, Set<string>>} matching_node_id_cache
|
|
177
|
+
* @returns {Set<string>}
|
|
178
|
+
*/
|
|
179
|
+
function getMatchingNodeIds(graph, where_clause, matching_node_id_cache) {
|
|
180
|
+
const cached_node_ids = matching_node_id_cache.get(where_clause);
|
|
181
|
+
|
|
182
|
+
if (cached_node_ids) {
|
|
183
|
+
return cached_node_ids;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const query_result = queryGraph(graph, where_clause);
|
|
187
|
+
|
|
188
|
+
if (query_result.diagnostics.length > 0) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`Expected derived summary query "${where_clause}" to be valid.`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const matching_node_ids = new Set(
|
|
195
|
+
query_result.nodes.map((matching_node) => matching_node.id),
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
matching_node_id_cache.set(where_clause, matching_node_ids);
|
|
199
|
+
|
|
200
|
+
return matching_node_ids;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @param {BuildGraphResult} graph
|
|
205
|
+
* @param {string} node_id
|
|
206
|
+
* @param {string} traversal_text
|
|
207
|
+
* @returns {Set<string>}
|
|
208
|
+
*/
|
|
209
|
+
function getTraversedNodeIds(graph, node_id, traversal_text) {
|
|
210
|
+
const traversal = parseTraversal(traversal_text);
|
|
211
|
+
/** @type {Set<string>} */
|
|
212
|
+
const target_node_ids = new Set();
|
|
213
|
+
|
|
214
|
+
for (const graph_edge of graph.edges) {
|
|
215
|
+
if (graph_edge.relation !== traversal.relation_name) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (traversal.direction === 'in' && graph_edge.to === node_id) {
|
|
220
|
+
target_node_ids.add(graph_edge.from);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (traversal.direction === 'out' && graph_edge.from === node_id) {
|
|
224
|
+
target_node_ids.add(graph_edge.to);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return target_node_ids;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* @param {string} traversal_text
|
|
233
|
+
* @returns {{ direction: 'in' | 'out', relation_name: string }}
|
|
234
|
+
*/
|
|
235
|
+
function parseTraversal(traversal_text) {
|
|
236
|
+
const traversal_match =
|
|
237
|
+
/^(?<direction>in|out):(?<relation_name>[a-zA-Z0-9_]+)$/du.exec(
|
|
238
|
+
traversal_text,
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
if (
|
|
242
|
+
!traversal_match?.groups?.direction ||
|
|
243
|
+
!traversal_match.groups.relation_name
|
|
244
|
+
) {
|
|
245
|
+
throw new Error(`Invalid derived summary traversal "${traversal_text}".`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
direction: /** @type {'in' | 'out'} */ (traversal_match.groups.direction),
|
|
250
|
+
relation_name: traversal_match.groups.relation_name,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* @param {PatramRepoConfig['derived_summaries']} derived_summaries
|
|
256
|
+
* @returns {Map<string, { definition: DerivedSummaryConfig, name: string }>}
|
|
257
|
+
*/
|
|
258
|
+
function createSummaryByKind(derived_summaries) {
|
|
259
|
+
/** @type {Map<string, { definition: DerivedSummaryConfig, name: string }>} */
|
|
260
|
+
const summary_by_kind = new Map();
|
|
261
|
+
|
|
262
|
+
if (!derived_summaries) {
|
|
263
|
+
return summary_by_kind;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
for (const [summary_name, summary_definition] of Object.entries(
|
|
267
|
+
derived_summaries,
|
|
268
|
+
)) {
|
|
269
|
+
for (const kind_name of summary_definition.kinds) {
|
|
270
|
+
summary_by_kind.set(kind_name, {
|
|
271
|
+
definition: summary_definition,
|
|
272
|
+
name: summary_name,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return summary_by_kind;
|
|
278
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {import('./output-view.types.ts').OutputDerivedSummary} derived_summary
|
|
3
|
+
* @returns {string}
|
|
4
|
+
*/
|
|
5
|
+
export function formatDerivedSummaryRow(derived_summary) {
|
|
6
|
+
return derived_summary.fields
|
|
7
|
+
.map((field) => `${field.name}: ${String(field.value)}`)
|
|
8
|
+
.join(' ');
|
|
9
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {import('./output-view.types.ts').OutputNodeItem} output_item
|
|
3
|
+
* @returns {string}
|
|
4
|
+
*/
|
|
5
|
+
export function formatNodeHeader(output_item) {
|
|
6
|
+
if (isDocumentNode(output_item)) {
|
|
7
|
+
return `document ${output_item.path}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return `${output_item.node_kind} ${output_item.id}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {import('./output-view.types.ts').OutputNodeItem} output_item
|
|
15
|
+
* @returns {boolean}
|
|
16
|
+
*/
|
|
17
|
+
export function isDocumentNode(output_item) {
|
|
18
|
+
return output_item.id === `doc:${output_item.path}`;
|
|
19
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {{ header: string, metadata_rows: string[], metadata_indent?: string, title: string, title_indent?: string }} options
|
|
3
|
+
* @returns {string}
|
|
4
|
+
*/
|
|
5
|
+
export function formatOutputItemBlock(options) {
|
|
6
|
+
const metadata_indent = options.metadata_indent ?? '';
|
|
7
|
+
const title_indent = options.title_indent ?? ' ';
|
|
8
|
+
/** @type {string[]} */
|
|
9
|
+
const lines = [options.header];
|
|
10
|
+
|
|
11
|
+
if (options.metadata_rows.length > 0) {
|
|
12
|
+
lines.push(
|
|
13
|
+
...options.metadata_rows.map(
|
|
14
|
+
(metadata_row) => `${metadata_indent}${metadata_row}`,
|
|
15
|
+
),
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
lines.push('', `${title_indent}${options.title}`);
|
|
20
|
+
|
|
21
|
+
return lines.join('\n');
|
|
22
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { formatDerivedSummaryRow } from './format-derived-summary-row.js';
|
|
2
|
+
import { isDocumentNode } from './format-node-header.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {import('./output-view.types.ts').OutputNodeItem} output_item
|
|
6
|
+
* @returns {string[]}
|
|
7
|
+
*/
|
|
8
|
+
export function formatOutputNodeMetadataRows(output_item) {
|
|
9
|
+
/** @type {string[]} */
|
|
10
|
+
const metadata_rows = [];
|
|
11
|
+
/** @type {string[]} */
|
|
12
|
+
const stored_metadata_fields = [];
|
|
13
|
+
|
|
14
|
+
if (isDocumentNode(output_item)) {
|
|
15
|
+
stored_metadata_fields.push(`kind: ${output_item.node_kind}`);
|
|
16
|
+
} else {
|
|
17
|
+
stored_metadata_fields.push(`path: ${output_item.path}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (output_item.status) {
|
|
21
|
+
stored_metadata_fields.push(`status: ${output_item.status}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (stored_metadata_fields.length > 0) {
|
|
25
|
+
metadata_rows.push(stored_metadata_fields.join(' '));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (output_item.derived_summary) {
|
|
29
|
+
metadata_rows.push(formatDerivedSummaryRow(output_item.derived_summary));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return metadata_rows;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {import('./output-view.types.ts').OutputResolvedLinkTarget} target
|
|
37
|
+
* @returns {string[]}
|
|
38
|
+
*/
|
|
39
|
+
export function formatResolvedLinkMetadataRows(target) {
|
|
40
|
+
/** @type {string[]} */
|
|
41
|
+
const metadata_rows = [];
|
|
42
|
+
/** @type {string[]} */
|
|
43
|
+
const stored_metadata_fields = [];
|
|
44
|
+
|
|
45
|
+
if (target.kind) {
|
|
46
|
+
stored_metadata_fields.push(`kind: ${target.kind}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (target.status) {
|
|
50
|
+
stored_metadata_fields.push(`status: ${target.status}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (stored_metadata_fields.length > 0) {
|
|
54
|
+
metadata_rows.push(stored_metadata_fields.join(' '));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (target.derived_summary) {
|
|
58
|
+
metadata_rows.push(formatDerivedSummaryRow(target.derived_summary));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return metadata_rows;
|
|
62
|
+
}
|