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
@@ -1,26 +1,19 @@
1
1
  /**
2
- * @import { ClassDefinition } from './patram-config.js';
3
- * @import {
4
- * DerivedSummaryFieldConfig,
5
- * MetadataFieldConfig,
6
- * PatramRepoConfig,
7
- * } from './schema.js';
8
- * @import { PatramDiagnostic } from './load-patram-config.js';
2
+ * @import { MetadataFieldConfig, PatramRepoConfig, PatramDiagnostic } from './load-patram-config.types.ts';
9
3
  */
10
- /* eslint-disable max-lines */
11
4
 
12
- import { z } from 'zod';
13
-
14
- import { parsePatramConfig } from './patram-config.js';
5
+ import { CONFIG_FILE_NAME, isReservedStructuralFieldName } from './schema.js';
15
6
  import { getQuerySemanticDiagnostics } from '../graph/query/inspect.js';
16
- import { parseWhereClause } from '../graph/query/parse.js';
17
- import { resolvePatramGraphConfig } from './resolve-patram-graph-config.js';
18
- import {
19
- CONFIG_FILE_NAME,
20
- isKnownMarkdownStyle,
21
- isMixedStyleValue,
22
- isReservedStructuralFieldName,
23
- } from './schema.js';
7
+ import { parseQueryExpression } from '../graph/query/parse-query.js';
8
+
9
+ const LEGACY_TOP_LEVEL_KEYS = {
10
+ class_schemas:
11
+ 'Top-level "class_schemas" is not supported. Use "types" and field rules.',
12
+ classes: 'Top-level "classes" is not supported. Use "types".',
13
+ mappings: 'Top-level "mappings" is not supported.',
14
+ path_classes: 'Top-level "path_classes" is not supported.',
15
+ relations: 'Top-level "relations" is not supported. Use ref fields.',
16
+ };
24
17
 
