patram 0.2.0 → 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.
@@ -1,6 +1,12 @@
1
1
  /* eslint-disable max-lines */
2
2
  /**
3
- * @import { LoadPatramConfigResult, PatramDiagnostic, PatramRepoConfig } from './load-patram-config.types.ts';
3
+ * @import {
4
+ * ClassSchemaConfig,
5
+ * LoadPatramConfigResult,
6
+ * MetadataFieldConfig,
7
+ * PatramDiagnostic,
8
+ * PatramRepoConfig,
9
+ * } from './load-patram-config.types.ts';
4
10
  */
5
11
 
6
12
  import { readFile } from 'node:fs/promises';
@@ -11,6 +17,7 @@ import { z } from 'zod';
11
17
 
12
18
  import { parsePatramConfig } from './patram-config.js';
13
19
  import { parseWhereClause } from './parse-where-clause.js';
20
+ import { getQuerySemanticDiagnostics } from './query-inspection.js';
14
21
  import { resolvePatramGraphConfig } from './resolve-patram-graph-config.js';
15
22
  import { DEFAULT_INCLUDE_PATTERNS } from './source-file-defaults.js';
16
23
 
@@ -31,6 +38,7 @@ import { DEFAULT_INCLUDE_PATTERNS } from './source-file-defaults.js';
31
38
  */
32
39
 
33
40
  const CONFIG_FILE_NAME = '.patram.json';
41
+ const RESERVED_STRUCTURAL_FIELD_NAMES = new Set(['$class', '$id', '$path']);
34
42
 
35
43
  const stored_query_schema = z
