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.
Files changed (67) hide show
  1. package/bin/patram.js +25 -147
  2. package/lib/build-graph-identity.js +270 -0
  3. package/lib/build-graph.js +156 -77
  4. package/lib/check-graph.js +23 -7
  5. package/lib/claim-helpers.js +55 -0
  6. package/lib/cli-help-metadata.js +552 -0
  7. package/lib/command-output.js +83 -0
  8. package/lib/derived-summary.js +278 -0
  9. package/lib/format-derived-summary-row.js +9 -0
  10. package/lib/format-node-header.js +19 -0
  11. package/lib/format-output-item-block.js +22 -0
  12. package/lib/format-output-metadata.js +62 -0
  13. package/lib/layout-stored-queries.js +361 -0
  14. package/lib/list-queries.js +18 -0
  15. package/lib/list-source-files.js +50 -15
  16. package/lib/load-patram-config.js +505 -18
  17. package/lib/load-patram-config.types.ts +40 -0
  18. package/lib/load-project-graph.js +124 -0
  19. package/lib/output-view.types.ts +88 -0
  20. package/lib/parse-claims.js +38 -158
  21. package/lib/parse-claims.types.ts +7 -0
  22. package/lib/parse-cli-arguments-helpers.js +446 -0
  23. package/lib/parse-cli-arguments.js +266 -0
  24. package/lib/parse-cli-arguments.types.ts +69 -0
  25. package/lib/parse-cli-color-options.js +44 -0
  26. package/lib/parse-cli-query-pagination.js +49 -0
  27. package/lib/parse-jsdoc-blocks.js +184 -0
  28. package/lib/parse-jsdoc-claims.js +280 -0
  29. package/lib/parse-jsdoc-prose.js +111 -0
  30. package/lib/parse-markdown-claims.js +242 -0
  31. package/lib/parse-markdown-directives.js +136 -0
  32. package/lib/parse-where-clause.js +707 -0
  33. package/lib/parse-where-clause.types.ts +70 -0
  34. package/lib/patram-cli.js +464 -0
  35. package/lib/patram-config.js +3 -1
  36. package/lib/patram-config.types.ts +2 -1
  37. package/lib/patram.js +6 -0
  38. package/lib/query-graph.js +368 -0
  39. package/lib/query-inspection.js +523 -0
  40. package/lib/render-check-output.js +315 -0
  41. package/lib/render-cli-help.js +419 -0
  42. package/lib/render-json-output.js +161 -0
  43. package/lib/render-output-view.js +222 -0
  44. package/lib/render-plain-output.js +182 -0
  45. package/lib/render-rich-output.js +240 -0
  46. package/lib/render-rich-source.js +1333 -0
  47. package/lib/resolve-check-target.js +190 -0
  48. package/lib/resolve-output-mode.js +60 -0
  49. package/lib/resolve-patram-graph-config.js +88 -0
  50. package/lib/resolve-where-clause.js +66 -0
  51. package/lib/show-document.js +311 -0
  52. package/lib/source-file-defaults.js +28 -0
  53. package/lib/tagged-fenced-block-error.js +17 -0
  54. package/lib/tagged-fenced-block-markdown.js +111 -0
  55. package/lib/tagged-fenced-block-metadata.js +97 -0
  56. package/lib/tagged-fenced-block-parser.js +292 -0
  57. package/lib/tagged-fenced-blocks.js +100 -0
  58. package/lib/tagged-fenced-blocks.types.ts +38 -0
  59. package/lib/write-paged-output.js +87 -0
  60. package/package.json +28 -12
  61. package/bin/patram.test.js +0 -184
  62. package/lib/build-graph.test.js +0 -141
  63. package/lib/check-graph.test.js +0 -103
  64. package/lib/list-source-files.test.js +0 -101
  65. package/lib/load-patram-config.test.js +0 -211
  66. package/lib/parse-claims.test.js +0 -113
  67. 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
+ }