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.
Files changed (49) hide show
  1. package/lib/build-graph-identity.js +57 -24
  2. package/lib/build-graph.js +383 -17
  3. package/lib/build-graph.types.ts +5 -2
  4. package/lib/check-directive-metadata.js +516 -0
  5. package/lib/check-directive-value.js +282 -0
  6. package/lib/check-graph.js +24 -5
  7. package/lib/cli-help-metadata.js +580 -0
  8. package/lib/derived-summary.js +280 -0
  9. package/lib/directive-diagnostics.js +38 -0
  10. package/lib/directive-type-rules.js +133 -0
  11. package/lib/discover-fields.js +427 -0
  12. package/lib/discover-fields.types.ts +52 -0
  13. package/lib/format-derived-summary-row.js +9 -0
  14. package/lib/format-node-header.js +21 -0
  15. package/lib/format-output-item-block.js +22 -0
  16. package/lib/format-output-metadata.js +54 -0
  17. package/lib/layout-stored-queries.js +96 -2
  18. package/lib/load-patram-config.js +754 -18
  19. package/lib/load-patram-config.types.ts +128 -2
  20. package/lib/load-project-graph.js +4 -1
  21. package/lib/output-view.types.ts +29 -6
  22. package/lib/parse-cli-arguments-helpers.js +263 -90
  23. package/lib/parse-cli-arguments.js +160 -8
  24. package/lib/parse-cli-arguments.types.ts +49 -4
  25. package/lib/parse-where-clause.js +670 -209
  26. package/lib/parse-where-clause.types.ts +72 -0
  27. package/lib/patram-cli.js +180 -21
  28. package/lib/patram-config.js +31 -31
  29. package/lib/patram-config.types.ts +10 -4
  30. package/lib/patram.js +6 -0
  31. package/lib/query-graph.js +444 -113
  32. package/lib/query-inspection.js +798 -0
  33. package/lib/render-check-output.js +1 -1
  34. package/lib/render-cli-help.js +419 -0
  35. package/lib/render-field-discovery.js +148 -0
  36. package/lib/render-json-output.js +66 -14
  37. package/lib/render-output-view.js +272 -22
  38. package/lib/render-plain-output.js +31 -86
  39. package/lib/render-rich-output.js +34 -87
  40. package/lib/resolve-patram-graph-config.js +15 -9
  41. package/lib/resolve-where-clause.js +18 -3
  42. package/lib/show-document.js +51 -7
  43. package/lib/tagged-fenced-block-error.js +17 -0
  44. package/lib/tagged-fenced-block-markdown.js +111 -0
  45. package/lib/tagged-fenced-block-metadata.js +97 -0
  46. package/lib/tagged-fenced-block-parser.js +292 -0
  47. package/lib/tagged-fenced-blocks.js +100 -0
  48. package/lib/tagged-fenced-blocks.types.ts +38 -0
  49. 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
+ }