patram 0.7.0 → 0.9.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 (140) hide show
  1. package/bin/patram.js +2 -2
  2. package/lib/{parse-cli-color-options.js → cli/color-options.js} +2 -2
  3. package/lib/cli/command-helpers.js +35 -0
  4. package/lib/cli/commands/check.js +73 -0
  5. package/lib/cli/commands/fields.js +57 -0
  6. package/lib/cli/commands/queries.js +41 -0
  7. package/lib/cli/commands/query.js +239 -0
  8. package/lib/cli/commands/refs.js +72 -0
  9. package/lib/cli/commands/show.js +58 -0
  10. package/lib/{cli-help-metadata.js → cli/help-metadata.js} +54 -15
  11. package/lib/cli/main.js +76 -0
  12. package/lib/{parse-cli-arguments-helpers.js → cli/parse-arguments-helpers.js} +18 -11
  13. package/lib/{parse-cli-arguments.js → cli/parse-arguments.js} +12 -12
  14. package/lib/{parse-cli-query-pagination.js → cli/query-pagination.js} +2 -2
  15. package/lib/{render-cli-help.js → cli/render-help.js} +4 -4
  16. package/lib/{resolve-output-mode.js → cli/resolve-output-mode.js} +2 -2
  17. package/lib/cli/test-helpers.js +30 -0
  18. package/lib/config/defaults.d.ts +10 -0
  19. package/lib/config/defaults.js +80 -0
  20. package/lib/config/load-patram-config.d.ts +76 -0
  21. package/lib/config/load-patram-config.js +315 -0
  22. package/lib/config/load-patram-config.types.d.ts +45 -0
  23. package/lib/{patram-config.d.ts → config/patram-config.d.ts} +31 -31
  24. package/lib/{patram-config.js → config/patram-config.js} +3 -3
  25. package/lib/{patram-config.types.d.ts → config/patram-config.types.d.ts} +1 -1
  26. package/lib/{resolve-patram-graph-config.d.ts → config/resolve-patram-graph-config.d.ts} +2 -2
  27. package/lib/{resolve-patram-graph-config.js → config/resolve-patram-graph-config.js} +3 -3
  28. package/lib/{load-patram-config.d.ts → config/schema.d.ts} +147 -191
  29. package/lib/config/schema.js +324 -0
  30. package/lib/{source-file-defaults.d.ts → config/source-file-defaults.d.ts} +0 -1
  31. package/lib/{source-file-defaults.js → config/source-file-defaults.js} +1 -1
  32. package/lib/config/validation.d.ts +27 -0
  33. package/lib/config/validation.js +615 -0
  34. package/lib/directive-validation-test-helpers.js +1 -1
  35. package/lib/{build-graph-identity.d.ts → graph/build-graph-identity.d.ts} +2 -2
  36. package/lib/{build-graph-identity.js → graph/build-graph-identity.js} +1 -1
  37. package/lib/{build-graph.d.ts → graph/build-graph.d.ts} +3 -3
  38. package/lib/{build-graph.js → graph/build-graph.js} +17 -13
  39. package/lib/{build-graph.types.d.ts → graph/build-graph.types.d.ts} +1 -1
  40. package/lib/graph/check-directive-metadata.d.ts +23 -0
  41. package/lib/{check-directive-metadata.js → graph/check-directive-metadata.js} +7 -7
  42. package/lib/graph/check-directive-path-target.d.ts +32 -0
  43. package/lib/{check-directive-path-target.js → graph/check-directive-path-target.js} +4 -4
  44. package/lib/graph/check-directive-value.d.ts +19 -0
  45. package/lib/{check-directive-value.js → graph/check-directive-value.js} +3 -3
  46. package/lib/graph/check-graph.d.ts +29 -0
  47. package/lib/{check-graph.js → graph/check-graph.js} +6 -6
  48. package/lib/graph/directive-diagnostics.d.ts +20 -0
  49. package/lib/{directive-diagnostics.js → graph/directive-diagnostics.js} +2 -2
  50. package/lib/graph/directive-type-rules.d.ts +18 -0
  51. package/lib/{directive-type-rules.js → graph/directive-type-rules.js} +3 -3
  52. package/lib/{document-node-identity.d.ts → graph/document-node-identity.d.ts} +2 -2
  53. package/lib/{document-node-identity.js → graph/document-node-identity.js} +2 -2
  54. package/lib/graph/inspect-reverse-references.d.ts +22 -0
  55. package/lib/graph/inspect-reverse-references.js +184 -0
  56. package/lib/{load-project-graph.d.ts → graph/load-project-graph.d.ts} +10 -10
  57. package/lib/{load-project-graph.js → graph/load-project-graph.js} +12 -12
  58. package/lib/{parse-where-clause.types.d.ts → graph/parse-where-clause.types.d.ts} +1 -1
  59. package/lib/{query-graph.d.ts → graph/query/execute.d.ts} +11 -11
  60. package/lib/{query-graph.js → graph/query/execute.js} +12 -12
  61. package/lib/{query-inspection.d.ts → graph/query/inspect.d.ts} +10 -8
  62. package/lib/{query-inspection.js → graph/query/inspect.js} +16 -17
  63. package/lib/{parse-where-clause.d.ts → graph/query/parse.d.ts} +6 -6
  64. package/lib/{parse-where-clause.js → graph/query/parse.js} +2 -2
  65. package/lib/graph/query/resolve.d.ts +30 -0
  66. package/lib/{resolve-where-clause.js → graph/query/resolve.js} +1 -1
  67. package/lib/graph/reverse-reference-test-helpers.d.ts +55 -0
  68. package/lib/graph/reverse-reference-test-helpers.js +76 -0
  69. package/lib/{command-output.js → output/command-output.js} +6 -5
  70. package/lib/{derived-summary.js → output/derived-summary.js} +7 -7
  71. package/lib/output/layout-incoming-references.js +105 -0
  72. package/lib/output/layout-incoming-summary-lines.js +16 -0
  73. package/lib/{layout-stored-queries.js → output/layout-stored-queries.js} +9 -9
  74. package/lib/{list-queries.js → output/list-queries.js} +1 -1
  75. package/lib/{render-check-output.js → output/render-check-output.js} +1 -1
  76. package/lib/{render-field-discovery.js → output/render-field-discovery.js} +3 -3
  77. package/lib/output/render-output-view.js +56 -0
  78. package/lib/{render-json-output.js → output/renderers/json.js} +92 -63
  79. package/lib/{render-plain-output.js → output/renderers/plain.js} +62 -7
  80. package/lib/{render-rich-output.js → output/renderers/rich.js} +69 -8
  81. package/lib/{resolve-check-target.js → output/resolve-check-target.js} +1 -1
  82. package/lib/{render-rich-source.js → output/rich-source/render.js} +6 -6
  83. package/lib/{show-document.js → output/show-document.js} +54 -16
  84. package/lib/{render-output-view.js → output/view-model/index.js} +56 -47
  85. package/lib/{write-paged-output.js → output/write-paged-output.js} +9 -5
  86. package/lib/{claim-helpers.d.ts → parse/claim-helpers.d.ts} +2 -2
  87. package/lib/{parse-jsdoc-claims.d.ts → parse/jsdoc/parse-jsdoc-claims.d.ts} +2 -2
  88. package/lib/{parse-jsdoc-claims.js → parse/jsdoc/parse-jsdoc-claims.js} +9 -9
  89. package/lib/{parse-jsdoc-prose.d.ts → parse/jsdoc/parse-jsdoc-prose.d.ts} +1 -1
  90. package/lib/{parse-jsdoc-prose.js → parse/jsdoc/parse-jsdoc-prose.js} +1 -1
  91. package/lib/{parse-markdown-claims.d.ts → parse/markdown/parse-markdown-claims.d.ts} +3 -3
  92. package/lib/{parse-markdown-claims.js → parse/markdown/parse-markdown-claims.js} +8 -8
  93. package/lib/{parse-markdown-directives.d.ts → parse/markdown/parse-markdown-directives.d.ts} +2 -2
  94. package/lib/{parse-markdown-directives.js → parse/markdown/parse-markdown-directives.js} +3 -3
  95. package/lib/{parse-claims.d.ts → parse/parse-claims.d.ts} +4 -13
  96. package/lib/{parse-claims.js → parse/parse-claims.js} +18 -26
  97. package/lib/{parse-claims.types.d.ts → parse/parse-claims.types.d.ts} +1 -1
  98. package/lib/{tagged-fenced-block-error.d.ts → parse/tagged-fenced/tagged-fenced-block-error.d.ts} +2 -2
  99. package/lib/{tagged-fenced-block-parser.d.ts → parse/tagged-fenced/tagged-fenced-block-parser.d.ts} +3 -3
  100. package/lib/{tagged-fenced-blocks.d.ts → parse/tagged-fenced/tagged-fenced-blocks.d.ts} +7 -7
  101. package/lib/{tagged-fenced-blocks.js → parse/tagged-fenced/tagged-fenced-blocks.js} +3 -3
  102. package/lib/{parse-yaml-claims.d.ts → parse/yaml/parse-yaml-claims.d.ts} +4 -4
  103. package/lib/{parse-yaml-claims.js → parse/yaml/parse-yaml-claims.js} +22 -13
  104. package/lib/patram.d.ts +29 -28
  105. package/lib/patram.js +5 -6
  106. package/lib/{discover-fields.js → scan/discover-fields.js} +9 -8
  107. package/lib/scan/list-repo-files.d.ts +16 -0
  108. package/lib/{list-source-files.js → scan/list-repo-files.js} +2 -35
  109. package/lib/{list-source-files.d.ts → scan/list-source-files.d.ts} +4 -11
  110. package/lib/scan/list-source-files.js +45 -0
  111. package/package.json +8 -7
  112. package/lib/build-graph.types.ts +0 -27
  113. package/lib/discover-fields.types.ts +0 -52
  114. package/lib/load-patram-config.js +0 -1215
  115. package/lib/load-patram-config.types.d.ts +0 -45
  116. package/lib/load-patram-config.types.ts +0 -56
  117. package/lib/output-view.types.d.ts +0 -80
  118. package/lib/output-view.types.ts +0 -96
  119. package/lib/overlay-graph.d.ts +0 -43
  120. package/lib/overlay-graph.js +0 -191
  121. package/lib/parse-claims.types.ts +0 -41
  122. package/lib/parse-cli-arguments.types.ts +0 -69
  123. package/lib/parse-where-clause.types.ts +0 -87
  124. package/lib/patram-cli.js +0 -528
  125. package/lib/patram-config.types.ts +0 -22
  126. package/lib/tagged-fenced-blocks.types.ts +0 -38
  127. /package/lib/{format-derived-summary-row.js → output/format-derived-summary-row.js} +0 -0
  128. /package/lib/{format-node-header.js → output/format-node-header.js} +0 -0
  129. /package/lib/{format-output-item-block.js → output/format-output-item-block.js} +0 -0
  130. /package/lib/{format-output-metadata.js → output/format-output-metadata.js} +0 -0
  131. /package/lib/{claim-helpers.js → parse/claim-helpers.js} +0 -0
  132. /package/lib/{parse-jsdoc-blocks.d.ts → parse/jsdoc/parse-jsdoc-blocks.d.ts} +0 -0
  133. /package/lib/{parse-jsdoc-blocks.js → parse/jsdoc/parse-jsdoc-blocks.js} +0 -0
  134. /package/lib/{tagged-fenced-block-error.js → parse/tagged-fenced/tagged-fenced-block-error.js} +0 -0
  135. /package/lib/{tagged-fenced-block-markdown.d.ts → parse/tagged-fenced/tagged-fenced-block-markdown.d.ts} +0 -0
  136. /package/lib/{tagged-fenced-block-markdown.js → parse/tagged-fenced/tagged-fenced-block-markdown.js} +0 -0
  137. /package/lib/{tagged-fenced-block-metadata.d.ts → parse/tagged-fenced/tagged-fenced-block-metadata.d.ts} +0 -0
  138. /package/lib/{tagged-fenced-block-metadata.js → parse/tagged-fenced/tagged-fenced-block-metadata.js} +0 -0
  139. /package/lib/{tagged-fenced-block-parser.js → parse/tagged-fenced/tagged-fenced-block-parser.js} +0 -0
  140. /package/lib/{tagged-fenced-blocks.types.d.ts → parse/tagged-fenced/tagged-fenced-blocks.types.d.ts} +0 -0
