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,111 @@
1
+ /**
2
+ * @import { PatramClaimFields } from './parse-claims.types.ts';
3
+ */
4
+
5
+ const JSDOC_SENTENCE_PATTERN = /^(.+?[.!?])(?:\s+|$)([\s\S]*)$/du;
6
+ const JSDOC_TITLE_LENGTH_LIMIT = 120;
7
+
8
+ /**
9
+ * @param {Array<Array<{ column: number, content: string, line: number }>>} prose_paragraphs
10
+ * @param {Array<{ column: number, content: string, line: number }>} paragraph_lines
11
+ */
12
+ export function pushJsdocParagraph(prose_paragraphs, paragraph_lines) {
13
+ if (paragraph_lines.length > 0) {
14
+ prose_paragraphs.push(paragraph_lines);
15
+ }
16
+ }
17
+
18
+ /**
19
+ * @param {string} file_path
20
+ * @param {Array<Array<{ column: number, content: string, line: number }>>} prose_paragraphs
21
+ * @returns {Array<{ claim_fields: PatramClaimFields, claim_type: string, order: number }>}
22
+ */
23
+ export function createJsdocProseClaimEntries(file_path, prose_paragraphs) {
24
+ if (prose_paragraphs.length === 0) {
25
+ return [];
26
+ }
27
+
28
+ const first_paragraph = prose_paragraphs[0];
29
+ const first_origin = {
30
+ column: first_paragraph[0].column,
31
+ line: first_paragraph[0].line,
32
+ path: file_path,
33
+ };
34
+ const title_result = splitJsdocParagraphTitle(
35
+ first_paragraph.map((line) => line.content).join(' '),
36
+ );
37
+ /** @type {Array<{ claim_fields: PatramClaimFields, claim_type: string, order: number }>} */
38
+ const claim_entries = [
39
+ {
40
+ claim_fields: {
41
+ origin: first_origin,
42
+ value: title_result.title,
43
+ },
44
+ claim_type: 'document.title',
45
+ order: 0,
46
+ },
47
+ ];
48
+ /** @type {string[]} */
49
+ const description_parts = [];
50
+ /** @type {{ column: number, line: number, path: string } | null} */
51
+ let description_origin = null;
52
+
53
+ if (title_result.remainder.length > 0) {
54
+ description_parts.push(title_result.remainder);
55
+ description_origin = first_origin;
56
+ }
57
+
58
+ for (const paragraph_lines of prose_paragraphs.slice(1)) {
59
+ if (description_origin === null) {
60
+ description_origin = {
61
+ column: paragraph_lines[0].column,
62
+ line: paragraph_lines[0].line,
63
+ path: file_path,
64
+ };
65
+ }
66
+
67
+ description_parts.push(
68
+ paragraph_lines.map((line) => line.content).join(' '),
69
+ );
70
+ }
71
+
72
+ if (description_origin && description_parts.length > 0) {
73
+ claim_entries.push({
74
+ claim_fields: {
75
+ origin: description_origin,
76
+ value: description_parts.join('\n\n'),
77
+ },
78
+ claim_type: 'document.description',
79
+ order: 1,
80
+ });
81
+ }
82
+
83
+ return claim_entries;
84
+ }
85
+
86
+ /**
87
+ * @param {string} paragraph_text
88
+ * @returns {{ remainder: string, title: string }}
89
+ */
90
+ function splitJsdocParagraphTitle(paragraph_text) {
91
+ if (paragraph_text.length <= JSDOC_TITLE_LENGTH_LIMIT) {
92
+ return {
93
+ remainder: '',
94
+ title: paragraph_text,
95
+ };
96
+ }
97
+
98
+ const sentence_match = paragraph_text.match(JSDOC_SENTENCE_PATTERN);
99
+
100
+ if (!sentence_match) {
101
+ return {
102
+ remainder: '',
103
+ title: paragraph_text,
104
+ };
105
+ }
106
+
107
+ return {
108
+ remainder: sentence_match[2].trim(),
109
+ title: sentence_match[1].trim(),
110
+ };
111
+ }
@@ -0,0 +1,242 @@
1
+ /**
2
+ * @import { PatramClaim, ParseClaimsInput, PatramClaimFields } from './parse-claims.types.ts';
3
+ */
4
+
5
+ import { createClaim, isPathLikeTarget } from './claim-helpers.js';
6
+ import {
7
+ matchHiddenDirectiveFields,
8
+ matchVisibleDirectiveFields,
9
+ parseFrontMatterDirectiveFields,
10
+ } from './parse-markdown-directives.js';
11
+
12
+ /**
13
+ * Markdown claim parsing.
14
+ *
15
+ * Extracts document titles, directives, and links from markdown source while
16
+ * ignoring fenced-code link noise.
17
+ *
18
+ * Kind: parse
19
+ * Status: active
20
+ * Tracked in: ../docs/plans/v0/source-anchor-dogfooding.md
21
+ * Decided by: ../docs/decisions/markdown-metadata-directive-syntax.md
22
+ * Decided by: ../docs/decisions/markdown-link-claim-scope.md
23
+ * @patram
24
+ * @see {@link ./parse-claims.js}
25
+ * @see {@link ../docs/decisions/markdown-metadata-directive-syntax.md}
26
+ */
27
+
28
+ const HEADING_PATTERN = /^#\s+(.+)$/du;
29
+ const MARKDOWN_FENCE_PATTERN = /^([`~]{3,})/du;
30
+ const MARKDOWN_LINK_PATTERN = /\[([^\]]+)\]\(([^)]+)\)/dgu;
31
+
32
+ /**
33
+ * @param {ParseClaimsInput} parse_input
34
+ * @returns {PatramClaim[]}
35
+ */
36
+ export function parseMarkdownClaims(parse_input) {
37
+ const lines = parse_input.source.split('\n');
38
+
39
+ /** @type {PatramClaim[]} */
40
+ const claims = [];
41
+ const front_matter_result = parseFrontMatterDirectiveFields(
42
+ parse_input.path,
43
+ lines,
44
+ );
45
+ /** @type {{ character: string, length: number } | null} */
46
+ let open_fence = null;
47
+ const title_result = getMarkdownTitle(lines, front_matter_result.body_start);
48
+
49
+ if (title_result) {
50
+ claims.push(
51
+ createClaim(parse_input.path, claims.length + 1, 'document.title', {
52
+ origin: {
53
+ column: 1,
54
+ line: title_result.line,
55
+ path: parse_input.path,
56
+ },
57
+ value: title_result.value,
58
+ }),
59
+ );
60
+ }
61
+
62
+ for (const directive_fields of front_matter_result.directive_fields) {
63
+ claims.push(
64
+ createClaim(parse_input.path, claims.length + 1, 'directive', {
65
+ ...directive_fields,
66
+ }),
67
+ );
68
+ }
69
+
70
+ for (const [line_index, line] of lines.entries()) {
71
+ if (line_index < front_matter_result.body_start) {
72
+ continue;
73
+ }
74
+
75
+ const line_number = line_index + 1;
76
+
77
+ if (open_fence) {
78
+ if (isClosingMarkdownFence(line, open_fence)) {
79
+ open_fence = null;
80
+ }
81
+
82
+ continue;
83
+ }
84
+
85
+ open_fence = parseOpeningMarkdownFence(line);
86
+
87
+ if (open_fence) {
88
+ continue;
89
+ }
90
+
91
+ collectMarkdownLinkClaims(parse_input.path, line, line_number, claims);
92
+ collectVisibleDirectiveClaims(parse_input.path, line, line_number, claims);
93
+ collectHiddenDirectiveClaims(parse_input.path, line, line_number, claims);
94
+ }
95
+
96
+ return claims;
97
+ }
98
+
99
+ /**
100
+ * @param {string} file_path
101
+ * @param {string} line
102
+ * @param {number} line_number
103
+ * @param {PatramClaim[]} claims
104
+ */
105
+ function collectMarkdownLinkClaims(file_path, line, line_number, claims) {
106
+ for (const link_match of line.matchAll(MARKDOWN_LINK_PATTERN)) {
107
+ const link_text = link_match[1];
108
+ const target_value = link_match[2];
109
+
110
+ if (!isPathLikeTarget(target_value)) {
111
+ continue;
112
+ }
113
+
114
+ const column_number =
115
+ link_match.index === undefined ? 1 : link_match.index + 1;
116
+
117
+ claims.push(
118
+ createClaim(file_path, claims.length + 1, 'markdown.link', {
119
+ origin: {
120
+ column: column_number,
121
+ line: line_number,
122
+ path: file_path,
123
+ },
124
+ value: {
125
+ target: target_value,
126
+ text: link_text,
127
+ },
128
+ }),
129
+ );
130
+ }
131
+ }
132
+
133
+ /**
134
+ * @param {string} file_path
135
+ * @param {string} line
136
+ * @param {number} line_number
137
+ * @param {PatramClaim[]} claims
138
+ */
139
+ function collectVisibleDirectiveClaims(file_path, line, line_number, claims) {
140
+ const directive_fields = matchVisibleDirectiveFields(
141
+ file_path,
142
+ line,
143
+ line_number,
144
+ );
145
+
146
+ if (directive_fields) {
147
+ pushDirectiveClaim(file_path, claims, directive_fields);
148
+ }
149
+ }
150
+
151
+ /**
152
+ * @param {string} file_path
153
+ * @param {string} line
154
+ * @param {number} line_number
155
+ * @param {PatramClaim[]} claims
156
+ */
157
+ function collectHiddenDirectiveClaims(file_path, line, line_number, claims) {
158
+ const directive_fields = matchHiddenDirectiveFields(
159
+ file_path,
160
+ line,
161
+ line_number,
162
+ );
163
+
164
+ if (directive_fields) {
165
+ pushDirectiveClaim(file_path, claims, directive_fields);
166
+ }
167
+ }
168
+
169
+ /**
170
+ * @param {string[]} lines
171
+ * @param {number} start_line_index
172
+ * @returns {{ line: number, value: string } | null}
173
+ */
174
+ function getMarkdownTitle(lines, start_line_index) {
175
+ const first_line = lines[start_line_index];
176
+
177
+ if (first_line === undefined) {
178
+ return null;
179
+ }
180
+
181
+ const trimmed_line = first_line.trim();
182
+
183
+ if (trimmed_line.length === 0) {
184
+ return null;
185
+ }
186
+
187
+ const heading_match = trimmed_line.match(HEADING_PATTERN);
188
+
189
+ if (heading_match) {
190
+ return {
191
+ line: start_line_index + 1,
192
+ value: heading_match[1].trim(),
193
+ };
194
+ }
195
+
196
+ return {
197
+ line: start_line_index + 1,
198
+ value: trimmed_line,
199
+ };
200
+ }
201
+
202
+ /**
203
+ * @param {string} line
204
+ * @returns {{ character: string, length: number } | null}
205
+ */
206
+ function parseOpeningMarkdownFence(line) {
207
+ const trimmed_line = line.trimStart();
208
+ const fence_match = trimmed_line.match(MARKDOWN_FENCE_PATTERN);
209
+
210
+ if (!fence_match) {
211
+ return null;
212
+ }
213
+
214
+ return {
215
+ character: fence_match[1][0],
216
+ length: fence_match[1].length,
217
+ };
218
+ }
219
+
220
+ /**
221
+ * @param {string} line
222
+ * @param {{ character: string, length: number }} open_fence
223
+ * @returns {boolean}
224
+ */
225
+ function isClosingMarkdownFence(line, open_fence) {
226
+ const trimmed_line = line.trimStart();
227
+
228
+ return trimmed_line.startsWith(
229
+ open_fence.character.repeat(open_fence.length),
230
+ );
231
+ }
232
+
233
+ /**
234
+ * @param {string} file_path
235
+ * @param {PatramClaim[]} claims
236
+ * @param {PatramClaimFields} directive_fields
237
+ */
238
+ function pushDirectiveClaim(file_path, claims, directive_fields) {
239
+ claims.push(
240
+ createClaim(file_path, claims.length + 1, 'directive', directive_fields),
241
+ );
242
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * @import { PatramClaimFields } from './parse-claims.types.ts';
3
+ */
4
+
5
+ const FRONT_MATTER_BOUNDARY_PATTERN = /^---$/du;
6
+ const FRONT_MATTER_DIRECTIVE_PATTERN = /^([A-Za-z][A-Za-z0-9 _-]*):\s+(.+)$/du;
7
+ const MARKDOWN_HIDDEN_DIRECTIVE_PATTERN =
8
+ /^\[patram\s+([A-Za-z][A-Za-z0-9 _-]*)=(.+)\]:\s*#\s*$/du;
9
+ const VISIBLE_DIRECTIVE_PATTERN = /^(?:-\s+)?([A-Z][A-Za-z _-]*):\s+(.+)$/du;
10
+
11
+ /**
12
+ * @param {string} file_path
13
+ * @param {string[]} lines
14
+ * @returns {{ body_start: number, directive_fields: PatramClaimFields[] }}
15
+ */
16
+ export function parseFrontMatterDirectiveFields(file_path, lines) {
17
+ if (lines[0] !== '---') {
18
+ return {
19
+ body_start: 0,
20
+ directive_fields: [],
21
+ };
22
+ }
23
+
24
+ const closing_line_index = findFrontMatterClosingLineIndex(lines);
25
+
26
+ if (closing_line_index < 0) {
27
+ return {
28
+ body_start: 0,
29
+ directive_fields: [],
30
+ };
31
+ }
32
+
33
+ /** @type {PatramClaimFields[]} */
34
+ const directive_fields = [];
35
+
36
+ for (let line_index = 1; line_index < closing_line_index; line_index += 1) {
37
+ const directive_fields_match = matchDirectiveFields(
38
+ FRONT_MATTER_DIRECTIVE_PATTERN,
39
+ file_path,
40
+ lines[line_index],
41
+ line_index + 1,
42
+ );
43
+
44
+ if (!directive_fields_match) {
45
+ continue;
46
+ }
47
+
48
+ directive_fields.push(directive_fields_match);
49
+ }
50
+
51
+ return {
52
+ body_start: closing_line_index + 1,
53
+ directive_fields,
54
+ };
55
+ }
56
+
57
+ /**
58
+ * @param {string} file_path
59
+ * @param {string} line
60
+ * @param {number} line_number
61
+ * @returns {PatramClaimFields | null}
62
+ */
63
+ export function matchVisibleDirectiveFields(file_path, line, line_number) {
64
+ return matchDirectiveFields(
65
+ VISIBLE_DIRECTIVE_PATTERN,
66
+ file_path,
67
+ line,
68
+ line_number,
69
+ );
70
+ }
71
+
72
+ /**
73
+ * @param {string} file_path
74
+ * @param {string} line
75
+ * @param {number} line_number
76
+ * @returns {PatramClaimFields | null}
77
+ */
78
+ export function matchHiddenDirectiveFields(file_path, line, line_number) {
79
+ return matchDirectiveFields(
80
+ MARKDOWN_HIDDEN_DIRECTIVE_PATTERN,
81
+ file_path,
82
+ line,
83
+ line_number,
84
+ );
85
+ }
86
+
87
+ /**
88
+ * @param {string[]} lines
89
+ * @returns {number}
90
+ */
91
+ function findFrontMatterClosingLineIndex(lines) {
92
+ for (let line_index = 1; line_index < lines.length; line_index += 1) {
93
+ if (FRONT_MATTER_BOUNDARY_PATTERN.test(lines[line_index])) {
94
+ return line_index;
95
+ }
96
+ }
97
+
98
+ return -1;
99
+ }
100
+
101
+ /**
102
+ * @param {RegExp} pattern
103
+ * @param {string} file_path
104
+ * @param {string} line
105
+ * @param {number} line_number
106
+ * @returns {PatramClaimFields | null}
107
+ */
108
+ function matchDirectiveFields(pattern, file_path, line, line_number) {
109
+ const directive_match = line.match(pattern);
110
+
111
+ if (!directive_match) {
112
+ return null;
113
+ }
114
+
115
+ return {
116
+ name: normalizeDirectiveName(directive_match[1]),
117
+ origin: {
118
+ column: 1,
119
+ line: line_number,
120
+ path: file_path,
121
+ },
122
+ parser: 'markdown',
123
+ value: directive_match[2].trim(),
124
+ };
125
+ }
126
+
127
+ /**
128
+ * @param {string} directive_label
129
+ * @returns {string}
130
+ */
131
+ export function normalizeDirectiveName(directive_label) {
132
+ return directive_label
133
+ .trim()
134
+ .toLowerCase()
135
+ .replaceAll(/[\s-]+/dgu, '_');
136
+ }