25
18
  /**
26
19
  * @param {unknown} config_value
@@ -30,47 +23,23 @@ export function validateLegacyConfigShape(config_value) {
30
23
  if (
31
24
  config_value === null ||
32
25
  typeof config_value !== 'object' ||
33
- !Object.hasOwn(config_value, 'class_schemas')
26
+ Array.isArray(config_value)
34
27
  ) {
35
28
  return [];
36
29
  }
37
30
 
38
- return [
39
- createConfigDiagnostic(
40
- 'class_schemas',
41
- 'Top-level "class_schemas" is not supported. Move entries into classes.<name>.schema.',
42
- ),
43
- ];
44
- }
45
-
46
- /**
47
- * @param {PatramRepoConfig} repo_config
48
- * @returns {PatramDiagnostic[]}
49
- */
50
- export function validateGraphSchema(repo_config) {
51
- if (
52
- repo_config.classes === undefined &&
53
- repo_config.mappings === undefined &&
54
- repo_config.relations === undefined
55
- ) {
56
- return [];
57
- }
31
+ /** @type {PatramDiagnostic[]} */
32
+ const diagnostics = [];
58
33
 
59
- try {
60
- parsePatramConfig({
61
- classes: collectGraphClassDefinitions(repo_config.classes),
62
- mappings: repo_config.mappings ?? {},
63
- relations: repo_config.relations ?? {},
64
- });
65
- } catch (error) {
66
- if (error instanceof z.ZodError) {
67
- return error.issues.map(createValidationDiagnostic);
34
+ for (const [legacy_key, message] of Object.entries(LEGACY_TOP_LEVEL_KEYS)) {
35
+ if (!Object.hasOwn(config_value, legacy_key)) {
36
+ continue;
68
37
  }
69
38
 
70
- throw error;
39
+ diagnostics.push(createConfigDiagnostic(legacy_key, message));
71
40
  }
72
41
 
73
- return [];
42
+ return diagnostics;
74
43
  }
75
44
 
76
45
  /**
@@ -78,19 +47,22 @@ export function validateGraphSchema(repo_config) {
78
47
  * @returns {PatramDiagnostic[]}
79
48
  */
80
49
  export function validateFieldSchemaConfig(repo_config) {
81
- const path_classes = repo_config.path_classes ?? {};
82
- const classes = repo_config.classes ?? {};
83
50
  const fields = repo_config.fields ?? {};
51
+ const types = repo_config.types ?? {};
84
52
  /** @type {PatramDiagnostic[]} */
85
53
  const diagnostics = [];
86
54
 
87
- collectFieldConfigDiagnostics(diagnostics, path_classes, fields);
88
- collectClassSchemaConfigDiagnostics(
89
- diagnostics,
90
- path_classes,
91
- fields,
92
- classes,
93
- );
55
+ for (const [field_name, field_definition] of Object.entries(fields)) {
56
+ diagnostics.push(
57
+ ...validateFieldDefinition(field_name, field_definition, types),
58
+ );
59
+ }
60
+
61
+ for (const [type_name, type_definition] of Object.entries(types)) {
62
+ diagnostics.push(
63
+ ...validateTypeDefinition(type_name, type_definition, fields),
64
+ );
65
+ }
94
66
 
95
67
  return diagnostics;
96
68
  }
@@ -106,510 +78,180 @@ export function validateStoredQueries(repo_config) {
106
78
  for (const [query_name, stored_query] of Object.entries(
107
79
  repo_config.queries,
108
80
  )) {
109
- collectWhereClauseDiagnostics(
110
- diagnostics,
111
- repo_config,
112
- stored_query.where,
113
- `queries.${query_name}.where`,
114
- );
115
- }
81
+ const query_text = stored_query.cypher;
116
82
 
117
- return diagnostics;
118
- }
119
-
120
- /**
121
- * @param {PatramRepoConfig} repo_config
122
- * @returns {PatramDiagnostic[]}
123
- */
124
- export function validateDerivedSummaries(repo_config) {
125
- if (!repo_config.derived_summaries) {
126
- return [];
127
- }
128
-
129
- const graph_config = resolvePatramGraphConfig(repo_config);
130
- const known_relation_names = new Set(Object.keys(graph_config.relations));
131
- /** @type {PatramDiagnostic[]} */
132
- const diagnostics = [];
133
- /** @type {Map<string, string>} */
134
- const class_coverage = new Map();
83
+ if (!query_text) {
84
+ continue;
85
+ }
135
86
 
136
- for (const [summary_name, summary_definition] of Object.entries(
137
- repo_config.derived_summaries,
138
- )) {
139
- collectDuplicateClassDiagnostics(
140
- diagnostics,
141
- class_coverage,
142
- summary_definition.classes,
143
- summary_name,
144
- );
145
- collectDerivedSummaryFieldDiagnostics(
87
+ collectWhereClauseDiagnostics(
146
88
  diagnostics,
147
- known_relation_names,
148
89
  repo_config,
149
- summary_name,
150
- summary_definition.fields,
90
+ query_text,
91
+ `queries.${query_name}.cypher`,
151
92
  );
152
93
  }
153
94
 
154
95
  return diagnostics;
155
96
  }
156
97
 
157
- /**
158
- * @param {import('zod').core.$ZodIssue} issue
159
- * @returns {PatramDiagnostic}
160
- */
161
- function createValidationDiagnostic(issue) {
162
- const issue_path = formatIssuePath(issue.path);
163
-
164
- if (issue_path) {
165
- return {
166
- code: 'config.invalid',
167
- column: 1,
168
- level: 'error',
169
- line: 1,
170
- message: `Invalid config at "${issue_path}": ${issue.message}`,
171
- path: CONFIG_FILE_NAME,
172
- };
173
- }
174
-
175
- return {
176
- code: 'config.invalid',
177
- column: 1,
178
- level: 'error',
179
- line: 1,
180
- message: `Invalid config: ${issue.message}`,
181
- path: CONFIG_FILE_NAME,
182
- };
183
- }
184
-
185
98
  /**
186
99
  * @param {PatramDiagnostic[]} diagnostics
187
- * @param {Map<string, string>} class_coverage
188
- * @param {string[]} class_names
189
- * @param {string} summary_name
100
+ * @param {string} field_path
101
+ * @param {string[] | undefined} referenced_types
102
+ * @param {NonNullable<PatramRepoConfig['types']>} types
190
103
  */
191
- function collectDuplicateClassDiagnostics(
104
+ function collectTypeReferenceDiagnostics(
192
105
  diagnostics,
193
- class_coverage,
194
- class_names,
195
- summary_name,
106
+ field_path,
107
+ referenced_types,
108
+ types,
196
109
  ) {
197
- for (const class_name of class_names) {
198
- const existing_summary_name = class_coverage.get(class_name);
199
-
200
- if (!existing_summary_name) {
201
- class_coverage.set(class_name, summary_name);
110
+ for (const referenced_type of referenced_types ?? []) {
111
+ if (types[referenced_type] !== undefined) {
202
112
  continue;
203
113
  }
204
114
 
205
115
  diagnostics.push(
206
- createConfigDiagnostic(
207
- `derived_summaries.${summary_name}.classes`,
208
- `Class "${class_name}" is already covered by derived summary "${existing_summary_name}".`,
209
- ),
116
+ createConfigDiagnostic(field_path, `Unknown type "${referenced_type}".`),
210
117
  );
211
118
  }
212
119
  }
213
120
 
214
121
  /**
215
122
  * @param {PatramDiagnostic[]} diagnostics
216
- * @param {Set<string>} known_relation_names
217
123
  * @param {PatramRepoConfig} repo_config
218
- * @param {string} summary_name
219
- * @param {DerivedSummaryFieldConfig[]} field_definitions
220
- */
221
- function collectDerivedSummaryFieldDiagnostics(
222
- diagnostics,
223
- known_relation_names,
224
- repo_config,
225
- summary_name,
226
- field_definitions,
227
- ) {
228
- for (const [field_index, field_definition] of field_definitions.entries()) {
229
- if ('count' in field_definition) {
230
- collectTraversalDiagnostic(
231
- diagnostics,
232
- field_definition.count.traversal,
233
- known_relation_names,
234
- `derived_summaries.${summary_name}.fields.${field_index}.count.traversal`,
235
- );
236
- collectWhereClauseDiagnostics(
237
- diagnostics,
238
- repo_config,
239
- field_definition.count.where,
240
- `derived_summaries.${summary_name}.fields.${field_index}.count.where`,
241
- );
242
- continue;
243
- }
244
-
245
- for (const [case_index, select_case] of field_definition.select.entries()) {
246
- collectWhereClauseDiagnostics(
247
- diagnostics,
248
- repo_config,
249
- select_case.when,
250
- `derived_summaries.${summary_name}.fields.${field_index}.select.${case_index}.when`,
251
- );
252
- }
253
- }
254
- }
255
-
256
- /**
257
- * @param {PatramDiagnostic[]} diagnostics
258
- * @param {string} traversal_text
259
- * @param {Set<string>} known_relation_names
124
+ * @param {string} where_clause
260
125
  * @param {string} diagnostic_path
261
126
  */
262
- function collectTraversalDiagnostic(
127
+ function collectWhereClauseDiagnostics(
263
128
  diagnostics,
264
- traversal_text,
265
- known_relation_names,
129
+ repo_config,
130
+ where_clause,
266
131
  diagnostic_path,
267
132
  ) {
268
- const traversal_match =
269
- /^(?<direction>in|out):(?<relation_name>[a-zA-Z0-9_]+)$/du.exec(
270
- traversal_text,
271
- );
133
+ const parse_result = parseQueryExpression(where_clause, repo_config);
272
134
 
273
- if (!traversal_match?.groups?.relation_name) {
274
- diagnostics.push(
275
- createConfigDiagnostic(
276
- diagnostic_path,
277
- 'Derived summary traversal must use "in:<relation>" or "out:<relation>".',
278
- ),
279
- );
280
-
281
- return;
282
- }
283
-
284
- if (known_relation_names.has(traversal_match.groups.relation_name)) {
135
+ if (!parse_result.success) {
136
+ diagnostics.push(replaceDiagnosticPath(parse_result.diagnostic));
285
137
  return;
286
138
  }
287
139
 
288
140
  diagnostics.push(
289
- createConfigDiagnostic(
290
- diagnostic_path,
291
- `Unknown relation "${traversal_match.groups.relation_name}" in derived summary traversal.`,
292
- ),
293
- );
294
- }
295
-
296
- /**
297
- * @param {PatramDiagnostic[]} diagnostics
298
- * @param {Record<string, { prefixes: string[] }>} path_classes
299
- * @param {Record<string, MetadataFieldConfig>} fields
300
- */
301
- function collectFieldConfigDiagnostics(diagnostics, path_classes, fields) {
302
- for (const [field_name, field_definition] of Object.entries(fields)) {
303
- if (collectReservedFieldDiagnostic(diagnostics, field_name)) {
304
- continue;
305
- }
306
-
307
- collectDisplayOrderDiagnostic(diagnostics, field_name, field_definition);
308
- collectFieldPathClassDiagnostic(
309
- diagnostics,
310
- path_classes,
311
- field_name,
312
- field_definition,
313
- );
314
- }
315
- }
316
-
317
- /**
318
- * @param {PatramDiagnostic[]} diagnostics
319
- * @param {string} field_name
320
- * @returns {boolean}
321
- */
322
- function collectReservedFieldDiagnostic(diagnostics, field_name) {
323
- if (
324
- !field_name.startsWith('$') ||
325
- !isReservedStructuralFieldName(field_name)
326
- ) {
327
- return false;
328
- }
329
-
330
- diagnostics.push(
331
- createConfigDiagnostic(
332
- `fields.${field_name}`,
333
- 'Metadata field names must not start with "$".',
141
+ ...getQuerySemanticDiagnostics(
142
+ repo_config,
143
+ { kind: 'stored_query', name: diagnostic_path.split('.')[1] ?? '' },
144
+ parse_result.expression,
334
145
  ),
335
146
  );
336
-
337
- return true;
338
147
  }
339
148
 
340
149
  /**
341
- * @param {PatramDiagnostic[]} diagnostics
342
- * @param {string} field_name
343
- * @param {MetadataFieldConfig} field_definition
150
+ * @param {string} path
151
+ * @param {string} message
152
+ * @returns {PatramDiagnostic}
344
153
  */
345
- function collectDisplayOrderDiagnostic(
346
- diagnostics,
347
- field_name,
348
- field_definition,
349
- ) {
350
- if (
351
- field_definition.display?.order === undefined ||
352
- (Number.isInteger(field_definition.display.order) &&
353
- field_definition.display.order >= 0)
354
- ) {
355
- return;
356
- }
357
-
358
- diagnostics.push(
359
- createConfigDiagnostic(
360
- `fields.${field_name}.display.order`,
361
- 'Display order must be a non-negative integer.',
362
- ),
363
- );
154
+ function createConfigDiagnostic(path, message) {
155
+ return {
156
+ code: 'config.invalid',
157
+ column: 1,
158
+ level: 'error',
159
+ line: 1,
160
+ message: `Invalid config at "${path}": ${message}`,
161
+ path: CONFIG_FILE_NAME,
162
+ };
364
163
  }
365
164
 
366
165
  /**
367
- * @param {PatramDiagnostic[]} diagnostics
368
- * @param {Record<string, { prefixes: string[] }>} path_classes
369
166
  * @param {string} field_name
370
167
  * @param {MetadataFieldConfig} field_definition
168
+ * @param {NonNullable<PatramRepoConfig['types']>} types
169
+ * @returns {PatramDiagnostic[]}
371
170
  */
372
- function collectFieldPathClassDiagnostic(
373
- diagnostics,
374
- path_classes,
375
- field_name,
376
- field_definition,
377
- ) {
378
- if (
379
- !('path_class' in field_definition) ||
380
- field_definition.path_class === undefined
381
- ) {
382
- return;
383
- }
171
+ function validateFieldDefinition(field_name, field_definition, types) {
172
+ /** @type {PatramDiagnostic[]} */
173
+ const diagnostics = [];
384
174
 
385
- if (field_definition.type !== 'path') {
175
+ if (isReservedStructuralFieldName(field_name)) {
386
176
  diagnostics.push(
387
177
  createConfigDiagnostic(
388
- `fields.${field_name}.path_class`,
389
- 'Path classes are only valid for path fields.',
178
+ `fields.${field_name}`,
179
+ 'Metadata field names must not start with "$".',
390
180
  ),
391
181
  );
392
-
393
- return;
394
182
  }
395
183
 
396
- if (path_classes[field_definition.path_class]) {
397
- return;
398
- }
399
-
400
- diagnostics.push(
401
- createConfigDiagnostic(
402
- `fields.${field_name}.path_class`,
403
- `Unknown path class "${field_definition.path_class}".`,
404
- ),
184
+ collectTypeReferenceDiagnostics(
185
+ diagnostics,
186
+ `fields.${field_name}.on`,
187
+ field_definition.on,
188
+ types,
189
+ );
190
+ collectTypeReferenceDiagnostics(
191
+ diagnostics,
192
+ `fields.${field_name}.required_on`,
193
+ field_definition.required_on,
194
+ types,
405
195
  );
406
- }
407
-
408
- /**
409
- * @param {PatramDiagnostic[]} diagnostics
410
- * @param {Record<string, { prefixes: string[] }>} path_classes
411
- * @param {Record<string, MetadataFieldConfig>} fields
412
- * @param {NonNullable<PatramRepoConfig['classes']>} classes
413
- */
414
- function collectClassSchemaConfigDiagnostics(
415
- diagnostics,
416
- path_classes,
417
- fields,
418
- classes,
419
- ) {
420
- for (const [class_name, class_definition] of Object.entries(classes)) {
421
- const schema_definition = class_definition.schema;
422
-
423
- if (!schema_definition) {
424
- continue;
425
- }
426
-
427
- collectMarkdownStylesDiagnostic(
428
- diagnostics,
429
- `classes.${class_name}.schema.markdown_styles`,
430
- schema_definition.markdown_styles,
431
- );
432
- collectMixedStylesDiagnostic(
433
- diagnostics,
434
- `classes.${class_name}.schema.mixed_styles`,
435
- schema_definition.mixed_styles,
436
- );
437
-
438
- for (const field_name of Object.keys(schema_definition.fields)) {
439
- if (fields[field_name]) {
440
- collectMarkdownStylesDiagnostic(
441
- diagnostics,
442
- `classes.${class_name}.schema.fields.${field_name}.markdown_styles`,
443
- schema_definition.fields[field_name].markdown_styles,
444
- );
445
- } else {
446
- diagnostics.push(
447
- createConfigDiagnostic(
448
- `classes.${class_name}.schema.fields.${field_name}`,
449
- `Unknown field "${field_name}".`,
450
- ),
451
- );
452
- }
453
- }
454
- }
455
-
456
- for (const [class_name, class_definition] of Object.entries(classes)) {
457
- const schema_definition = class_definition.schema;
458
-
459
- if (!schema_definition) {
460
- continue;
461
- }
462
-
463
- if (
464
- schema_definition.document_path_class === undefined ||
465
- path_classes[schema_definition.document_path_class]
466
- ) {
467
- continue;
468
- }
469
196
 
197
+ if (
198
+ field_definition.type === 'ref' &&
199
+ field_definition.to !== 'document' &&
200
+ types[field_definition.to] === undefined
201
+ ) {
470
202
  diagnostics.push(
471
203
  createConfigDiagnostic(
472
- `classes.${class_name}.schema.document_path_class`,
473
- `Unknown path class "${schema_definition.document_path_class}".`,
204
+ `fields.${field_name}.to`,
205
+ `Unknown target type "${field_definition.to}".`,
474
206
  ),
475
207
  );
476
208
  }
209
+
210
+ return diagnostics;
477
211
  }
478
212
 
479
213
  /**
480
- * @param {PatramDiagnostic[]} diagnostics
481
- * @param {string} diagnostic_path
482
- * @param {string[] | undefined} markdown_styles
214
+ * @param {string} type_name
215
+ * @param {NonNullable<PatramRepoConfig['types']>[string]} type_definition
216
+ * @param {NonNullable<PatramRepoConfig['fields']>} fields
217
+ * @returns {PatramDiagnostic[]}
483
218
  */
484
- function collectMarkdownStylesDiagnostic(
485
- diagnostics,
486
- diagnostic_path,
487
- markdown_styles,
488
- ) {
489
- if (markdown_styles === undefined) {
490
- return;
491
- }
492
-
493
- if (markdown_styles.length === 0) {
494
- diagnostics.push(
219
+ function validateTypeDefinition(type_name, type_definition, fields) {
220
+ if (
221
+ type_definition.defined_by !== undefined &&
222
+ fields[type_definition.defined_by] === undefined
223
+ ) {
224
+ return [
495
225
  createConfigDiagnostic(
496
- diagnostic_path,
497
- 'Markdown styles must contain at least one style.',
226
+ `types.${type_name}.defined_by`,
227
+ `Unknown field "${type_definition.defined_by}".`,
498
228
  ),
499
- );
500
-
501
- return;
229
+ ];
502
230
  }
503
231
 
504
- for (const markdown_style of markdown_styles) {
505
- if (isKnownMarkdownStyle(markdown_style)) {
506
- continue;
507
- }
508
-
509
- diagnostics.push(
510
- createConfigDiagnostic(
511
- diagnostic_path,
512
- `Unknown markdown style "${markdown_style}".`,
513
- ),
514
- );
515
- }
516
- }
232
+ const defined_by_field = type_definition.defined_by
233
+ ? fields[type_definition.defined_by]
234
+ : undefined;
517
235
 
518
- /**
519
- * @param {PatramDiagnostic[]} diagnostics
520
- * @param {string} diagnostic_path
521
- * @param {string | undefined} mixed_styles
522
- */
523
- function collectMixedStylesDiagnostic(
524
- diagnostics,
525
- diagnostic_path,
526
- mixed_styles,
527
- ) {
528
- if (mixed_styles === undefined || isMixedStyleValue(mixed_styles)) {
529
- return;
236
+ if (defined_by_field?.type !== 'ref') {
237
+ return [];
530
238
  }
531
239
 
532
- diagnostics.push(
240
+ return [
533
241
  createConfigDiagnostic(
534
- diagnostic_path,
535
- 'Mixed styles must be "ignore" or "error".',
242
+ `types.${type_name}.defined_by`,
243
+ 'Type identity fields must use a scalar field type.',
536
244
  ),
537
- );
538
- }
539
-
540
- /**
541
- * @param {PatramRepoConfig['classes']} classes
542
- * @returns {Record<string, ClassDefinition>}
543
- */
544
- function collectGraphClassDefinitions(classes) {
545
- /** @type {Record<string, ClassDefinition>} */
546
- const graph_class_definitions = {};
547
-
548
- for (const [class_name, class_definition] of Object.entries(classes ?? {})) {
549
- graph_class_definitions[class_name] = {
550
- builtin: class_definition.builtin,
551
- label: class_definition.label,
552
- };
553
- }
554
-
555
- return graph_class_definitions;
556
- }
557
-
558
- /**
559
- * @param {PatramDiagnostic[]} diagnostics
560
- * @param {PatramRepoConfig} repo_config
561
- * @param {string} where_clause
562
- * @param {string} diagnostic_path
563
- */
564
- function collectWhereClauseDiagnostics(
565
- diagnostics,
566
- repo_config,
567
- where_clause,
568
- diagnostic_path,
569
- ) {
570
- const parse_result = parseWhereClause(where_clause);
571
-
572
- if (!parse_result.success) {
573
- diagnostics.push(
574
- createConfigDiagnostic(diagnostic_path, parse_result.diagnostic.message),
575
- );
576
-
577
- return;
578
- }
579
-
580
- const semantic_diagnostics = getQuerySemanticDiagnostics(
581
- repo_config,
582
- { kind: 'ad_hoc' },
583
- parse_result.expression,
584
- );
585
-
586
- for (const semantic_diagnostic of semantic_diagnostics) {
587
- diagnostics.push(
588
- createConfigDiagnostic(diagnostic_path, semantic_diagnostic.message),
589
- );
590
- }
245
+ ];
591
246
  }
592
247
 
593
248
  /**
594
- * @param {string} issue_path
595
- * @param {string} message
249
+ * @param {PatramDiagnostic} diagnostic
596
250
  * @returns {PatramDiagnostic}
597
251
  */
598
- function createConfigDiagnostic(issue_path, message) {
252
+ function replaceDiagnosticPath(diagnostic) {
599
253
  return {
600
- code: 'config.invalid',
601
- column: 1,
602
- level: 'error',
603
- line: 1,
604
- message: `Invalid config at "${issue_path}": ${message}`,
254
+ ...diagnostic,
605
255
  path: CONFIG_FILE_NAME,
606
256
  };
607
257
  }
608
-
609
- /**
610
- * @param {(string | number | symbol | undefined)[]} issue_path
611
- * @returns {string}
612
- */
613
- function formatIssuePath(issue_path) {
614
- return issue_path.map(String).join('.');
615
- }