patram 0.4.0 → 0.6.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,12 +1,8 @@
1
+ /** @import * as $k$$l$patram$j$config$k$js from './patram-config.js'; */
1
2
  /* eslint-disable max-lines */
3
+
2
4
  /**
3
- * @import {
4
- * ClassSchemaConfig,
5
- * LoadPatramConfigResult,
6
- * MetadataFieldConfig,
7
- * PatramDiagnostic,
8
- * PatramRepoConfig,
9
- * } from './load-patram-config.types.ts';
5
+ * @import { ClassDefinition } from './patram-config.js';
10
6
  */
11
7
 
12
8
  import { readFile } from 'node:fs/promises';
@@ -15,7 +11,12 @@ import process from 'node:process';
15
11
 
16
12
  import { z } from 'zod';
17
13
 
18
- import { parsePatramConfig } from './patram-config.js';
14
+ import {
15
+ class_definition_schema,
16
+ mapping_definition_schema,
17
+ parsePatramConfig,
18
+ relation_definition_schema,
19
+ } from './patram-config.js';
19
20
  import { parseWhereClause } from './parse-where-clause.js';
20
21
  import { getQuerySemanticDiagnostics } from './query-inspection.js';
21
22
  import { resolvePatramGraphConfig } from './resolve-patram-graph-config.js';
@@ -39,13 +40,37 @@ import { DEFAULT_INCLUDE_PATTERNS } from './source-file-defaults.js';
39
40
 
40
41
  const CONFIG_FILE_NAME = '.patram.json';
41
42
  const RESERVED_STRUCTURAL_FIELD_NAMES = new Set(['$class', '$id', '$path']);
43
+ const MARKDOWN_STYLE_NAMES = [
44
+ 'front_matter',
45
+ 'visible_line',
46
+ 'list_item',
47
+ 'hidden_tag',
48
+ ];
49
+ const MARKDOWN_STYLE_NAME_SET = new Set(MARKDOWN_STYLE_NAMES);
50
+ const MIXED_STYLE_VALUES = new Set(['ignore', 'error']);
51
+
52
+ /**
53
+ * @typedef {object} PatramDiagnostic
54
+ * @property {string} code
55
+ * @property {number} column
56
+ * @property {'error'} level
57
+ * @property {number} line
58
+ * @property {string} message
59
+ * @property {string} path
60
+ */
42
61
 
62
+ /**
63
+ * @typedef {z.output<typeof stored_query_schema>} StoredQueryConfig
64
+ */
43
65
  const stored_query_schema = z
44
66
  .object({
45
67
  where: z.string().min(1, 'Stored query "where" must not be empty.'),
46
68
  })
47
69
  .strict();
48
70
 
