patram 0.1.1 → 0.3.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 +57 -24
- package/lib/build-graph.js +383 -17
- package/lib/build-graph.types.ts +5 -2
- package/lib/check-directive-metadata.js +516 -0
- package/lib/check-directive-value.js +282 -0
- package/lib/check-graph.js +24 -5
- package/lib/cli-help-metadata.js +580 -0
- package/lib/derived-summary.js +280 -0
- package/lib/directive-diagnostics.js +38 -0
- package/lib/directive-type-rules.js +133 -0
- package/lib/discover-fields.js +427 -0
- package/lib/discover-fields.types.ts +52 -0
- package/lib/format-derived-summary-row.js +9 -0
- package/lib/format-node-header.js +21 -0
- package/lib/format-output-item-block.js +22 -0
- package/lib/format-output-metadata.js +54 -0
- package/lib/layout-stored-queries.js +96 -2
- package/lib/load-patram-config.js +754 -18
- package/lib/load-patram-config.types.ts +128 -2
- package/lib/load-project-graph.js +4 -1
- package/lib/output-view.types.ts +29 -6
- package/lib/parse-cli-arguments-helpers.js +263 -90
- package/lib/parse-cli-arguments.js +160 -8
- package/lib/parse-cli-arguments.types.ts +49 -4
- package/lib/parse-where-clause.js +670 -209
- package/lib/parse-where-clause.types.ts +72 -0
- package/lib/patram-cli.js +180 -21
- package/lib/patram-config.js +31 -31
- package/lib/patram-config.types.ts +10 -4
- package/lib/patram.js +6 -0
- package/lib/query-graph.js +444 -113
- package/lib/query-inspection.js +798 -0
- package/lib/render-check-output.js +1 -1
- package/lib/render-cli-help.js +419 -0
- package/lib/render-field-discovery.js +148 -0
- package/lib/render-json-output.js +66 -14
- package/lib/render-output-view.js +272 -22
- package/lib/render-plain-output.js +31 -86
- package/lib/render-rich-output.js +34 -87
- package/lib/resolve-patram-graph-config.js +15 -9
- package/lib/resolve-where-clause.js +18 -3
- package/lib/show-document.js +51 -7
- 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/package.json +12 -7
|
@@ -0,0 +1,280 @@
|
|
|
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_class = createSummaryByClass(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_class.get(
|
|
44
|
+
graph_node.$class ?? 'document',
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
if (!configured_summary) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
fields: configured_summary.definition.fields.map(
|
|
53
|
+
(field_definition) => ({
|
|
54
|
+
name: field_definition.name,
|
|
55
|
+
value: evaluateFieldValue(
|
|
56
|
+
field_definition,
|
|
57
|
+
configured_summary.definition,
|
|
58
|
+
graph,
|
|
59
|
+
graph_node,
|
|
60
|
+
matching_node_id_cache,
|
|
61
|
+
),
|
|
62
|
+
}),
|
|
63
|
+
),
|
|
64
|
+
name: configured_summary.name,
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {DerivedSummaryFieldConfig} field_definition
|
|
72
|
+
* @param {DerivedSummaryConfig} summary_definition
|
|
73
|
+
* @param {BuildGraphResult} graph
|
|
74
|
+
* @param {GraphNode} graph_node
|
|
75
|
+
* @param {Map<string, Set<string>>} matching_node_id_cache
|
|
76
|
+
* @returns {DerivedSummaryScalar}
|
|
77
|
+
*/
|
|
78
|
+
function evaluateFieldValue(
|
|
79
|
+
field_definition,
|
|
80
|
+
summary_definition,
|
|
81
|
+
graph,
|
|
82
|
+
graph_node,
|
|
83
|
+
matching_node_id_cache,
|
|
84
|
+
) {
|
|
85
|
+
if ('count' in field_definition) {
|
|
86
|
+
return countMatchingTraversalNodes(
|
|
87
|
+
graph,
|
|
88
|
+
graph_node,
|
|
89
|
+
field_definition.count.traversal,
|
|
90
|
+
field_definition.count.where,
|
|
91
|
+
matching_node_id_cache,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return selectFieldValue(
|
|
96
|
+
graph,
|
|
97
|
+
graph_node,
|
|
98
|
+
summary_definition,
|
|
99
|
+
field_definition,
|
|
100
|
+
matching_node_id_cache,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @param {BuildGraphResult} graph
|
|
106
|
+
* @param {GraphNode} graph_node
|
|
107
|
+
* @param {string} traversal_text
|
|
108
|
+
* @param {string} where_clause
|
|
109
|
+
* @param {Map<string, Set<string>>} matching_node_id_cache
|
|
110
|
+
* @returns {number}
|
|
111
|
+
*/
|
|
112
|
+
function countMatchingTraversalNodes(
|
|
113
|
+
graph,
|
|
114
|
+
graph_node,
|
|
115
|
+
traversal_text,
|
|
116
|
+
where_clause,
|
|
117
|
+
matching_node_id_cache,
|
|
118
|
+
) {
|
|
119
|
+
const target_node_ids = getTraversedNodeIds(
|
|
120
|
+
graph,
|
|
121
|
+
graph_node.id,
|
|
122
|
+
traversal_text,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
if (target_node_ids.size === 0) {
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const matching_node_ids = getMatchingNodeIds(
|
|
130
|
+
graph,
|
|
131
|
+
where_clause,
|
|
132
|
+
matching_node_id_cache,
|
|
133
|
+
);
|
|
134
|
+
let matching_count = 0;
|
|
135
|
+
|
|
136
|
+
for (const target_node_id of target_node_ids) {
|
|
137
|
+
if (matching_node_ids.has(target_node_id)) {
|
|
138
|
+
matching_count += 1;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return matching_count;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @param {BuildGraphResult} graph
|
|
147
|
+
* @param {GraphNode} graph_node
|
|
148
|
+
* @param {DerivedSummaryConfig} summary_definition
|
|
149
|
+
* @param {Extract<DerivedSummaryFieldConfig, { select: unknown }>} field_definition
|
|
150
|
+
* @param {Map<string, Set<string>>} matching_node_id_cache
|
|
151
|
+
* @returns {DerivedSummaryScalar}
|
|
152
|
+
*/
|
|
153
|
+
function selectFieldValue(
|
|
154
|
+
graph,
|
|
155
|
+
graph_node,
|
|
156
|
+
summary_definition,
|
|
157
|
+
field_definition,
|
|
158
|
+
matching_node_id_cache,
|
|
159
|
+
) {
|
|
160
|
+
for (const select_case of field_definition.select) {
|
|
161
|
+
const matching_node_ids = getMatchingNodeIds(
|
|
162
|
+
graph,
|
|
163
|
+
select_case.when,
|
|
164
|
+
matching_node_id_cache,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
if (matching_node_ids.has(graph_node.id)) {
|
|
168
|
+
return select_case.value;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return field_definition.default;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @param {BuildGraphResult} graph
|
|
177
|
+
* @param {string} where_clause
|
|
178
|
+
* @param {Map<string, Set<string>>} matching_node_id_cache
|
|
179
|
+
* @returns {Set<string>}
|
|
180
|
+
*/
|
|
181
|
+
function getMatchingNodeIds(graph, where_clause, matching_node_id_cache) {
|
|
182
|
+
const cached_node_ids = matching_node_id_cache.get(where_clause);
|
|
183
|
+
|
|
184
|
+
if (cached_node_ids) {
|
|
185
|
+
return cached_node_ids;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const query_result = queryGraph(graph, where_clause);
|
|
189
|
+
|
|
190
|
+
if (query_result.diagnostics.length > 0) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`Expected derived summary query "${where_clause}" to be valid.`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const matching_node_ids = new Set(
|
|
197
|
+
query_result.nodes.map((matching_node) => matching_node.id),
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
matching_node_id_cache.set(where_clause, matching_node_ids);
|
|
201
|
+
|
|
202
|
+
return matching_node_ids;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* @param {BuildGraphResult} graph
|
|
207
|
+
* @param {string} node_id
|
|
208
|
+
* @param {string} traversal_text
|
|
209
|
+
* @returns {Set<string>}
|
|
210
|
+
*/
|
|
211
|
+
function getTraversedNodeIds(graph, node_id, traversal_text) {
|
|
212
|
+
const traversal = parseTraversal(traversal_text);
|
|
213
|
+
/** @type {Set<string>} */
|
|
214
|
+
const target_node_ids = new Set();
|
|
215
|
+
|
|
216
|
+
for (const graph_edge of graph.edges) {
|
|
217
|
+
if (graph_edge.relation !== traversal.relation_name) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (traversal.direction === 'in' && graph_edge.to === node_id) {
|
|
222
|
+
target_node_ids.add(graph_edge.from);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (traversal.direction === 'out' && graph_edge.from === node_id) {
|
|
226
|
+
target_node_ids.add(graph_edge.to);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return target_node_ids;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* @param {string} traversal_text
|
|
235
|
+
* @returns {{ direction: 'in' | 'out', relation_name: string }}
|
|
236
|
+
*/
|
|
237
|
+
function parseTraversal(traversal_text) {
|
|
238
|
+
const traversal_match =
|
|
239
|
+
/^(?<direction>in|out):(?<relation_name>[a-zA-Z0-9_]+)$/du.exec(
|
|
240
|
+
traversal_text,
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
if (
|
|
244
|
+
!traversal_match?.groups?.direction ||
|
|
245
|
+
!traversal_match.groups.relation_name
|
|
246
|
+
) {
|
|
247
|
+
throw new Error(`Invalid derived summary traversal "${traversal_text}".`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
direction: /** @type {'in' | 'out'} */ (traversal_match.groups.direction),
|
|
252
|
+
relation_name: traversal_match.groups.relation_name,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* @param {PatramRepoConfig['derived_summaries']} derived_summaries
|
|
258
|
+
* @returns {Map<string, { definition: DerivedSummaryConfig, name: string }>}
|
|
259
|
+
*/
|
|
260
|
+
function createSummaryByClass(derived_summaries) {
|
|
261
|
+
/** @type {Map<string, { definition: DerivedSummaryConfig, name: string }>} */
|
|
262
|
+
const summary_by_class = new Map();
|
|
263
|
+
|
|
264
|
+
if (!derived_summaries) {
|
|
265
|
+
return summary_by_class;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
for (const [summary_name, summary_definition] of Object.entries(
|
|
269
|
+
derived_summaries,
|
|
270
|
+
)) {
|
|
271
|
+
for (const class_name of summary_definition.classes) {
|
|
272
|
+
summary_by_class.set(class_name, {
|
|
273
|
+
definition: summary_definition,
|
|
274
|
+
name: summary_name,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return summary_by_class;
|
|
280
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { PatramDiagnostic } from './load-patram-config.types.ts';
|
|
3
|
+
* @import { PatramClaim } from './parse-claims.types.ts';
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {PatramClaim} claim
|
|
8
|
+
* @param {string} code
|
|
9
|
+
* @param {string} message
|
|
10
|
+
* @returns {PatramDiagnostic}
|
|
11
|
+
*/
|
|
12
|
+
export function createOriginDiagnostic(claim, code, message) {
|
|
13
|
+
return {
|
|
14
|
+
code,
|
|
15
|
+
column: claim.origin.column,
|
|
16
|
+
level: 'error',
|
|
17
|
+
line: claim.origin.line,
|
|
18
|
+
message,
|
|
19
|
+
path: claim.origin.path,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} document_path
|
|
25
|
+
* @param {string} code
|
|
26
|
+
* @param {string} message
|
|
27
|
+
* @returns {PatramDiagnostic}
|
|
28
|
+
*/
|
|
29
|
+
export function createDocumentDiagnostic(document_path, code, message) {
|
|
30
|
+
return {
|
|
31
|
+
code,
|
|
32
|
+
column: 1,
|
|
33
|
+
level: 'error',
|
|
34
|
+
line: 1,
|
|
35
|
+
message,
|
|
36
|
+
path: document_path,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { MetadataFieldConfig } from './load-patram-config.types.ts';
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { isPathLikeTarget } from './claim-helpers.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {MetadataFieldConfig} type_definition
|
|
9
|
+
* @param {string} directive_value
|
|
10
|
+
* @returns {boolean}
|
|
11
|
+
*/
|
|
12
|
+
export function isDirectiveValueValid(type_definition, directive_value) {
|
|
13
|
+
if (directive_value.length === 0) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
switch (type_definition.type) {
|
|
18
|
+
case 'string':
|
|
19
|
+
return true;
|
|
20
|
+
case 'integer':
|
|
21
|
+
return /^-?\d+$/du.test(directive_value);
|
|
22
|
+
case 'path':
|
|
23
|
+
return isPathLikeTarget(directive_value);
|
|
24
|
+
case 'glob':
|
|
25
|
+
return true;
|
|
26
|
+
case 'date':
|
|
27
|
+
return isValidDateValue(directive_value);
|
|
28
|
+
case 'date_time':
|
|
29
|
+
return isValidDateTimeValue(directive_value);
|
|
30
|
+
default:
|
|
31
|
+
throw new Error(`Unsupported directive type "${type_definition.type}".`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {string} directive_name
|
|
37
|
+
* @param {Exclude<MetadataFieldConfig['type'], 'enum'>} type_name
|
|
38
|
+
* @returns {string}
|
|
39
|
+
*/
|
|
40
|
+
export function getInvalidTypeMessage(directive_name, type_name) {
|
|
41
|
+
switch (type_name) {
|
|
42
|
+
case 'string':
|
|
43
|
+
return `Directive "${directive_name}" must be a non-empty string.`;
|
|
44
|
+
case 'integer':
|
|
45
|
+
return `Directive "${directive_name}" must be a base-10 integer.`;
|
|
46
|
+
case 'path':
|
|
47
|
+
return `Directive "${directive_name}" must be a path-like string.`;
|
|
48
|
+
case 'glob':
|
|
49
|
+
return `Directive "${directive_name}" must be a non-empty glob string.`;
|
|
50
|
+
case 'date':
|
|
51
|
+
return `Directive "${directive_name}" must use YYYY-MM-DD.`;
|
|
52
|
+
case 'date_time':
|
|
53
|
+
return `Directive "${directive_name}" must use YYYY-MM-DD HH:MM.`;
|
|
54
|
+
default:
|
|
55
|
+
throw new Error(`Unsupported directive type "${type_name}".`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {string[]} values
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
63
|
+
export function formatQuotedList(values) {
|
|
64
|
+
return values.map((value) => `"${value}"`).join(', ');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {string} directive_value
|
|
69
|
+
* @returns {boolean}
|
|
70
|
+
*/
|
|
71
|
+
function isValidDateValue(directive_value) {
|
|
72
|
+
const date_match = /^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})$/du.exec(
|
|
73
|
+
directive_value,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if (!date_match?.groups) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return isRealCalendarDate(
|
|
81
|
+
Number(date_match.groups.year),
|
|
82
|
+
Number(date_match.groups.month),
|
|
83
|
+
Number(date_match.groups.day),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {string} directive_value
|
|
89
|
+
* @returns {boolean}
|
|
90
|
+
*/
|
|
91
|
+
function isValidDateTimeValue(directive_value) {
|
|
92
|
+
const date_time_match =
|
|
93
|
+
/^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2}) (?<hour>\d{2}):(?<minute>\d{2})$/du.exec(
|
|
94
|
+
directive_value,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
if (!date_time_match?.groups) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const hour = Number(date_time_match.groups.hour);
|
|
102
|
+
const minute = Number(date_time_match.groups.minute);
|
|
103
|
+
|
|
104
|
+
if (hour > 23 || minute > 59) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return isRealCalendarDate(
|
|
109
|
+
Number(date_time_match.groups.year),
|
|
110
|
+
Number(date_time_match.groups.month),
|
|
111
|
+
Number(date_time_match.groups.day),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @param {number} year
|
|
117
|
+
* @param {number} month
|
|
118
|
+
* @param {number} day
|
|
119
|
+
* @returns {boolean}
|
|
120
|
+
*/
|
|
121
|
+
function isRealCalendarDate(year, month, day) {
|
|
122
|
+
if (month < 1 || month > 12 || day < 1 || day > 31) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const candidate_date = new Date(Date.UTC(year, month - 1, day));
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
candidate_date.getUTCFullYear() === year &&
|
|
130
|
+
candidate_date.getUTCMonth() === month - 1 &&
|
|
131
|
+
candidate_date.getUTCDate() === day
|
|
132
|
+
);
|
|
133
|
+
}
|