patram 0.11.0 → 0.12.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 (110) hide show
  1. package/bin/patram.js +4 -4
  2. package/lib/cli/commands/fields.js +0 -4
  3. package/lib/cli/commands/queries.js +10 -20
  4. package/lib/cli/commands/query.js +1 -8
  5. package/lib/cli/commands/refs.js +3 -10
  6. package/lib/cli/commands/show.js +1 -8
  7. package/lib/cli/help-metadata.js +71 -106
  8. package/lib/cli/main.js +10 -10
  9. package/lib/cli/parse-arguments-helpers.js +165 -59
  10. package/lib/cli/parse-arguments.js +4 -4
  11. package/lib/cli/render-help.js +2 -2
  12. package/lib/config/defaults.js +33 -25
  13. package/lib/config/load-patram-config.d.ts +8 -33
  14. package/lib/config/load-patram-config.js +9 -33
  15. package/lib/config/load-patram-config.types.d.ts +3 -40
  16. package/lib/config/manage-stored-queries-helpers.d.ts +4 -4
  17. package/lib/config/manage-stored-queries-helpers.js +91 -33
  18. package/lib/config/manage-stored-queries.d.ts +4 -4
  19. package/lib/config/manage-stored-queries.js +11 -5
  20. package/lib/config/patram-config.d.ts +34 -34
  21. package/lib/config/patram-config.js +3 -3
  22. package/lib/config/patram-config.types.d.ts +5 -11
  23. package/lib/config/resolve-patram-graph-config.d.ts +5 -1
  24. package/lib/config/resolve-patram-graph-config.js +3 -119
  25. package/lib/config/schema.d.ts +158 -269
  26. package/lib/config/schema.js +72 -210
  27. package/lib/config/validate-patram-config-value.js +6 -31
  28. package/lib/config/validation.d.ts +2 -12
  29. package/lib/config/validation.js +125 -483
  30. package/lib/find-close-match.d.ts +4 -1
  31. package/lib/graph/build-graph-identity.d.ts +1 -32
  32. package/lib/graph/build-graph-identity.js +5 -269
  33. package/lib/graph/build-graph.d.ts +13 -4
  34. package/lib/graph/build-graph.js +347 -488
  35. package/lib/graph/build-graph.types.d.ts +8 -9
  36. package/lib/graph/check-directive-metadata-helpers.d.ts +30 -0
  37. package/lib/graph/check-directive-metadata-helpers.js +126 -0
  38. package/lib/graph/check-directive-metadata.d.ts +8 -9
  39. package/lib/graph/check-directive-metadata.js +70 -561
  40. package/lib/graph/check-directive-path-target.d.ts +6 -13
  41. package/lib/graph/check-directive-path-target.js +26 -57
  42. package/lib/graph/check-directive-value.d.ts +1 -5
  43. package/lib/graph/check-directive-value.js +40 -180
  44. package/lib/graph/check-graph.d.ts +5 -5
  45. package/lib/graph/check-graph.js +8 -6
  46. package/lib/graph/document-node-identity.d.ts +23 -7
  47. package/lib/graph/document-node-identity.js +417 -160
  48. package/lib/graph/graph-node.d.ts +42 -0
  49. package/lib/graph/graph-node.js +83 -0
  50. package/lib/graph/inspect-reverse-references.js +16 -11
  51. package/lib/graph/load-project-graph.d.ts +7 -7
  52. package/lib/graph/load-project-graph.js +7 -7
  53. package/lib/graph/parse-where-clause.types.d.ts +3 -2
  54. package/lib/graph/query/cypher-reader.d.ts +59 -0
  55. package/lib/graph/query/cypher-reader.js +151 -0
  56. package/lib/graph/query/cypher-support.d.ts +79 -0
  57. package/lib/graph/query/cypher-support.js +213 -0
  58. package/lib/graph/query/cypher-tokenize.d.ts +13 -0
  59. package/lib/graph/query/cypher-tokenize.js +225 -0
  60. package/lib/graph/query/cypher.types.d.ts +43 -0
  61. package/lib/graph/query/execute.d.ts +7 -7
  62. package/lib/graph/query/execute.js +71 -33
  63. package/lib/graph/query/inspect.js +58 -24
  64. package/lib/graph/query/parse-cypher-patterns.d.ts +27 -0
  65. package/lib/graph/query/parse-cypher-patterns.js +382 -0
  66. package/lib/graph/query/parse-cypher.d.ts +7 -0
  67. package/lib/graph/query/parse-cypher.js +580 -0
  68. package/lib/graph/query/parse-query.d.ts +13 -0
  69. package/lib/graph/query/parse-query.js +97 -0
  70. package/lib/graph/query/resolve.js +77 -23
  71. package/lib/output/command-output.js +12 -5
  72. package/lib/output/compact-layout.js +221 -0
  73. package/lib/output/format-output-item-block.js +31 -1
  74. package/lib/output/format-output-metadata.js +16 -29
  75. package/lib/output/format-stored-query-block.js +95 -0
  76. package/lib/output/layout-incoming-references.js +101 -19
  77. package/lib/output/layout-stored-queries.js +23 -330
  78. package/lib/output/list-queries.js +1 -1
  79. package/lib/output/render-field-discovery.js +11 -2
  80. package/lib/output/render-output-view.js +9 -5
  81. package/lib/output/renderers/json.js +5 -26
  82. package/lib/output/renderers/plain.js +155 -35
  83. package/lib/output/renderers/rich.js +250 -36
  84. package/lib/output/resolved-link-layout.js +43 -0
  85. package/lib/output/rich-source/render.js +193 -35
  86. package/lib/output/show-document.js +25 -18
  87. package/lib/output/view-model/index.js +124 -103
  88. package/lib/parse/jsdoc/parse-jsdoc-blocks.js +1 -1
  89. package/lib/parse/jsdoc/parse-jsdoc-claims.js +12 -6
  90. package/lib/parse/markdown/parse-markdown-claims.js +99 -62
  91. package/lib/parse/markdown/parse-markdown-directives.d.ts +10 -6
  92. package/lib/parse/markdown/parse-markdown-directives.js +104 -18
  93. package/lib/parse/markdown/parse-markdown-prose.d.ts +27 -0
  94. package/lib/parse/markdown/parse-markdown-prose.js +243 -0
  95. package/lib/parse/parse-claims.d.ts +2 -6
  96. package/lib/parse/parse-claims.js +11 -53
  97. package/lib/parse/tagged-fenced/tagged-fenced-blocks.d.ts +4 -4
  98. package/lib/parse/tagged-fenced/tagged-fenced-blocks.js +4 -4
  99. package/lib/parse/yaml/parse-yaml-claims.js +4 -4
  100. package/lib/patram.d.ts +3 -5
  101. package/lib/patram.js +1 -1
  102. package/lib/scan/discover-fields.js +194 -55
  103. package/lib/scan/list-source-files.d.ts +4 -4
  104. package/lib/scan/list-source-files.js +4 -4
  105. package/package.json +1 -1
  106. package/lib/directive-validation-test-helpers.js +0 -87
  107. package/lib/graph/query/parse.d.ts +0 -75
  108. package/lib/graph/query/parse.js +0 -1064
  109. package/lib/output/derived-summary.js +0 -280
  110. package/lib/output/format-derived-summary-row.js +0 -9