36
44
  .object({
@@ -80,31 +88,126 @@ const derived_summary_field_schema = z
80
88
 
81
89
  const derived_summary_schema = z
82
90
  .object({
91
+ classes: z
92
+ .array(z.string().min(1))
93
+ .min(1, 'Derived summary "classes" must contain at least one class.'),
83
94
  fields: z
84
95
  .array(derived_summary_field_schema)
85
96
  .min(1, 'Derived summary "fields" must contain at least one field.'),
86
- kinds: z
87
- .array(z.string().min(1))
88
- .min(1, 'Derived summary "kinds" must contain at least one kind.'),
89
97
  })
90
98
  .strict()
91
99
  .superRefine(validateDerivedSummaryDefinition);
92
100
 
101
+ const field_display_schema = z
102
+ .object({
103
+ hidden: z.boolean().optional(),
104
+ order: z.number().optional(),
105
+ })
106
+ .strict();
107
+
108
+ const field_query_schema = z
109
+ .object({
110
+ contains: z.boolean().optional(),
111
+ prefix: z.boolean().optional(),
112
+ })
113
+ .strict();
114
+
115
+ const field_base_shape = {
116
+ display: field_display_schema.optional(),
117
+ multiple: z.boolean().optional(),
118
+ path_class: z.string().min(1).optional(),
119
+ };
120
+
121
+ const metadata_field_schema = z.discriminatedUnion('type', [
122
+ z
123
+ .object({
124
+ ...field_base_shape,
125
+ query: field_query_schema.optional(),
126
+ type: z.literal('string'),
127
+ })
128
+ .strict(),
129
+ z
130
+ .object({
131
+ ...field_base_shape,
132
+ type: z.literal('integer'),
133
+ })
134
+ .strict(),
135
+ z
136
+ .object({
137
+ ...field_base_shape,
138
+ type: z.literal('enum'),
139
+ values: z
140
+ .array(z.string().min(1, 'Field enum values must not be empty.'))
141
+ .min(1, 'Field enum values must contain at least one value.'),
142
+ })
143
+ .strict(),
144
+ z
145
+ .object({
146
+ ...field_base_shape,
147
+ type: z.literal('path'),
148
+ })
149
+ .strict(),
150
+ z
151
+ .object({
152
+ ...field_base_shape,
153
+ type: z.literal('glob'),
154
+ })
155
+ .strict(),
156
+ z
157
+ .object({
158
+ ...field_base_shape,
159
+ type: z.literal('date'),
160
+ })
161
+ .strict(),
162
+ z
163
+ .object({
164
+ ...field_base_shape,
165
+ type: z.literal('date_time'),
166
+ })
167
+ .strict(),
168
+ ]);
169
+
170
+ const class_field_rule_schema = z
171
+ .object({
172
+ presence: z.enum(['required', 'optional', 'forbidden']),
173
+ })
174
+ .strict();
175
+
176
+ const class_schema_schema = z
177
+ .object({
178
+ document_path_class: z.string().min(1).optional(),
179
+ fields: z.record(z.string().min(1), class_field_rule_schema).default({}),
180
+ unknown_fields: z.enum(['ignore', 'error']).optional(),
181
+ })
182
+ .strict();
183
+
184
+ const path_class_schema = z
185
+ .object({
186
+ prefixes: z
187
+ .array(z.string().min(1, 'Path class prefixes must not be empty.'))
188
+ .min(1, 'Path classes must contain at least one prefix.'),
189
+ })
190
+ .strict();
191
+
93
192
  const patram_repo_config_schema = z
94
193
  .object({
194
+ class_schemas: z.record(z.string().min(1), class_schema_schema).optional(),
195
+ classes: z.unknown().optional(),
95
196
  derived_summaries: z
96
197
  .record(z.string().min(1), derived_summary_schema)
97
198
  .optional(),
199
+ fields: z.record(z.string().min(1), metadata_field_schema).optional(),
98
200
  include: z
99
201
  .array(z.string().min(1, 'Include globs must not be empty.'))
100
202
  .min(1, 'Include must contain at least one glob.')
101
203
  .default(DEFAULT_INCLUDE_PATTERNS),
102
- kinds: z.unknown().optional(),
103
204
  mappings: z.unknown().optional(),
205
+ path_classes: z.record(z.string().min(1), path_class_schema).optional(),
104
206
  queries: z.record(z.string().min(1), stored_query_schema).default({}),
105
207
  relations: z.unknown().optional(),
106
208
  })
107
- .strict();
209
+ .strict()
210
+ .superRefine(validateFieldDefinitionKeys);
108
211
 
109
212
  /**
110
213
  * Load and validate the repo Patram config.
@@ -142,6 +245,18 @@ export async function loadPatramConfig(project_directory = process.cwd()) {
142
245
  }
143
246
 
144
247
  const normalized_config = normalizeRepoConfig(config_result.data);
248
+ const field_schema_diagnostics = validateFieldSchemaConfig(normalized_config);
249
+
250
+ if (field_schema_diagnostics.length > 0) {
251
+ return createLoadResult(null, field_schema_diagnostics);
252
+ }
253
+
254
+ const stored_query_diagnostics = validateStoredQueries(normalized_config);
255
+
256
+ if (stored_query_diagnostics.length > 0) {
257
+ return createLoadResult(null, stored_query_diagnostics);
258
+ }
259
+
145
260
  const derived_summary_diagnostics =
146
261
  validateDerivedSummaries(normalized_config);
147
262
 
@@ -249,6 +364,24 @@ function createValidationDiagnostic(issue) {
249
364
  };
250
365
  }
251
366
 
367
+ /**
368
+ * @param {{ fields?: Record<string, MetadataFieldConfig> }} repo_config
369
+ * @param {import('zod').RefinementCtx} refinement_context
370
+ */
371
+ function validateFieldDefinitionKeys(repo_config, refinement_context) {
372
+ for (const field_name of Object.keys(repo_config.fields ?? {})) {
373
+ if (!field_name.startsWith('$')) {
374
+ continue;
375
+ }
376
+
377
+ refinement_context.addIssue({
378
+ code: 'custom',
379
+ message: 'Metadata field names must not start with "$".',
380
+ path: ['fields', field_name],
381
+ });
382
+ }
383
+ }
384
+
252
385
  /**
253
386
  * @param {{ count?: unknown, default?: unknown, select?: unknown }} field_definition
254
387
  * @param {import('zod').RefinementCtx} refinement_context
@@ -332,12 +465,12 @@ function validateDerivedSummaryDefinition(
332
465
  }
333
466
 
334
467
  /**
335
- * @param {{ include: string[], queries: Record<string, { where: string }>, kinds?: unknown, mappings?: unknown, relations?: unknown }} repo_config
468
+ * @param {{ include: string[], queries: Record<string, { where: string }>, classes?: unknown, mappings?: unknown, relations?: unknown }} repo_config
336
469
  * @returns {PatramDiagnostic[]}
337
470
  */
338
471
  function validateGraphSchema(repo_config) {
339
472
  if (
340
- repo_config.kinds === undefined &&
473
+ repo_config.classes === undefined &&
341
474
  repo_config.mappings === undefined &&
342
475
  repo_config.relations === undefined
343
476
  ) {
@@ -346,7 +479,7 @@ function validateGraphSchema(repo_config) {
346
479
 
347
480
  try {
348
481
  parsePatramConfig({
349
- kinds: repo_config.kinds ?? {},
482
+ classes: repo_config.classes ?? {},
350
483
  mappings: repo_config.mappings ?? {},
351
484
  relations: repo_config.relations ?? {},
352
485
  });
@@ -361,6 +494,51 @@ function validateGraphSchema(repo_config) {
361
494
  return [];
362
495
  }
363
496
 
497
+ /**
498
+ * @param {PatramRepoConfig} repo_config
499
+ * @returns {PatramDiagnostic[]}
500
+ */
501
+ function validateFieldSchemaConfig(repo_config) {
502
+ const path_classes = repo_config.path_classes ?? {};
503
+ const classes = repo_config.classes ?? {};
504
+ const fields = repo_config.fields ?? {};
505
+ /** @type {PatramDiagnostic[]} */
506
+ const diagnostics = [];
507
+
508
+ collectFieldConfigDiagnostics(diagnostics, path_classes, fields);
509
+ collectClassSchemaConfigDiagnostics(
510
+ diagnostics,
511
+ path_classes,
512
+ classes,
513
+ fields,
514
+ repo_config.class_schemas,
515
+ );
516
+
517
+ return diagnostics;
518
+ }
519
+
520
+ /**
521
+ * @param {PatramRepoConfig} repo_config
522
+ * @returns {PatramDiagnostic[]}
523
+ */
524
+ function validateStoredQueries(repo_config) {
525
+ /** @type {PatramDiagnostic[]} */
526
+ const diagnostics = [];
527
+
528
+ for (const [query_name, stored_query] of Object.entries(
529
+ repo_config.queries,
530
+ )) {
531
+ collectWhereClauseDiagnostics(
532
+ diagnostics,
533
+ repo_config,
534
+ stored_query.where,
535
+ `queries.${query_name}.where`,
536
+ );
537
+ }
538
+
539
+ return diagnostics;
540
+ }
541
+
364
542
  /**
365
543
  * @param {PatramRepoConfig} repo_config
366
544
  * @returns {PatramDiagnostic[]}
@@ -374,20 +552,21 @@ function validateDerivedSummaries(repo_config) {
374
552
  const known_relation_names = new Set(Object.keys(graph_config.relations));
375
553
  /** @type {PatramDiagnostic[]} */
376
554
  const diagnostics = [];
377
- const kind_coverage = new Map();
555
+ const class_coverage = new Map();
378
556
 
379
557
  for (const [summary_name, summary_definition] of Object.entries(
380
558
  repo_config.derived_summaries,
381
559
  )) {
382
- collectDuplicateKindDiagnostics(
560
+ collectDuplicateClassDiagnostics(
383
561
  diagnostics,
384
- kind_coverage,
385
- summary_definition.kinds,
562
+ class_coverage,
563
+ summary_definition.classes,
386
564
  summary_name,
387
565
  );
388
566
  collectDerivedSummaryFieldDiagnostics(
389
567
  diagnostics,
390
568
  known_relation_names,
569
+ repo_config,
391
570
  summary_name,
392
571
  summary_definition.fields,
393
572
  );
@@ -407,7 +586,7 @@ function createDefaultRepoConfig() {
407
586
  }
408
587
 
409
588
  /**
410
- * @param {{ derived_summaries?: unknown, include: string[], queries: Record<string, { where: string }>, kinds?: unknown, mappings?: unknown, relations?: unknown }} repo_config
589
+ * @param {{ class_schemas?: unknown, classes?: unknown, derived_summaries?: unknown, fields?: unknown, include: string[], mappings?: unknown, path_classes?: unknown, queries: Record<string, { where: string }>, relations?: unknown }} repo_config
411
590
  * @returns {PatramRepoConfig}
412
591
  */
413
592
  function normalizeRepoConfig(repo_config) {
@@ -417,61 +596,69 @@ function normalizeRepoConfig(repo_config) {
417
596
  queries: { ...repo_config.queries },
418
597
  };
419
598
 
420
- if (
421
- repo_config.derived_summaries !== undefined &&
422
- repo_config.derived_summaries !== null
423
- ) {
424
- normalized_config.derived_summaries =
425
- /** @type {PatramRepoConfig['derived_summaries']} */ (
426
- repo_config.derived_summaries
427
- );
428
- }
429
-
430
- if (repo_config.kinds !== undefined && repo_config.kinds !== null) {
431
- normalized_config.kinds = /** @type {PatramRepoConfig['kinds']} */ (
432
- repo_config.kinds
433
- );
434
- }
435
-
436
- if (repo_config.mappings !== undefined && repo_config.mappings !== null) {
437
- normalized_config.mappings = /** @type {PatramRepoConfig['mappings']} */ (
438
- repo_config.mappings
439
- );
440
- }
441
-
442
- if (repo_config.relations !== undefined && repo_config.relations !== null) {
443
- normalized_config.relations = /** @type {PatramRepoConfig['relations']} */ (
444
- repo_config.relations
445
- );
446
- }
599
+ assignOptionalRepoConfigField(
600
+ normalized_config,
601
+ 'class_schemas',
602
+ repo_config.class_schemas,
603
+ );
604
+ assignOptionalRepoConfigField(
605
+ normalized_config,
606
+ 'classes',
607
+ repo_config.classes,
608
+ );
609
+ assignOptionalRepoConfigField(
610
+ normalized_config,
611
+ 'derived_summaries',
612
+ repo_config.derived_summaries,
613
+ );
614
+ assignOptionalRepoConfigField(
615
+ normalized_config,
616
+ 'fields',
617
+ repo_config.fields,
618
+ );
619
+ assignOptionalRepoConfigField(
620
+ normalized_config,
621
+ 'mappings',
622
+ repo_config.mappings,
623
+ );
624
+ assignOptionalRepoConfigField(
625
+ normalized_config,
626
+ 'path_classes',
627
+ repo_config.path_classes,
628
+ );
629
+ assignOptionalRepoConfigField(
630
+ normalized_config,
631
+ 'relations',
632
+ repo_config.relations,
633
+ );
447
634
 
448
635
  return normalized_config;
449
636
  }
450
637
 
451
638
  /**
452
639
  * @param {PatramDiagnostic[]} diagnostics
453
- * @param {Map<string, string>} kind_coverage
454
- * @param {string[]} kind_names
640
+ * @param {Map<string, string>} class_coverage
641
+ * @param {string[]} class_names
455
642
  * @param {string} summary_name
456
643
  */
457
- function collectDuplicateKindDiagnostics(
644
+ function collectDuplicateClassDiagnostics(
458
645
  diagnostics,
459
- kind_coverage,
460
- kind_names,
646
+ class_coverage,
647
+ class_names,
461
648
  summary_name,
462
649
  ) {
463
- for (const kind_name of kind_names) {
464
- const existing_summary_name = kind_coverage.get(kind_name);
650
+ for (const class_name of class_names) {
651
+ const existing_summary_name = class_coverage.get(class_name);
465
652
 
466
653
  if (!existing_summary_name) {
467
- kind_coverage.set(kind_name, summary_name);
654
+ class_coverage.set(class_name, summary_name);
468
655
  continue;
469
656
  }
470
657
 
471
658
  diagnostics.push(
472
659
  createConfigDiagnostic(
473
- `derived_summaries.${summary_name}.kinds`,
474
- `Kind "${kind_name}" is already covered by derived summary "${existing_summary_name}".`,
660
+ `derived_summaries.${summary_name}.classes`,
661
+ `Class "${class_name}" is already covered by derived summary "${existing_summary_name}".`,
475
662
  ),
476
663
  );
477
664
  }
@@ -480,12 +667,14 @@ function collectDuplicateKindDiagnostics(
480
667
  /**
481
668
  * @param {PatramDiagnostic[]} diagnostics
482
669
  * @param {Set<string>} known_relation_names
670
+ * @param {PatramRepoConfig} repo_config
483
671
  * @param {string} summary_name
484
672
  * @param {import('./load-patram-config.types.ts').DerivedSummaryFieldConfig[]} field_definitions
485
673
  */
486
674
  function collectDerivedSummaryFieldDiagnostics(
487
675
  diagnostics,
488
676
  known_relation_names,
677
+ repo_config,
489
678
  summary_name,
490
679
  field_definitions,
491
680
  ) {
@@ -499,8 +688,8 @@ function collectDerivedSummaryFieldDiagnostics(
499
688
  );
500
689
  collectWhereClauseDiagnostics(
501
690
  diagnostics,
691
+ repo_config,
502
692
  field_definition.count.where,
503
- known_relation_names,
504
693
  `derived_summaries.${summary_name}.fields.${field_index}.count.where`,
505
694
  );
506
695
  continue;
@@ -509,8 +698,8 @@ function collectDerivedSummaryFieldDiagnostics(
509
698
  for (const [case_index, select_case] of field_definition.select.entries()) {
510
699
  collectWhereClauseDiagnostics(
511
700
  diagnostics,
701
+ repo_config,
512
702
  select_case.when,
513
- known_relation_names,
514
703
  `derived_summaries.${summary_name}.fields.${field_index}.select.${case_index}.when`,
515
704
  );
516
705
  }
@@ -559,84 +748,232 @@ function collectTraversalDiagnostic(
559
748
 
560
749
  /**
561
750
  * @param {PatramDiagnostic[]} diagnostics
562
- * @param {string} where_clause
563
- * @param {Set<string>} known_relation_names
564
- * @param {string} diagnostic_path
751
+ * @param {Record<string, { prefixes: string[] }>} path_classes
752
+ * @param {Record<string, MetadataFieldConfig>} fields
565
753
  */
566
- function collectWhereClauseDiagnostics(
754
+ function collectFieldConfigDiagnostics(diagnostics, path_classes, fields) {
755
+ for (const [field_name, field_definition] of Object.entries(fields)) {
756
+ if (collectReservedFieldDiagnostic(diagnostics, field_name)) {
757
+ continue;
758
+ }
759
+
760
+ collectDisplayOrderDiagnostic(diagnostics, field_name, field_definition);
761
+ collectFieldPathClassDiagnostic(
762
+ diagnostics,
763
+ path_classes,
764
+ field_name,
765
+ field_definition,
766
+ );
767
+ }
768
+ }
769
+
770
+ /**
771
+ * @param {PatramDiagnostic[]} diagnostics
772
+ * @param {string} field_name
773
+ * @returns {boolean}
774
+ */
775
+ function collectReservedFieldDiagnostic(diagnostics, field_name) {
776
+ if (
777
+ !field_name.startsWith('$') ||
778
+ !RESERVED_STRUCTURAL_FIELD_NAMES.has(field_name)
779
+ ) {
780
+ return false;
781
+ }
782
+
783
+ diagnostics.push(
784
+ createConfigDiagnostic(
785
+ `fields.${field_name}`,
786
+ 'Metadata field names must not start with "$".',
787
+ ),
788
+ );
789
+
790
+ return true;
791
+ }
792
+
793
+ /**
794
+ * @param {PatramDiagnostic[]} diagnostics
795
+ * @param {string} field_name
796
+ * @param {MetadataFieldConfig} field_definition
797
+ */
798
+ function collectDisplayOrderDiagnostic(
567
799
  diagnostics,
568
- where_clause,
569
- known_relation_names,
570
- diagnostic_path,
800
+ field_name,
801
+ field_definition,
571
802
  ) {
572
- const parse_result = parseWhereClause(where_clause);
803
+ if (
804
+ field_definition.display?.order === undefined ||
805
+ (Number.isInteger(field_definition.display.order) &&
806
+ field_definition.display.order >= 0)
807
+ ) {
808
+ return;
809
+ }
573
810
 
574
- if (!parse_result.success) {
811
+ diagnostics.push(
812
+ createConfigDiagnostic(
813
+ `fields.${field_name}.display.order`,
814
+ 'Display order must be a non-negative integer.',
815
+ ),
816
+ );
817
+ }
818
+
819
+ /**
820
+ * @param {PatramDiagnostic[]} diagnostics
821
+ * @param {Record<string, { prefixes: string[] }>} path_classes
822
+ * @param {string} field_name
823
+ * @param {MetadataFieldConfig} field_definition
824
+ */
825
+ function collectFieldPathClassDiagnostic(
826
+ diagnostics,
827
+ path_classes,
828
+ field_name,
829
+ field_definition,
830
+ ) {
831
+ if (
832
+ !('path_class' in field_definition) ||
833
+ field_definition.path_class === undefined
834
+ ) {
835
+ return;
836
+ }
837
+
838
+ if (field_definition.type !== 'path') {
575
839
  diagnostics.push(
576
- createConfigDiagnostic(diagnostic_path, parse_result.diagnostic.message),
840
+ createConfigDiagnostic(
841
+ `fields.${field_name}.path_class`,
842
+ 'Path classes are only valid for path fields.',
843
+ ),
577
844
  );
578
845
 
579
846
  return;
580
847
  }
581
848
 
582
- for (const clause of parse_result.clauses) {
583
- collectClauseRelationDiagnostics(
584
- diagnostics,
585
- clause.term,
586
- known_relation_names,
587
- diagnostic_path,
588
- );
849
+ if (path_classes[field_definition.path_class]) {
850
+ return;
589
851
  }
852
+
853
+ diagnostics.push(
854
+ createConfigDiagnostic(
855
+ `fields.${field_name}.path_class`,
856
+ `Unknown path class "${field_definition.path_class}".`,
857
+ ),
858
+ );
590
859
  }
591
860
 
592
861
  /**
593
862
  * @param {PatramDiagnostic[]} diagnostics
594
- * @param {import('./parse-where-clause.types.ts').ParsedTerm} term
595
- * @param {Set<string>} known_relation_names
596
- * @param {string} diagnostic_path
863
+ * @param {Record<string, { prefixes: string[] }>} path_classes
864
+ * @param {Record<string, unknown>} classes
865
+ * @param {Record<string, MetadataFieldConfig>} fields
866
+ * @param {PatramRepoConfig['class_schemas']} class_schemas
597
867
  */
598
- function collectClauseRelationDiagnostics(
868
+ function collectClassSchemaConfigDiagnostics(
599
869
  diagnostics,
600
- term,
601
- known_relation_names,
602
- diagnostic_path,
870
+ path_classes,
871
+ classes,
872
+ fields,
873
+ class_schemas,
603
874
  ) {
604
- if (term.kind === 'aggregate') {
605
- if (!known_relation_names.has(term.traversal.relation_name)) {
875
+ if (!class_schemas) {
876
+ return;
877
+ }
878
+
879
+ for (const class_name of Object.keys(class_schemas)) {
880
+ if (classes[class_name]) {
881
+ continue;
882
+ }
883
+
884
+ diagnostics.push(
885
+ createConfigDiagnostic(
886
+ `class_schemas.${class_name}`,
887
+ `Unknown class "${class_name}".`,
888
+ ),
889
+ );
890
+ }
891
+
892
+ for (const [class_name, schema_definition] of Object.entries(class_schemas)) {
893
+ for (const field_name of Object.keys(schema_definition.fields)) {
894
+ if (fields[field_name]) {
895
+ continue;
896
+ }
897
+
606
898
  diagnostics.push(
607
899
  createConfigDiagnostic(
608
- diagnostic_path,
609
- `Unknown relation "${term.traversal.relation_name}" in traversal clause.`,
900
+ `class_schemas.${class_name}.fields.${field_name}`,
901
+ `Unknown field "${field_name}".`,
610
902
  ),
611
903
  );
612
904
  }
905
+ }
613
906
 
614
- for (const nested_clause of term.clauses) {
615
- collectClauseRelationDiagnostics(
616
- diagnostics,
617
- nested_clause.term,
618
- known_relation_names,
619
- diagnostic_path,
620
- );
907
+ for (const [class_name, schema_definition] of Object.entries(class_schemas)) {
908
+ if (
909
+ schema_definition.document_path_class === undefined ||
910
+ path_classes[schema_definition.document_path_class]
911
+ ) {
912
+ continue;
621
913
  }
622
914
 
623
- return;
915
+ diagnostics.push(
916
+ createConfigDiagnostic(
917
+ `class_schemas.${class_name}.document_path_class`,
918
+ `Unknown path class "${schema_definition.document_path_class}".`,
919
+ ),
920
+ );
624
921
  }
922
+ }
625
923
 
626
- if (term.kind !== 'relation' && term.kind !== 'relation_target') {
924
+ /**
925
+ * @template {Exclude<keyof PatramRepoConfig, 'include' | 'queries'>} TKey
926
+ * @param {PatramRepoConfig} normalized_config
927
+ * @param {TKey} field_name
928
+ * @param {unknown} field_value
929
+ */
930
+ function assignOptionalRepoConfigField(
931
+ normalized_config,
932
+ field_name,
933
+ field_value,
934
+ ) {
935
+ if (field_value === undefined || field_value === null) {
627
936
  return;
628
937
  }
629
938
 
630
- if (known_relation_names.has(term.relation_name)) {
939
+ normalized_config[field_name] = /** @type {PatramRepoConfig[TKey]} */ (
940
+ field_value
941
+ );
942
+ }
943
+
944
+ /**
945
+ * @param {PatramDiagnostic[]} diagnostics
946
+ * @param {PatramRepoConfig} repo_config
947
+ * @param {string} where_clause
948
+ * @param {string} diagnostic_path
949
+ */
950
+ function collectWhereClauseDiagnostics(
951
+ diagnostics,
952
+ repo_config,
953
+ where_clause,
954
+ diagnostic_path,
955
+ ) {
956
+ const parse_result = parseWhereClause(where_clause);
957
+
958
+ if (!parse_result.success) {
959
+ diagnostics.push(
960
+ createConfigDiagnostic(diagnostic_path, parse_result.diagnostic.message),
961
+ );
962
+
631
963
  return;
632
964
  }
633
965
 
634
- diagnostics.push(
635
- createConfigDiagnostic(
636
- diagnostic_path,
637
- `Unknown relation "${term.relation_name}" in relation clause.`,
638
- ),
966
+ const semantic_diagnostics = getQuerySemanticDiagnostics(
967
+ repo_config,
968
+ { kind: 'ad_hoc' },
969
+ parse_result.clauses,
639
970
  );
971
+
972
+ for (const semantic_diagnostic of semantic_diagnostics) {
973
+ diagnostics.push(
974
+ createConfigDiagnostic(diagnostic_path, semantic_diagnostic.message),
975
+ );
976
+ }
640
977
  }
641
978
 
642
979
  /**