@@ -1,1215 +0,0 @@
1
- /** @import * as $k$$l$patram$j$config$k$js from './patram-config.js'; */
2
- /* eslint-disable max-lines */
3
-
4
- /**
5
- * @import { ClassDefinition } from './patram-config.js';
6
- */
7
-
8
- import { readFile } from 'node:fs/promises';
9
- import { resolve } from 'node:path';
10
- import process from 'node:process';
11
-
12
- import { z } from 'zod';
13
-
14
- import {
15
- class_definition_schema,
16
- mapping_definition_schema,
17
- parsePatramConfig,
18
- relation_definition_schema,
19
- } from './patram-config.js';
20
- import { parseWhereClause } from './parse-where-clause.js';
21
- import { getQuerySemanticDiagnostics } from './query-inspection.js';
22
- import { resolvePatramGraphConfig } from './resolve-patram-graph-config.js';
23
- import { DEFAULT_INCLUDE_PATTERNS } from './source-file-defaults.js';
24
-
25
- /**
26
- * Repo config loading.
27
- *
28
- * Reads `.patram.json`, applies defaults, and validates repo config and graph
29
- * schema before command execution.
30
- *
31
- * Kind: config
32
- * Status: active
33
- * Tracked in: ../docs/plans/v0/source-anchor-dogfooding.md
34
- * Decided by: ../docs/decisions/single-config-file.md
35
- * Decided by: ../docs/decisions/optional-config-default-scan.md
36
- * @patram
37
- * @see {@link ./resolve-patram-graph-config.js}
38
- * @see {@link ../docs/decisions/single-config-file.md}
39
- */
40
-
41
- const CONFIG_FILE_NAME = '.patram.json';
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
- */
61
-
62
- /**
63
- * @typedef {z.output<typeof stored_query_schema>} StoredQueryConfig
64
- */
65
- const stored_query_schema = z
66
- .object({
67
- where: z.string().min(1, 'Stored query "where" must not be empty.'),
68
- })
69
- .strict();
70
-
71
- /**
72
- * @typedef {z.output<typeof derived_summary_scalar_schema>} DerivedSummaryScalar
73
- */
74
- const derived_summary_scalar_schema = z.union([
75
- z.boolean(),
76
- z.number(),
77
- z.string(),
78
- z.null(),
79
- ]);
80
-
81
- /**
82
- * @typedef {z.output<typeof derived_summary_count_schema>} DerivedSummaryCountConfig
83
- */
84
- const derived_summary_count_schema = z
85
- .object({
86
- traversal: z
87
- .string()
88
- .min(1, 'Derived summary count "traversal" must not be empty.'),
89
- where: z
90
- .string()
91
- .min(1, 'Derived summary count "where" must not be empty.'),
92
- })
93
- .strict();
94
-
95
- /**
96
- * @typedef {z.output<typeof derived_summary_select_case_schema>} DerivedSummarySelectCaseConfig
97
- */
98
- const derived_summary_select_case_schema = z
99
- .object({
100
- value: derived_summary_scalar_schema,
101
- when: z.string().min(1, 'Derived summary select "when" must not be empty.'),
102
- })
103
- .strict();
104
-
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
116
- .object({
117
- count: derived_summary_count_schema,
118
- name: derived_summary_field_name_schema,
119
- })
120
- .strict();
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
- */
140
- const derived_summary_schema = z
141
- .object({
142
- classes: z
143
- .array(z.string().min(1))
144
- .min(1, 'Derived summary "classes" must contain at least one class.'),
145
- fields: z
146
- .array(derived_summary_field_schema)
147
- .min(1, 'Derived summary "fields" must contain at least one field.'),
148
- })
149
- .strict()
150
- .superRefine(validateDerivedSummaryDefinition);
151
-
152
- /**
153
- * @typedef {z.output<typeof field_display_schema>} FieldDisplayConfig
154
- */
155
- const field_display_schema = z
156
- .object({
157
- hidden: z.boolean().optional(),
158
- order: z.number().optional(),
159
- })
160
- .strict();
161
-
162
- /**
163
- * @typedef {z.output<typeof field_query_schema>} FieldQueryConfig
164
- */
165
- const field_query_schema = z
166
- .object({
167
- contains: z.boolean().optional(),
168
- prefix: z.boolean().optional(),
169
- })
170
- .strict();
171
-
172
- const field_base_shape = {
173
- display: field_display_schema.optional(),
174
- multiple: z.boolean().optional(),
175
- path_class: z.string().min(1).optional(),
176
- };
177
-
178
- /**
179
- * @typedef {z.output<typeof metadata_field_schema>} MetadataFieldConfig
180
- */
181
- const metadata_field_schema = z.discriminatedUnion('type', [
182
- z
183
- .object({
184
- ...field_base_shape,
185
- query: field_query_schema.optional(),
186
- type: z.literal('string'),
187
- })
188
- .strict(),
189
- z
190
- .object({
191
- ...field_base_shape,
192
- type: z.literal('integer'),
193
- })
194
- .strict(),
195
- z
196
- .object({
197
- ...field_base_shape,
198
- type: z.literal('enum'),
199
- values: z
200
- .array(z.string().min(1, 'Field enum values must not be empty.'))
201
- .min(1, 'Field enum values must contain at least one value.'),
202
- })
203
- .strict(),
204
- z
205
- .object({
206
- ...field_base_shape,
207
- type: z.literal('path'),
208
- })
209
- .strict(),
210
- z
211
- .object({
212
- ...field_base_shape,
213
- type: z.literal('glob'),
214
- })
215
- .strict(),
216
- z
217
- .object({
218
- ...field_base_shape,
219
- type: z.literal('date'),
220
- })
221
- .strict(),
222
- z
223
- .object({
224
- ...field_base_shape,
225
- type: z.literal('date_time'),
226
- })
227
- .strict(),
228
- ]);
229
-
230
- /**
231
- * @typedef {z.output<typeof class_field_rule_schema>} ClassFieldRuleConfig
232
- */
233
- const class_field_rule_schema = z
234
- .object({
235
- markdown_styles: z.array(z.string().min(1)).optional(),
236
- presence: z.enum(['required', 'optional', 'forbidden']),
237
- })
238
- .strict();
239
-
240
- /**
241
- * @typedef {z.output<typeof class_schema_schema>} ClassSchemaConfig
242
- */
243
- const class_schema_schema = z
244
- .object({
245
- document_path_class: z.string().min(1).optional(),
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(),
249
- unknown_fields: z.enum(['ignore', 'error']).optional(),
250
- })
251
- .strict();
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
- */
263
- const path_class_schema = z
264
- .object({
265
- prefixes: z
266
- .array(z.string().min(1, 'Path class prefixes must not be empty.'))
267
- .min(1, 'Path classes must contain at least one prefix.'),
268
- })
269
- .strict();
270
-
271
- /**
272
- * @typedef {z.output<typeof patram_repo_config_schema>} PatramRepoConfig
273
- */
274
- const patram_repo_config_schema = z
275
- .object({
276
- classes: z
277
- .record(z.string().min(1), repo_class_definition_schema)
278
- .optional(),
279
- derived_summaries: z
280
- .record(z.string().min(1), derived_summary_schema)
281
- .optional(),
282
- fields: z.record(z.string().min(1), metadata_field_schema).optional(),
283
- include: z
284
- .array(z.string().min(1, 'Include globs must not be empty.'))
285
- .min(1, 'Include must contain at least one glob.')
286
- .default(DEFAULT_INCLUDE_PATTERNS),
287
- mappings: z.record(z.string().min(1), mapping_definition_schema).optional(),
288
- path_classes: z.record(z.string().min(1), path_class_schema).optional(),
289
- queries: z.record(z.string().min(1), stored_query_schema).default({}),
290
- relations: z
291
- .record(z.string().min(1), relation_definition_schema)
292
- .optional(),
293
- })
294
- .strict()
295
- .superRefine(validateFieldDefinitionKeys);
296
-
297
- /**
298
- * @typedef {object} LoadPatramConfigResult
299
- * @property {PatramRepoConfig | null} config
300
- * @property {string} config_path
301
- * @property {PatramDiagnostic[]} diagnostics
302
- */
303
-
304
- /**
305
- * Load and validate the repo Patram config.
306
- *
307
- * @param {string} [project_directory]
308
- * @returns {Promise<LoadPatramConfigResult>}
309
- */
310
- export async function loadPatramConfig(project_directory = process.cwd()) {
311
- const config_file_path = resolve(project_directory, CONFIG_FILE_NAME);
312
- const config_source = await readConfigSource(config_file_path);
313
-
314
- if (config_source === null) {
315
- return createLoadResult(createDefaultRepoConfig(), []);
316
- }
317
-
318
- const parse_result = parseConfigJson(config_source);
319
-
320
- if (!parse_result.success) {
321
- return createLoadResult(null, [parse_result.diagnostic]);
322
- }
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
-
332
- const config_result = patram_repo_config_schema.safeParse(parse_result.value);
333
-
334
- if (!config_result.success) {
335
- return createLoadResult(
336
- null,
337
- config_result.error.issues.map(createValidationDiagnostic),
338
- );
339
- }
340
-
341
- const graph_schema_diagnostics = validateGraphSchema(config_result.data);
342
-
343
- if (graph_schema_diagnostics.length > 0) {
344
- return createLoadResult(null, graph_schema_diagnostics);
345
- }
346
-
347
- const normalized_config = normalizeRepoConfig(config_result.data);
348
- const field_schema_diagnostics = validateFieldSchemaConfig(normalized_config);
349
-
350
- if (field_schema_diagnostics.length > 0) {
351
- return createLoadResult(null, field_schema_diagnostics);
352
- }
353
-
354
- const stored_query_diagnostics = validateStoredQueries(normalized_config);
355
-
356
- if (stored_query_diagnostics.length > 0) {
357
- return createLoadResult(null, stored_query_diagnostics);
358
- }
359
-
360
- const derived_summary_diagnostics =
361
- validateDerivedSummaries(normalized_config);
362
-
363
- if (derived_summary_diagnostics.length > 0) {
364
- return createLoadResult(null, derived_summary_diagnostics);
365
- }
366
-
367
- return createLoadResult(normalized_config, []);
368
- }
369
-
370
- /**
371
- * @param {string} config_file_path
372
- * @returns {Promise<string | null>}
373
- */
374
- async function readConfigSource(config_file_path) {
375
- try {
376
- return await readFile(config_file_path, 'utf8');
377
- } catch (error) {
378
- if (isMissingFileError(error)) {
379
- return null;
380
- }
381
-
382
- throw error;
383
- }
384
- }
385
-
386
- /**
387
- * @param {string} config_source
388
- * @returns {{ success: true, value: unknown } | { success: false, diagnostic: PatramDiagnostic }}
389
- */
390
- function parseConfigJson(config_source) {
391
- try {
392
- return {
393
- success: true,
394
- value: JSON.parse(config_source),
395
- };
396
- } catch (error) {
397
- if (error instanceof SyntaxError) {
398
- return {
399
- diagnostic: createInvalidJsonDiagnostic(config_source, error),
400
- success: false,
401
- };
402
- }
403
-
404
- throw error;
405
- }
406
- }
407
-
408
- /**
409
- * @param {PatramRepoConfig | null} config
410
- * @param {PatramDiagnostic[]} diagnostics
411
- * @returns {LoadPatramConfigResult}
412
- */
413
- function createLoadResult(config, diagnostics) {
414
- return {
415
- config,
416
- config_path: CONFIG_FILE_NAME,
417
- diagnostics,
418
- };
419
- }
420
-
421
- /**
422
- * @param {string} config_source
423
- * @param {SyntaxError} error
424
- * @returns {PatramDiagnostic}
425
- */
426
- function createInvalidJsonDiagnostic(config_source, error) {
427
- const origin = getJsonSyntaxOrigin(config_source, error.message);
428
-
429
- return {
430
- code: 'config.invalid_json',
431
- column: origin.column,
432
- level: 'error',
433
- line: origin.line,
434
- message: 'Invalid JSON syntax.',
435
- path: CONFIG_FILE_NAME,
436
- };
437
- }
438
-
439
- /**
440
- * @param {import('zod').core.$ZodIssue} issue
441
- * @returns {PatramDiagnostic}
442
- */
443
- function createValidationDiagnostic(issue) {
444
- const issue_path = formatIssuePath(issue.path);
445
-
446
- if (issue_path) {
447
- return {
448
- code: 'config.invalid',
449
- column: 1,
450
- level: 'error',
451
- line: 1,
452
- message: `Invalid config at "${issue_path}": ${issue.message}`,
453
- path: CONFIG_FILE_NAME,
454
- };
455
- }
456
-
457
- return {
458
- code: 'config.invalid',
459
- column: 1,
460
- level: 'error',
461
- line: 1,
462
- message: `Invalid config: ${issue.message}`,
463
- path: CONFIG_FILE_NAME,
464
- };
465
- }
466
-
467
- /**
468
- * @param {{ fields?: Record<string, MetadataFieldConfig> }} repo_config
469
- * @param {import('zod').RefinementCtx} refinement_context
470
- */
471
- function validateFieldDefinitionKeys(repo_config, refinement_context) {
472
- for (const field_name of Object.keys(repo_config.fields ?? {})) {
473
- if (!field_name.startsWith('$')) {
474
- continue;
475
- }
476
-
477
- refinement_context.addIssue({
478
- code: 'custom',
479
- message: 'Metadata field names must not start with "$".',
480
- path: ['fields', field_name],
481
- });
482
- }
483
- }
484
-
485
- /**
486
- * @param {{ fields: Array<{ name: string }> }} summary_definition
487
- * @param {import('zod').RefinementCtx} refinement_context
488
- */
489
- function validateDerivedSummaryDefinition(
490
- summary_definition,
491
- refinement_context,
492
- ) {
493
- const seen_field_names = new Set();
494
-
495
- for (const [
496
- field_index,
497
- field_definition,
498
- ] of summary_definition.fields.entries()) {
499
- if (!seen_field_names.has(field_definition.name)) {
500
- seen_field_names.add(field_definition.name);
501
- continue;
502
- }
503
-
504
- refinement_context.addIssue({
505
- code: 'custom',
506
- message: `Duplicate derived summary field "${field_definition.name}".`,
507
- path: ['fields', field_index, 'name'],
508
- });
509
- }
510
- }
511
-
512
- /**
513
- * @param {PatramRepoConfig} repo_config
514
- * @returns {PatramDiagnostic[]}
515
- */
516
- function validateGraphSchema(repo_config) {
517
- if (
518
- repo_config.classes === undefined &&
519
- repo_config.mappings === undefined &&
520
- repo_config.relations === undefined
521
- ) {
522
- return [];
523
- }
524
-
525
- try {
526
- parsePatramConfig({
527
- classes: collectGraphClassDefinitions(repo_config.classes),
528
- mappings: repo_config.mappings ?? {},
529
- relations: repo_config.relations ?? {},
530
- });
531
- } catch (error) {
532
- if (error instanceof z.ZodError) {
533
- return error.issues.map(createValidationDiagnostic);
534
- }
535
-
536
- throw error;
537
- }
538
-
539
- return [];
540
- }
541
-
542
- /**
543
- * @param {PatramRepoConfig} repo_config
544
- * @returns {PatramDiagnostic[]}
545
- */
546
- function validateFieldSchemaConfig(repo_config) {
547
- const path_classes = repo_config.path_classes ?? {};
548
- const classes = repo_config.classes ?? {};
549
- const fields = repo_config.fields ?? {};
550
- /** @type {PatramDiagnostic[]} */
551
- const diagnostics = [];
552
-
553
- collectFieldConfigDiagnostics(diagnostics, path_classes, fields);
554
- collectClassSchemaConfigDiagnostics(
555
- diagnostics,
556
- path_classes,
557
- fields,
558
- classes,
559
- );
560
-
561
- return diagnostics;
562
- }
563
-
564
- /**
565
- * @param {PatramRepoConfig} repo_config
566
- * @returns {PatramDiagnostic[]}
567
- */
568
- function validateStoredQueries(repo_config) {
569
- /** @type {PatramDiagnostic[]} */
570
- const diagnostics = [];
571
-
572
- for (const [query_name, stored_query] of Object.entries(
573
- repo_config.queries,
574
- )) {
575
- collectWhereClauseDiagnostics(
576
- diagnostics,
577
- repo_config,
578
- stored_query.where,
579
- `queries.${query_name}.where`,
580
- );
581
- }
582
-
583
- return diagnostics;
584
- }
585
-
586
- /**
587
- * @param {PatramRepoConfig} repo_config
588
- * @returns {PatramDiagnostic[]}
589
- */
590
- function validateDerivedSummaries(repo_config) {
591
- if (!repo_config.derived_summaries) {
592
- return [];
593
- }
594
-
595
- const graph_config = resolvePatramGraphConfig(repo_config);
596
- const known_relation_names = new Set(Object.keys(graph_config.relations));
597
- /** @type {PatramDiagnostic[]} */
598
- const diagnostics = [];
599
- const class_coverage = new Map();
600
-
601
- for (const [summary_name, summary_definition] of Object.entries(
602
- repo_config.derived_summaries,
603
- )) {
604
- collectDuplicateClassDiagnostics(
605
- diagnostics,
606
- class_coverage,
607
- summary_definition.classes,
608
- summary_name,
609
- );
610
- collectDerivedSummaryFieldDiagnostics(
611
- diagnostics,
612
- known_relation_names,
613
- repo_config,
614
- summary_name,
615
- summary_definition.fields,
616
- );
617
- }
618
-
619
- return diagnostics;
620
- }
621
-
622
- /**
623
- * @returns {PatramRepoConfig}
624
- */
625
- function createDefaultRepoConfig() {
626
- return {
627
- include: [...DEFAULT_INCLUDE_PATTERNS],
628
- queries: {},
629
- };
630
- }
631
-
632
- /**
633
- * @param {PatramRepoConfig} repo_config
634
- * @returns {PatramRepoConfig}
635
- */
636
- function normalizeRepoConfig(repo_config) {
637
- /** @type {PatramRepoConfig} */
638
- const normalized_config = {
639
- include: [...repo_config.include],
640
- queries: { ...repo_config.queries },
641
- };
642
-
643
- assignOptionalRepoConfigField(
644
- normalized_config,
645
- 'classes',
646
- repo_config.classes,
647
- );
648
- assignOptionalRepoConfigField(
649
- normalized_config,
650
- 'derived_summaries',
651
- repo_config.derived_summaries,
652
- );
653
- assignOptionalRepoConfigField(
654
- normalized_config,
655
- 'fields',
656
- repo_config.fields,
657
- );
658
- assignOptionalRepoConfigField(
659
- normalized_config,
660
- 'mappings',
661
- repo_config.mappings,
662
- );
663
- assignOptionalRepoConfigField(
664
- normalized_config,
665
- 'path_classes',
666
- repo_config.path_classes,
667
- );
668
- assignOptionalRepoConfigField(
669
- normalized_config,
670
- 'relations',
671
- repo_config.relations,
672
- );
673
-
674
- return normalized_config;
675
- }
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
-
698
- /**
699
- * @param {PatramDiagnostic[]} diagnostics
700
- * @param {Map<string, string>} class_coverage
701
- * @param {string[]} class_names
702
- * @param {string} summary_name
703
- */
704
- function collectDuplicateClassDiagnostics(
705
- diagnostics,
706
- class_coverage,
707
- class_names,
708
- summary_name,
709
- ) {
710
- for (const class_name of class_names) {
711
- const existing_summary_name = class_coverage.get(class_name);
712
-
713
- if (!existing_summary_name) {
714
- class_coverage.set(class_name, summary_name);
715
- continue;
716
- }
717
-
718
- diagnostics.push(
719
- createConfigDiagnostic(
720
- `derived_summaries.${summary_name}.classes`,
721
- `Class "${class_name}" is already covered by derived summary "${existing_summary_name}".`,
722
- ),
723
- );
724
- }
725
- }
726
-
727
- /**
728
- * @param {PatramDiagnostic[]} diagnostics
729
- * @param {Set<string>} known_relation_names
730
- * @param {PatramRepoConfig} repo_config
731
- * @param {string} summary_name
732
- * @param {DerivedSummaryFieldConfig[]} field_definitions
733
- */
734
- function collectDerivedSummaryFieldDiagnostics(
735
- diagnostics,
736
- known_relation_names,
737
- repo_config,
738
- summary_name,
739
- field_definitions,
740
- ) {
741
- for (const [field_index, field_definition] of field_definitions.entries()) {
742
- if ('count' in field_definition) {
743
- collectTraversalDiagnostic(
744
- diagnostics,
745
- field_definition.count.traversal,
746
- known_relation_names,
747
- `derived_summaries.${summary_name}.fields.${field_index}.count.traversal`,
748
- );
749
- collectWhereClauseDiagnostics(
750
- diagnostics,
751
- repo_config,
752
- field_definition.count.where,
753
- `derived_summaries.${summary_name}.fields.${field_index}.count.where`,
754
- );
755
- continue;
756
- }
757
-
758
- for (const [case_index, select_case] of field_definition.select.entries()) {
759
- collectWhereClauseDiagnostics(
760
- diagnostics,
761
- repo_config,
762
- select_case.when,
763
- `derived_summaries.${summary_name}.fields.${field_index}.select.${case_index}.when`,
764
- );
765
- }
766
- }
767
- }
768
-
769
- /**
770
- * @param {PatramDiagnostic[]} diagnostics
771
- * @param {string} traversal_text
772
- * @param {Set<string>} known_relation_names
773
- * @param {string} diagnostic_path
774
- */
775
- function collectTraversalDiagnostic(
776
- diagnostics,
777
- traversal_text,
778
- known_relation_names,
779
- diagnostic_path,
780
- ) {
781
- const traversal_match =
782
- /^(?<direction>in|out):(?<relation_name>[a-zA-Z0-9_]+)$/du.exec(
783
- traversal_text,
784
- );
785
-
786
- if (!traversal_match?.groups?.relation_name) {
787
- diagnostics.push(
788
- createConfigDiagnostic(
789
- diagnostic_path,
790
- 'Derived summary traversal must use "in:<relation>" or "out:<relation>".',
791
- ),
792
- );
793
-
794
- return;
795
- }
796
-
797
- if (known_relation_names.has(traversal_match.groups.relation_name)) {
798
- return;
799
- }
800
-
801
- diagnostics.push(
802
- createConfigDiagnostic(
803
- diagnostic_path,
804
- `Unknown relation "${traversal_match.groups.relation_name}" in derived summary traversal.`,
805
- ),
806
- );
807
- }
808
-
809
- /**
810
- * @param {PatramDiagnostic[]} diagnostics
811
- * @param {Record<string, { prefixes: string[] }>} path_classes
812
- * @param {Record<string, MetadataFieldConfig>} fields
813
- */
814
- function collectFieldConfigDiagnostics(diagnostics, path_classes, fields) {
815
- for (const [field_name, field_definition] of Object.entries(fields)) {
816
- if (collectReservedFieldDiagnostic(diagnostics, field_name)) {
817
- continue;
818
- }
819
-
820
- collectDisplayOrderDiagnostic(diagnostics, field_name, field_definition);
821
- collectFieldPathClassDiagnostic(
822
- diagnostics,
823
- path_classes,
824
- field_name,
825
- field_definition,
826
- );
827
- }
828
- }
829
-
830
- /**
831
- * @param {PatramDiagnostic[]} diagnostics
832
- * @param {string} field_name
833
- * @returns {boolean}
834
- */
835
- function collectReservedFieldDiagnostic(diagnostics, field_name) {
836
- if (
837
- !field_name.startsWith('$') ||
838
- !RESERVED_STRUCTURAL_FIELD_NAMES.has(field_name)
839
- ) {
840
- return false;
841
- }
842
-
843
- diagnostics.push(
844
- createConfigDiagnostic(
845
- `fields.${field_name}`,
846
- 'Metadata field names must not start with "$".',
847
- ),
848
- );
849
-
850
- return true;
851
- }
852
-
853
- /**
854
- * @param {PatramDiagnostic[]} diagnostics
855
- * @param {string} field_name
856
- * @param {MetadataFieldConfig} field_definition
857
- */
858
- function collectDisplayOrderDiagnostic(
859
- diagnostics,
860
- field_name,
861
- field_definition,
862
- ) {
863
- if (
864
- field_definition.display?.order === undefined ||
865
- (Number.isInteger(field_definition.display.order) &&
866
- field_definition.display.order >= 0)
867
- ) {
868
- return;
869
- }
870
-
871
- diagnostics.push(
872
- createConfigDiagnostic(
873
- `fields.${field_name}.display.order`,
874
- 'Display order must be a non-negative integer.',
875
- ),
876
- );
877
- }
878
-
879
- /**
880
- * @param {PatramDiagnostic[]} diagnostics
881
- * @param {Record<string, { prefixes: string[] }>} path_classes
882
- * @param {string} field_name
883
- * @param {MetadataFieldConfig} field_definition
884
- */
885
- function collectFieldPathClassDiagnostic(
886
- diagnostics,
887
- path_classes,
888
- field_name,
889
- field_definition,
890
- ) {
891
- if (
892
- !('path_class' in field_definition) ||
893
- field_definition.path_class === undefined
894
- ) {
895
- return;
896
- }
897
-
898
- if (field_definition.type !== 'path') {
899
- diagnostics.push(
900
- createConfigDiagnostic(
901
- `fields.${field_name}.path_class`,
902
- 'Path classes are only valid for path fields.',
903
- ),
904
- );
905
-
906
- return;
907
- }
908
-
909
- if (path_classes[field_definition.path_class]) {
910
- return;
911
- }
912
-
913
- diagnostics.push(
914
- createConfigDiagnostic(
915
- `fields.${field_name}.path_class`,
916
- `Unknown path class "${field_definition.path_class}".`,
917
- ),
918
- );
919
- }
920
-
921
- /**
922
- * @param {PatramDiagnostic[]} diagnostics
923
- * @param {Record<string, { prefixes: string[] }>} path_classes
924
- * @param {Record<string, MetadataFieldConfig>} fields
925
- * @param {NonNullable<PatramRepoConfig['classes']>} classes
926
- */
927
- function collectClassSchemaConfigDiagnostics(
928
- diagnostics,
929
- path_classes,
930
- fields,
931
- classes,
932
- ) {
933
- for (const [class_name, class_definition] of Object.entries(classes)) {
934
- const schema_definition = class_definition.schema;
935
-
936
- if (!schema_definition) {
937
- continue;
938
- }
939
-
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,
949
- );
950
-
951
- for (const field_name of Object.keys(schema_definition.fields)) {
952
- if (fields[field_name]) {
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
- );
965
- }
966
- }
967
- }
968
-
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
-
976
- if (
977
- schema_definition.document_path_class === undefined ||
978
- path_classes[schema_definition.document_path_class]
979
- ) {
980
- continue;
981
- }
982
-
983
- diagnostics.push(
984
- createConfigDiagnostic(
985
- `classes.${class_name}.schema.document_path_class`,
986
- `Unknown path class "${schema_definition.document_path_class}".`,
987
- ),
988
- );
989
- }
990
- }
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
-
1071
- /**
1072
- * @template {Exclude<keyof PatramRepoConfig, 'include' | 'queries'>} TKey
1073
- * @param {PatramRepoConfig} normalized_config
1074
- * @param {TKey} field_name
1075
- * @param {unknown} field_value
1076
- */
1077
- function assignOptionalRepoConfigField(
1078
- normalized_config,
1079
- field_name,
1080
- field_value,
1081
- ) {
1082
- if (field_value === undefined || field_value === null) {
1083
- return;
1084
- }
1085
-
1086
- normalized_config[field_name] = /** @type {PatramRepoConfig[TKey]} */ (
1087
- field_value
1088
- );
1089
- }
1090
-
1091
- /**
1092
- * @param {PatramDiagnostic[]} diagnostics
1093
- * @param {PatramRepoConfig} repo_config
1094
- * @param {string} where_clause
1095
- * @param {string} diagnostic_path
1096
- */
1097
- function collectWhereClauseDiagnostics(
1098
- diagnostics,
1099
- repo_config,
1100
- where_clause,
1101
- diagnostic_path,
1102
- ) {
1103
- const parse_result = parseWhereClause(where_clause);
1104
-
1105
- if (!parse_result.success) {
1106
- diagnostics.push(
1107
- createConfigDiagnostic(diagnostic_path, parse_result.diagnostic.message),
1108
- );
1109
-
1110
- return;
1111
- }
1112
-
1113
- const semantic_diagnostics = getQuerySemanticDiagnostics(
1114
- repo_config,
1115
- { kind: 'ad_hoc' },
1116
- parse_result.expression,
1117
- );
1118
-
1119
- for (const semantic_diagnostic of semantic_diagnostics) {
1120
- diagnostics.push(
1121
- createConfigDiagnostic(diagnostic_path, semantic_diagnostic.message),
1122
- );
1123
- }
1124
- }
1125
-
1126
- /**
1127
- * @param {string} issue_path
1128
- * @param {string} message
1129
- * @returns {PatramDiagnostic}
1130
- */
1131
- function createConfigDiagnostic(issue_path, message) {
1132
- return {
1133
- code: 'config.invalid',
1134
- column: 1,
1135
- level: 'error',
1136
- line: 1,
1137
- message: `Invalid config at "${issue_path}": ${message}`,
1138
- path: CONFIG_FILE_NAME,
1139
- };
1140
- }
1141
-
1142
- /**
1143
- * @param {unknown} error
1144
- * @returns {error is NodeJS.ErrnoException}
1145
- */
1146
- function isMissingFileError(error) {
1147
- if (!(error instanceof Error)) {
1148
- return false;
1149
- }
1150
-
1151
- return 'code' in error && error.code === 'ENOENT';
1152
- }
1153
-
1154
- /**
1155
- * @param {string} config_source
1156
- * @param {string} error_message
1157
- * @returns {{ line: number, column: number }}
1158
- */
1159
- function getJsonSyntaxOrigin(config_source, error_message) {
1160
- const position_match = error_message.match(/position (?<offset>\d+)/du);
1161
-
1162
- if (position_match?.groups?.offset) {
1163
- const offset = Number.parseInt(position_match.groups.offset, 10);
1164
-
1165
- return getLineAndColumnFromOffset(config_source, offset);
1166
- }
1167
-
1168
- const token_match = error_message.match(/Unexpected token '(?<token>.)'/u);
1169
-
1170
- if (token_match?.groups?.token) {
1171
- const offset = config_source.lastIndexOf(token_match.groups.token);
1172
-
1173
- if (offset >= 0) {
1174
- return getLineAndColumnFromOffset(config_source, offset);
1175
- }
1176
- }
1177
-
1178
- return {
1179
- column: 1,
1180
- line: 1,
1181
- };
1182
- }
1183
-
1184
- /**
1185
- * @param {(string | number | symbol | undefined)[]} issue_path
1186
- * @returns {string}
1187
- */
1188
- function formatIssuePath(issue_path) {
1189
- return issue_path.map(String).join('.');
1190
- }
1191
-
1192
- /**
1193
- * @param {string} source_text
1194
- * @param {number} offset
1195
- * @returns {{ line: number, column: number }}
1196
- */
1197
- function getLineAndColumnFromOffset(source_text, offset) {
1198
- let line_number = 1;
1199
- let column_number = 1;
1200
-
1201
- for (const character of source_text.slice(0, offset)) {
1202
- if (character === '\n') {
1203
- line_number += 1;
1204
- column_number = 1;
1205
- continue;
1206
- }
1207
-
1208
- column_number += 1;
1209
- }
1210
-
1211
- return {
1212
- column: column_number,
1213
- line: line_number,
1214
- };
1215
- }