@@ -0,0 +1,213 @@
1
+ /**
2
+ * @import { PatramDiagnostic, PatramRepoConfig } from '../../config/load-patram-config.types.ts';
3
+ * @import {
4
+ * ParsedAggregateComparison,
5
+ * ParsedExpression,
6
+ * ParsedFieldTerm,
7
+ * ParsedTerm,
8
+ * } from '../parse-where-clause.types.ts';
9
+ * @import { CypherNodePattern } from './cypher.types.ts';
10
+ */
11
+
12
+ /**
13
+ * @param {number} column
14
+ * @param {string} message
15
+ * @returns {{ diagnostic: PatramDiagnostic, success: false }}
16
+ */
17
+ export function fail(column, message) {
18
+ return {
19
+ diagnostic: {
20
+ code: 'query.invalid',
21
+ column,
22
+ level: 'error',
23
+ line: 1,
24
+ message,
25
+ path: '<query>',
26
+ },
27
+ success: false,
28
+ };
29
+ }
30
+
31
+ /**
32
+ * @param {string} relation_type
33
+ * @returns {string}
34
+ */
35
+ export function relationTypeToRelationName(relation_type) {
36
+ return relation_type.toLowerCase();
37
+ }
38
+
39
+ /**
40
+ * @param {string} value
41
+ * @returns {string}
42
+ */
43
+ function toPascalCase(value) {
44
+ return value
45
+ .split('_')
46
+ .map((part) => part[0]?.toUpperCase() + part.slice(1))
47
+ .join('');
48
+ }
49
+
50
+ /**
51
+ * @param {string} value
52
+ * @returns {string}
53
+ */
54
+ function toLowerSnakeCase(value) {
55
+ return value
56
+ .replace(/([a-z0-9])([A-Z])/gu, '$1_$2')
57
+ .replace(/[\s-]+/gu, '_')
58
+ .toLowerCase();
59
+ }
60
+
61
+ /**
62
+ * @param {string} value
63
+ * @returns {value is ParsedAggregateComparison}
64
+ */
65
+ export function isAggregateComparison(value) {
66
+ return ['<', '<=', '<>', '=', '>', '>='].includes(value);
67
+ }
68
+
69
+ /**
70
+ * @param {CypherNodePattern} node_pattern
71
+ * @param {PatramRepoConfig | null} repo_config
72
+ * @returns {ParsedExpression | null}
73
+ */
74
+ export function createNodeLabelExpression(node_pattern, repo_config) {
75
+ if (!node_pattern.label_name) {
76
+ return null;
77
+ }
78
+
79
+ return {
80
+ kind: 'term',
81
+ term: createFieldTerm(
82
+ '$class',
83
+ '=',
84
+ resolveClassName(node_pattern.label_name, repo_config),
85
+ node_pattern.column,
86
+ ),
87
+ };
88
+ }
89
+
90
+ /**
91
+ * @param {string} field_name
92
+ * @param {ParsedFieldTerm['operator']} operator
93
+ * @param {string} value
94
+ * @param {number} column
95
+ * @returns {{ success: true, expression: ParsedExpression }}
96
+ */
97
+ export function createFieldExpression(field_name, operator, value, column) {
98
+ return {
99
+ expression: {
100
+ kind: 'term',
101
+ term: createFieldTerm(field_name, operator, value, column),
102
+ },
103
+ success: true,
104
+ };
105
+ }
106
+
107
+ /**
108
+ * @param {string} field_name
109
+ * @param {'in' | 'not in'} operator
110
+ * @param {string[]} values
111
+ * @param {number} column
112
+ * @returns {{ success: true, expression: ParsedExpression }}
113
+ */
114
+ export function createFieldSetExpression(field_name, operator, values, column) {
115
+ return {
116
+ expression: {
117
+ kind: 'term',
118
+ term: {
119
+ column,
120
+ field_name,
121
+ kind: 'field_set',
122
+ operator,
123
+ values,
124
+ },
125
+ },
126
+ success: true,
127
+ };
128
+ }
129
+
130
+ /**
131
+ * @param {number} column
132
+ * @returns {ParsedExpression}
133
+ */
134
+ export function createAlwaysTrueExpression(column) {
135
+ return {
136
+ kind: 'term',
137
+ term: createFieldTerm('$id', '^=', '', column),
138
+ };
139
+ }
140
+
141
+ /**
142
+ * @param {ParsedExpression[]} expressions
143
+ * @returns {ParsedExpression}
144
+ */
145
+ export function collapseAndExpressions(expressions) {
146
+ return collapseBooleanExpression('and', expressions);
147
+ }
148
+
149
+ /**
150
+ * @param {'and' | 'or'} kind
151
+ * @param {ParsedExpression[]} expressions
152
+ * @returns {ParsedExpression}
153
+ */
154
+ export function collapseBooleanExpression(kind, expressions) {
155
+ if (expressions.length === 0) {
156
+ return createAlwaysTrueExpression(1);
157
+ }
158
+
159
+ if (expressions.length === 1) {
160
+ return expressions[0];
161
+ }
162
+
163
+ return {
164
+ expressions,
165
+ kind,
166
+ };
167
+ }
168
+
169
+ /**
170
+ * @param {string} label_name
171
+ * @param {PatramRepoConfig | null} repo_config
172
+ * @returns {string}
173
+ */
174
+ function resolveClassName(label_name, repo_config) {
175
+ if (
176
+ label_name === 'Document' ||
177
+ label_name === 'document' ||
178
+ label_name === toPascalCase('document')
179
+ ) {
180
+ return 'document';
181
+ }
182
+
183
+ for (const [class_name, class_definition] of Object.entries(
184
+ repo_config?.types ?? {},
185
+ )) {
186
+ if (
187
+ class_definition.label === label_name ||
188
+ class_name === label_name ||
189
+ toPascalCase(class_name) === label_name
190
+ ) {
191
+ return class_name;
192
+ }
193
+ }
194
+
195
+ return toLowerSnakeCase(label_name);
196
+ }
197
+
198
+ /**
199
+ * @param {string} field_name
200
+ * @param {ParsedFieldTerm['operator']} operator
201
+ * @param {string} value
202
+ * @param {number} column
203
+ * @returns {ParsedTerm}
204
+ */
205
+ function createFieldTerm(field_name, operator, value, column) {
206
+ return {
207
+ column,
208
+ field_name,
209
+ kind: 'field',
210
+ operator,
211
+ value,
212
+ };
213
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @param {string} query_text
3
+ * @returns {{ success: true, tokens: CypherToken[] } | { diagnostic: PatramDiagnostic, success: false }}
4
+ */
5
+ export function tokenizeCypher(query_text: string): {
6
+ success: true;
7
+ tokens: CypherToken[];
8
+ } | {
9
+ diagnostic: PatramDiagnostic;
10
+ success: false;
11
+ };
12
+ import type { CypherToken } from './cypher.types.d.ts';
13
+ import type { PatramDiagnostic } from '../../config/load-patram-config.types.d.ts';
@@ -0,0 +1,225 @@
1
+ /**
2
+ * @import { PatramDiagnostic } from '../../config/load-patram-config.types.ts';
3
+ * @import { CypherToken } from './cypher.types.ts';
4
+ */
5
+
6
+ import { fail } from './cypher-support.js';
7
+
8
+ const DOUBLE_CHAR_SYMBOLS = ['->', '<-', '<=', '>=', '<>'];
9
+ const SINGLE_CHAR_SYMBOLS = '(){}[]:,.=-<>@';
10
+ const IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*/u;
11
+ const NUMBER_PATTERN = /^\d+/u;
12
+
13
+ /**
14
+ * @param {string} query_text
15
+ * @returns {{ success: true, tokens: CypherToken[] } | { diagnostic: PatramDiagnostic, success: false }}
16
+ */
17
+ export function tokenizeCypher(query_text) {
18
+ /** @type {CypherToken[]} */
19
+ const tokens = [];
20
+ let index = 0;
21
+
22
+ while (index < query_text.length) {
23
+ const char = query_text[index];
24
+
25
+ if (/\s/u.test(char)) {
26
+ index += 1;
27
+ continue;
28
+ }
29
+
30
+ const column = index + 1;
31
+ const string_result = readStringToken(query_text, index, column);
32
+
33
+ if (string_result) {
34
+ if (!string_result.success) {
35
+ return string_result;
36
+ }
37
+
38
+ tokens.push(string_result.token);
39
+ index = string_result.next_index;
40
+ continue;
41
+ }
42
+
43
+ const simple_token = readSimpleToken(query_text, index, column);
44
+
45
+ if (simple_token) {
46
+ tokens.push(simple_token.token);
47
+ index = simple_token.next_index;
48
+ continue;
49
+ }
50
+
51
+ return fail(column, `Unsupported query token "${char}".`);
52
+ }
53
+
54
+ return {
55
+ success: true,
56
+ tokens,
57
+ };
58
+ }
59
+
60
+ /**
61
+ * @param {string} query_text
62
+ * @param {number} start_index
63
+ * @param {number} column
64
+ * @returns {{ success: true, next_index: number, token: CypherToken } | { diagnostic: PatramDiagnostic, success: false } | null}
65
+ */
66
+ function readStringToken(query_text, start_index, column) {
67
+ const quote = query_text[start_index];
68
+
69
+ if (quote !== "'" && quote !== '"') {
70
+ return null;
71
+ }
72
+
73
+ const string_result = readQuotedString(query_text, start_index);
74
+
75
+ if (!string_result.success) {
76
+ return string_result;
77
+ }
78
+
79
+ return {
80
+ next_index: string_result.next_index,
81
+ success: true,
82
+ token: {
83
+ column,
84
+ index: start_index,
85
+ kind: 'string',
86
+ value: string_result.value,
87
+ },
88
+ };
89
+ }
90
+
91
+ /**
92
+ * @param {string} query_text
93
+ * @param {number} start_index
94
+ * @param {number} column
95
+ * @returns {{ next_index: number, token: CypherToken } | null}
96
+ */
97
+ function readSimpleToken(query_text, start_index, column) {
98
+ return (
99
+ readDoubleCharSymbol(query_text, start_index, column) ??
100
+ readSingleCharSymbol(query_text, start_index, column) ??
101
+ readPatternToken(
102
+ query_text,
103
+ start_index,
104
+ column,
105
+ NUMBER_PATTERN,
106
+ 'number',
107
+ ) ??
108
+ readPatternToken(
109
+ query_text,
110
+ start_index,
111
+ column,
112
+ IDENTIFIER_PATTERN,
113
+ 'identifier',
114
+ )
115
+ );
116
+ }
117
+
118
+ /**
119
+ * @param {string} query_text
120
+ * @param {number} start_index
121
+ * @param {number} column
122
+ * @returns {{ next_index: number, token: CypherToken } | null}
123
+ */
124
+ function readDoubleCharSymbol(query_text, start_index, column) {
125
+ const value = query_text.slice(start_index, start_index + 2);
126
+
127
+ if (!DOUBLE_CHAR_SYMBOLS.includes(value)) {
128
+ return null;
129
+ }
130
+
131
+ return createTokenResult(start_index, column, 'symbol', value);
132
+ }
133
+
134
+ /**
135
+ * @param {string} query_text
136
+ * @param {number} start_index
137
+ * @param {number} column
138
+ * @returns {{ next_index: number, token: CypherToken } | null}
139
+ */
140
+ function readSingleCharSymbol(query_text, start_index, column) {
141
+ const value = query_text[start_index];
142
+
143
+ if (!SINGLE_CHAR_SYMBOLS.includes(value)) {
144
+ return null;
145
+ }
146
+
147
+ return createTokenResult(start_index, column, 'symbol', value);
148
+ }
149
+
150
+ /**
151
+ * @param {string} query_text
152
+ * @param {number} start_index
153
+ * @param {number} column
154
+ * @param {RegExp} pattern
155
+ * @param {'identifier' | 'number'} kind
156
+ * @returns {{ next_index: number, token: CypherToken } | null}
157
+ */
158
+ function readPatternToken(query_text, start_index, column, pattern, kind) {
159
+ const match = query_text.slice(start_index).match(pattern);
160
+
161
+ if (!match) {
162
+ return null;
163
+ }
164
+
165
+ return createTokenResult(start_index, column, kind, match[0]);
166
+ }
167
+
168
+ /**
169
+ * @param {number} index
170
+ * @param {number} column
171
+ * @param {CypherToken['kind']} kind
172
+ * @param {string} value
173
+ * @returns {{ next_index: number, token: CypherToken }}
174
+ */
175
+ function createTokenResult(index, column, kind, value) {
176
+ return {
177
+ next_index: index + value.length,
178
+ token: {
179
+ column,
180
+ index,
181
+ kind,
182
+ value,
183
+ },
184
+ };
185
+ }
186
+
187
+ /**
188
+ * @param {string} query_text
189
+ * @param {number} start_index
190
+ * @returns {{ success: true, next_index: number, value: string } | { diagnostic: PatramDiagnostic, success: false }}
191
+ */
192
+ function readQuotedString(query_text, start_index) {
193
+ const quote = query_text[start_index];
194
+ let index = start_index + 1;
195
+ let value = '';
196
+
197
+ while (index < query_text.length) {
198
+ const char = query_text[index];
199
+
200
+ if (char === '\\') {
201
+ const escaped_char = query_text[index + 1];
202
+
203
+ if (escaped_char === undefined) {
204
+ break;
205
+ }
206
+
207
+ value += escaped_char;
208
+ index += 2;
209
+ continue;
210
+ }
211
+
212
+ if (char === quote) {
213
+ return {
214
+ next_index: index + 1,
215
+ success: true,
216
+ value,
217
+ };
218
+ }
219
+
220
+ value += char;
221
+ index += 1;
222
+ }
223
+
224
+ return fail(start_index + 1, 'Unterminated string literal.');
225
+ }
@@ -0,0 +1,43 @@
1
+ import type { PatramDiagnostic, PatramRepoConfig } from '../../config/load-patram-config.types.d.ts';
2
+ import type { ParsedAggregateTerm, ParsedExpression, ParsedTraversalTerm } from '../parse-where-clause.types.d.ts';
3
+ export interface CypherToken {
4
+ column: number;
5
+ index: number;
6
+ kind: 'identifier' | 'number' | 'string' | 'symbol';
7
+ value: string;
8
+ }
9
+ export interface CypherParserState {
10
+ bindings: Record<string, string>;
11
+ index: number;
12
+ query_text: string;
13
+ repo_config: PatramRepoConfig | null;
14
+ root_variable_name: string | null;
15
+ tokens: CypherToken[];
16
+ }
17
+ export interface CypherNodePattern {
18
+ column: number;
19
+ label_name: string | null;
20
+ variable_name: string | null;
21
+ }
22
+ export interface CypherRelationPattern {
23
+ column: number;
24
+ direction: 'in' | 'out';
25
+ relation_name: string;
26
+ }
27
+ export type CypherParseFailure = {
28
+ diagnostic: PatramDiagnostic;
29
+ success: false;
30
+ };
31
+ export type CypherAggregateResult = {
32
+ success: true;
33
+ term: ParsedAggregateTerm;
34
+ } | CypherParseFailure;
35
+ export type CypherSubqueryShapeResult = {
36
+ success: true;
37
+ related_node: CypherNodePattern;
38
+ traversal: ParsedTraversalTerm;
39
+ } | CypherParseFailure;
40
+ export type CypherExpressionResult = {
41
+ success: true;
42
+ expression: ParsedExpression;
43
+ } | CypherParseFailure;
@@ -37,13 +37,13 @@ export function queryGraph(graph: BuildGraphResult, where_clause: string, repo_c
37
37
  * Applies the v0 where-clause language to graph nodes and keeps pagination
38
38
  * separate from matching.
39
39
  *
40
- * Kind: graph
41
- * Status: active
42
- * Uses Term: ../../../docs/reference/terms/graph.md
43
- * Uses Term: ../../../docs/reference/terms/query.md
44
- * Tracked in: ../../../docs/plans/v0/source-anchor-dogfooding.md
45
- * Decided by: ../../../docs/decisions/query-language.md
46
- * Implements: ../../../docs/tasks/v0/query-command.md
40
+ * kind: graph
41
+ * status: active
42
+ * uses_term: ../../../docs/reference/terms/graph.md
43
+ * uses_term: ../../../docs/reference/terms/query.md
44
+ * tracked_in: ../../../docs/plans/v0/source-anchor-dogfooding.md
45
+ * decided_by: ../../../docs/decisions/query-language.md
46
+ * implements: ../../../docs/tasks/v0/query-command.md
47
47
  * @patram
48
48
  * @see {@link ../../../docs/decisions/query-language.md}
49
49
  * @see {@link ../load-project-graph.js}