71
+ /**
72
+ * @typedef {z.output<typeof derived_summary_scalar_schema>} DerivedSummaryScalar
73
+ */
49
74
  const derived_summary_scalar_schema = z.union([
50
75
  z.boolean(),
51
76
  z.number(),
@@ -53,6 +78,9 @@ const derived_summary_scalar_schema = z.union([
53
78
  z.null(),
54
79
  ]);
55
80
 
81
+ /**
82
+ * @typedef {z.output<typeof derived_summary_count_schema>} DerivedSummaryCountConfig
83
+ */
56
84
  const derived_summary_count_schema = z
57
85
  .object({
58
86
  traversal: z
@@ -64,6 +92,9 @@ const derived_summary_count_schema = z
64
92
  })
65
93
  .strict();
66
94
 
95
+ /**
96
+ * @typedef {z.output<typeof derived_summary_select_case_schema>} DerivedSummarySelectCaseConfig
97
+ */
67
98
  const derived_summary_select_case_schema = z
68
99
  .object({
69
100
  value: derived_summary_scalar_schema,
@@ -71,21 +102,41 @@ const derived_summary_select_case_schema = z
71
102
  })
72
103
  .strict();
73
104
 
74
- const derived_summary_field_schema = z
105
+ const derived_summary_field_name_schema = z
106
+ .string()
107
+ .regex(
108
+ /^[a-z][a-z0-9_]*$/du,
109
+ 'Derived summary field names must use lower_snake_case.',
110
+ );
111
+
112
+ /**
113
+ * @typedef {z.output<typeof derived_summary_field_schema>} DerivedSummaryFieldConfig
114
+ */
115
+ const derived_summary_count_field_schema = z
75
116
  .object({
76
- count: derived_summary_count_schema.optional(),
77
- default: derived_summary_scalar_schema.optional(),
78
- name: z
79
- .string()
80
- .regex(
81
- /^[a-z][a-z0-9_]*$/du,
82
- 'Derived summary field names must use lower_snake_case.',
83
- ),
84
- select: z.array(derived_summary_select_case_schema).optional(),
117
+ count: derived_summary_count_schema,
118
+ name: derived_summary_field_name_schema,
85
119
  })
86
- .strict()
87
- .superRefine(validateDerivedSummaryFieldDefinition);
120
+ .strict();
88
121
 
122
+ const derived_summary_select_field_schema = z
123
+ .object({
124
+ default: derived_summary_scalar_schema,
125
+ name: derived_summary_field_name_schema,
126
+ select: z
127
+ .array(derived_summary_select_case_schema)
128
+ .min(1, 'Derived summary "select" must contain at least one case.'),
129
+ })
130
+ .strict();
131
+
132
+ const derived_summary_field_schema = z.union([
133
+ derived_summary_count_field_schema,
134
+ derived_summary_select_field_schema,
135
+ ]);
136
+
137
+ /**
138
+ * @typedef {z.output<typeof derived_summary_schema>} DerivedSummaryConfig
139
+ */
89
140
  const derived_summary_schema = z
90
141
  .object({
91
142
  classes: z
@@ -98,6 +149,9 @@ const derived_summary_schema = z
98
149
  .strict()
99
150
  .superRefine(validateDerivedSummaryDefinition);
100
151
 
152
+ /**
153
+ * @typedef {z.output<typeof field_display_schema>} FieldDisplayConfig
154
+ */
101
155
  const field_display_schema = z
102
156
  .object({
103
157
  hidden: z.boolean().optional(),
@@ -105,6 +159,9 @@ const field_display_schema = z
105
159
  })
106
160
  .strict();
107
161
 
162
+ /**
163
+ * @typedef {z.output<typeof field_query_schema>} FieldQueryConfig
164
+ */
108
165
  const field_query_schema = z
109
166
  .object({
110
167
  contains: z.boolean().optional(),
@@ -118,6 +175,9 @@ const field_base_shape = {
118
175
  path_class: z.string().min(1).optional(),
119
176
  };
120
177
 
178
+ /**
179
+ * @typedef {z.output<typeof metadata_field_schema>} MetadataFieldConfig
180
+ */
121
181
  const metadata_field_schema = z.discriminatedUnion('type', [
122
182
  z
123
183
  .object({
@@ -167,20 +227,39 @@ const metadata_field_schema = z.discriminatedUnion('type', [
167
227
  .strict(),
168
228
  ]);
169
229
 
230
+ /**
231
+ * @typedef {z.output<typeof class_field_rule_schema>} ClassFieldRuleConfig
232
+ */
170
233
  const class_field_rule_schema = z
171
234
  .object({
235
+ markdown_styles: z.array(z.string().min(1)).optional(),
172
236
  presence: z.enum(['required', 'optional', 'forbidden']),
173
237
  })
174
238
  .strict();
175
239
 
240
+ /**
241
+ * @typedef {z.output<typeof class_schema_schema>} ClassSchemaConfig
242
+ */
176
243
  const class_schema_schema = z
177
244
  .object({
178
245
  document_path_class: z.string().min(1).optional(),
179
246
  fields: z.record(z.string().min(1), class_field_rule_schema).default({}),
247
+ markdown_styles: z.array(z.string().min(1)).optional(),
248
+ mixed_styles: z.string().min(1).optional(),
180
249
  unknown_fields: z.enum(['ignore', 'error']).optional(),
181
250
  })
182
251
  .strict();
183
252
 
253
+ /**
254
+ * @typedef {z.output<typeof repo_class_definition_schema>} RepoClassConfig
255
+ */
256
+ const repo_class_definition_schema = class_definition_schema.extend({
257
+ schema: class_schema_schema.optional(),
258
+ });
259
+
260
+ /**
261
+ * @typedef {z.output<typeof path_class_schema>} PathClassConfig
262
+ */
184
263
  const path_class_schema = z
185
264
  .object({
186
265
  prefixes: z
@@ -189,10 +268,14 @@ const path_class_schema = z
189
268
  })
190
269
  .strict();
191
270
 
271
+ /**
272
+ * @typedef {z.output<typeof patram_repo_config_schema>} PatramRepoConfig
273
+ */
192
274
  const patram_repo_config_schema = z
193
275
  .object({
194
- class_schemas: z.record(z.string().min(1), class_schema_schema).optional(),
195
- classes: z.unknown().optional(),
276
+ classes: z
277
+ .record(z.string().min(1), repo_class_definition_schema)
278
+ .optional(),
196
279
  derived_summaries: z
197
280
  .record(z.string().min(1), derived_summary_schema)
198
281
  .optional(),
@@ -201,14 +284,23 @@ const patram_repo_config_schema = z
201
284
  .array(z.string().min(1, 'Include globs must not be empty.'))
202
285
  .min(1, 'Include must contain at least one glob.')
203
286
  .default(DEFAULT_INCLUDE_PATTERNS),
204
- mappings: z.unknown().optional(),
287
+ mappings: z.record(z.string().min(1), mapping_definition_schema).optional(),
205
288
  path_classes: z.record(z.string().min(1), path_class_schema).optional(),
206
289
  queries: z.record(z.string().min(1), stored_query_schema).default({}),
207
- relations: z.unknown().optional(),
290
+ relations: z
291
+ .record(z.string().min(1), relation_definition_schema)
292
+ .optional(),
208
293
  })
209
294
  .strict()
210
295
  .superRefine(validateFieldDefinitionKeys);
211
296
 
297
+ /**
298
+ * @typedef {object} LoadPatramConfigResult
299
+ * @property {PatramRepoConfig | null} config
300
+ * @property {string} config_path
301
+ * @property {PatramDiagnostic[]} diagnostics
302
+ */
303
+
212
304
  /**
213
305
  * Load and validate the repo Patram config.
214
306
  *
@@ -229,6 +321,14 @@ export async function loadPatramConfig(project_directory = process.cwd()) {
229
321
  return createLoadResult(null, [parse_result.diagnostic]);
230
322
  }
231
323
 
324
+ const legacy_config_diagnostics = validateLegacyConfigShape(
325
+ parse_result.value,
326
+ );
327
+
328
+ if (legacy_config_diagnostics.length > 0) {
329
+ return createLoadResult(null, legacy_config_diagnostics);
330
+ }
331
+
232
332
  const config_result = patram_repo_config_schema.safeParse(parse_result.value);
233
333
 
234
334
  if (!config_result.success) {
@@ -382,61 +482,6 @@ function validateFieldDefinitionKeys(repo_config, refinement_context) {
382
482
  }
383
483
  }
384
484
 
385
- /**
386
- * @param {{ count?: unknown, default?: unknown, select?: unknown }} field_definition
387
- * @param {import('zod').RefinementCtx} refinement_context
388
- */
389
- function validateDerivedSummaryFieldDefinition(
390
- field_definition,
391
- refinement_context,
392
- ) {
393
- const evaluator_count =
394
- Number(field_definition.count !== undefined) +
395
- Number(field_definition.select !== undefined);
396
-
397
- if (evaluator_count !== 1) {
398
- refinement_context.addIssue({
399
- code: 'custom',
400
- message:
401
- 'Derived summary fields must define exactly one of "count" or "select".',
402
- });
403
- }
404
-
405
- if (
406
- field_definition.count !== undefined &&
407
- field_definition.default !== undefined
408
- ) {
409
- refinement_context.addIssue({
410
- code: 'custom',
411
- message: 'Derived summary count fields must not define "default".',
412
- path: ['default'],
413
- });
414
- }
415
-
416
- if (field_definition.select === undefined) {
417
- return;
418
- }
419
-
420
- if (
421
- Array.isArray(field_definition.select) &&
422
- field_definition.select.length === 0
423
- ) {
424
- refinement_context.addIssue({
425
- code: 'custom',
426
- message: 'Derived summary "select" must contain at least one case.',
427
- path: ['select'],
428
- });
429
- }
430
-
431
- if (field_definition.default === undefined) {
432
- refinement_context.addIssue({
433
- code: 'custom',
434
- message: 'Derived summary select fields must define "default".',
435
- path: ['default'],
436
- });
437
- }
438
- }
439
-
440
485
  /**
441
486
  * @param {{ fields: Array<{ name: string }> }} summary_definition
442
487
  * @param {import('zod').RefinementCtx} refinement_context
@@ -465,7 +510,7 @@ function validateDerivedSummaryDefinition(
465
510
  }
466
511
 
467
512
  /**
468
- * @param {{ include: string[], queries: Record<string, { where: string }>, classes?: unknown, mappings?: unknown, relations?: unknown }} repo_config
513
+ * @param {PatramRepoConfig} repo_config
469
514
  * @returns {PatramDiagnostic[]}
470
515
  */
471
516
  function validateGraphSchema(repo_config) {
@@ -479,7 +524,7 @@ function validateGraphSchema(repo_config) {
479
524
 
480
525
  try {
481
526
  parsePatramConfig({
482
- classes: repo_config.classes ?? {},
527
+ classes: collectGraphClassDefinitions(repo_config.classes),
483
528
  mappings: repo_config.mappings ?? {},
484
529
  relations: repo_config.relations ?? {},
485
530
  });
@@ -509,9 +554,8 @@ function validateFieldSchemaConfig(repo_config) {
509
554
  collectClassSchemaConfigDiagnostics(
510
555
  diagnostics,
511
556
  path_classes,
512
- classes,
513
557
  fields,
514
- repo_config.class_schemas,
558
+ classes,
515
559
  );
516
560
 
517
561
  return diagnostics;
@@ -586,7 +630,7 @@ function createDefaultRepoConfig() {
586
630
  }
587
631
 
588
632
  /**
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
633
+ * @param {PatramRepoConfig} repo_config
590
634
  * @returns {PatramRepoConfig}
591
635
  */
592
636
  function normalizeRepoConfig(repo_config) {
@@ -596,11 +640,6 @@ function normalizeRepoConfig(repo_config) {
596
640
  queries: { ...repo_config.queries },
597
641
  };
598
642
 
599
- assignOptionalRepoConfigField(
600
- normalized_config,
601
- 'class_schemas',
602
- repo_config.class_schemas,
603
- );
604
643
  assignOptionalRepoConfigField(
605
644
  normalized_config,
606
645
  'classes',
@@ -635,6 +674,27 @@ function normalizeRepoConfig(repo_config) {
635
674
  return normalized_config;
636
675
  }
637
676
 
677
+ /**
678
+ * @param {unknown} config_value
679
+ * @returns {PatramDiagnostic[]}
680
+ */
681
+ function validateLegacyConfigShape(config_value) {
682
+ if (
683
+ config_value === null ||
684
+ typeof config_value !== 'object' ||
685
+ !Object.hasOwn(config_value, 'class_schemas')
686
+ ) {
687
+ return [];
688
+ }
689
+
690
+ return [
691
+ createConfigDiagnostic(
692
+ 'class_schemas',
693
+ 'Top-level "class_schemas" is not supported. Move entries into classes.<name>.schema.',
694
+ ),
695
+ ];
696
+ }
697
+
638
698
  /**
639
699
  * @param {PatramDiagnostic[]} diagnostics
640
700
  * @param {Map<string, string>} class_coverage
@@ -669,7 +729,7 @@ function collectDuplicateClassDiagnostics(
669
729
  * @param {Set<string>} known_relation_names
670
730
  * @param {PatramRepoConfig} repo_config
671
731
  * @param {string} summary_name
672
- * @param {import('./load-patram-config.types.ts').DerivedSummaryFieldConfig[]} field_definitions
732
+ * @param {DerivedSummaryFieldConfig[]} field_definitions
673
733
  */
674
734
  function collectDerivedSummaryFieldDiagnostics(
675
735
  diagnostics,
@@ -861,50 +921,58 @@ function collectFieldPathClassDiagnostic(
861
921
  /**
862
922
  * @param {PatramDiagnostic[]} diagnostics
863
923
  * @param {Record<string, { prefixes: string[] }>} path_classes
864
- * @param {Record<string, unknown>} classes
865
924
  * @param {Record<string, MetadataFieldConfig>} fields
866
- * @param {PatramRepoConfig['class_schemas']} class_schemas
925
+ * @param {NonNullable<PatramRepoConfig['classes']>} classes
867
926
  */
868
927
  function collectClassSchemaConfigDiagnostics(
869
928
  diagnostics,
870
929
  path_classes,
871
- classes,
872
930
  fields,
873
- class_schemas,
931
+ classes,
874
932
  ) {
875
- if (!class_schemas) {
876
- return;
877
- }
933
+ for (const [class_name, class_definition] of Object.entries(classes)) {
934
+ const schema_definition = class_definition.schema;
878
935
 
879
- for (const class_name of Object.keys(class_schemas)) {
880
- if (classes[class_name]) {
936
+ if (!schema_definition) {
881
937
  continue;
882
938
  }
883
939
 
884
- diagnostics.push(
885
- createConfigDiagnostic(
886
- `class_schemas.${class_name}`,
887
- `Unknown class "${class_name}".`,
888
- ),
940
+ collectMarkdownStylesDiagnostic(
941
+ diagnostics,
942
+ `classes.${class_name}.schema.markdown_styles`,
943
+ schema_definition.markdown_styles,
944
+ );
945
+ collectMixedStylesDiagnostic(
946
+ diagnostics,
947
+ `classes.${class_name}.schema.mixed_styles`,
948
+ schema_definition.mixed_styles,
889
949
  );
890
- }
891
950
 
892
- for (const [class_name, schema_definition] of Object.entries(class_schemas)) {
893
951
  for (const field_name of Object.keys(schema_definition.fields)) {
894
952
  if (fields[field_name]) {
895
- continue;
953
+ collectMarkdownStylesDiagnostic(
954
+ diagnostics,
955
+ `classes.${class_name}.schema.fields.${field_name}.markdown_styles`,
956
+ schema_definition.fields[field_name].markdown_styles,
957
+ );
958
+ } else {
959
+ diagnostics.push(
960
+ createConfigDiagnostic(
961
+ `classes.${class_name}.schema.fields.${field_name}`,
962
+ `Unknown field "${field_name}".`,
963
+ ),
964
+ );
896
965
  }
897
-
898
- diagnostics.push(
899
- createConfigDiagnostic(
900
- `class_schemas.${class_name}.fields.${field_name}`,
901
- `Unknown field "${field_name}".`,
902
- ),
903
- );
904
966
  }
905
967
  }
906
968
 
907
- for (const [class_name, schema_definition] of Object.entries(class_schemas)) {
969
+ for (const [class_name, class_definition] of Object.entries(classes)) {
970
+ const schema_definition = class_definition.schema;
971
+
972
+ if (!schema_definition) {
973
+ continue;
974
+ }
975
+
908
976
  if (
909
977
  schema_definition.document_path_class === undefined ||
910
978
  path_classes[schema_definition.document_path_class]
@@ -914,13 +982,92 @@ function collectClassSchemaConfigDiagnostics(
914
982
 
915
983
  diagnostics.push(
916
984
  createConfigDiagnostic(
917
- `class_schemas.${class_name}.document_path_class`,
985
+ `classes.${class_name}.schema.document_path_class`,
918
986
  `Unknown path class "${schema_definition.document_path_class}".`,
919
987
  ),
920
988
  );
921
989
  }
922
990
  }
923
991
 
992
+ /**
993
+ * @param {PatramDiagnostic[]} diagnostics
994
+ * @param {string} diagnostic_path
995
+ * @param {string[] | undefined} markdown_styles
996
+ */
997
+ function collectMarkdownStylesDiagnostic(
998
+ diagnostics,
999
+ diagnostic_path,
1000
+ markdown_styles,
1001
+ ) {
1002
+ if (markdown_styles === undefined) {
1003
+ return;
1004
+ }
1005
+
1006
+ if (markdown_styles.length === 0) {
1007
+ diagnostics.push(
1008
+ createConfigDiagnostic(
1009
+ diagnostic_path,
1010
+ 'Markdown styles must contain at least one style.',
1011
+ ),
1012
+ );
1013
+
1014
+ return;
1015
+ }
1016
+
1017
+ for (const markdown_style of markdown_styles) {
1018
+ if (MARKDOWN_STYLE_NAME_SET.has(markdown_style)) {
1019
+ continue;
1020
+ }
1021
+
1022
+ diagnostics.push(
1023
+ createConfigDiagnostic(
1024
+ diagnostic_path,
1025
+ `Unknown markdown style "${markdown_style}".`,
1026
+ ),
1027
+ );
1028
+ }
1029
+ }
1030
+
1031
+ /**
1032
+ * @param {PatramDiagnostic[]} diagnostics
1033
+ * @param {string} diagnostic_path
1034
+ * @param {string | undefined} mixed_styles
1035
+ */
1036
+ function collectMixedStylesDiagnostic(
1037
+ diagnostics,
1038
+ diagnostic_path,
1039
+ mixed_styles,
1040
+ ) {
1041
+ if (mixed_styles === undefined || MIXED_STYLE_VALUES.has(mixed_styles)) {
1042
+ return;
1043
+ }
1044
+
1045
+ diagnostics.push(
1046
+ createConfigDiagnostic(
1047
+ diagnostic_path,
1048
+ 'Mixed styles must be "ignore" or "error".',
1049
+ ),
1050
+ );
1051
+ }
1052
+
1053
+ /**
1054
+ * @param {PatramRepoConfig['classes']} classes
1055
+ * @returns {Record<string, ClassDefinition>}
1056
+ */
1057
+ function collectGraphClassDefinitions(classes) {
1058
+ /** @type {Record<string, ClassDefinition>} */
1059
+ const graph_class_definitions = {};
1060
+
1061
+ for (const [class_name, class_definition] of Object.entries(classes ?? {})) {
1062
+ graph_class_definitions[class_name] = {
1063
+ builtin: class_definition.builtin,
1064
+ label: class_definition.label,
1065
+ };
1066
+ }
1067
+
1068
+ return graph_class_definitions;
1069
+ }
1070
+
924
1071
  /**
925
1072
  * @template {Exclude<keyof PatramRepoConfig, 'include' | 'queries'>} TKey
926
1073
  * @param {PatramRepoConfig} normalized_config