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.
- package/bin/patram.js +2 -2
- package/lib/{parse-cli-color-options.js → cli/color-options.js} +2 -2
- package/lib/cli/command-helpers.js +35 -0
- package/lib/cli/commands/check.js +73 -0
- package/lib/cli/commands/fields.js +57 -0
- package/lib/cli/commands/queries.js +41 -0
- package/lib/cli/commands/query.js +239 -0
- package/lib/cli/commands/refs.js +72 -0
- package/lib/cli/commands/show.js +58 -0
- package/lib/{cli-help-metadata.js → cli/help-metadata.js} +54 -15
- package/lib/cli/main.js +76 -0
- package/lib/{parse-cli-arguments-helpers.js → cli/parse-arguments-helpers.js} +18 -11
- package/lib/{parse-cli-arguments.js → cli/parse-arguments.js} +12 -12
- package/lib/{parse-cli-query-pagination.js → cli/query-pagination.js} +2 -2
- package/lib/{render-cli-help.js → cli/render-help.js} +4 -4
- package/lib/{resolve-output-mode.js → cli/resolve-output-mode.js} +2 -2
- package/lib/cli/test-helpers.js +30 -0
- package/lib/config/defaults.d.ts +10 -0
- package/lib/config/defaults.js +80 -0
- package/lib/config/load-patram-config.d.ts +76 -0
- package/lib/config/load-patram-config.js +315 -0
- package/lib/config/load-patram-config.types.d.ts +45 -0
- package/lib/{patram-config.d.ts → config/patram-config.d.ts} +31 -31
- package/lib/{patram-config.js → config/patram-config.js} +3 -3
- package/lib/{patram-config.types.d.ts → config/patram-config.types.d.ts} +1 -1
- package/lib/{resolve-patram-graph-config.d.ts → config/resolve-patram-graph-config.d.ts} +2 -2
- package/lib/{resolve-patram-graph-config.js → config/resolve-patram-graph-config.js} +3 -3
- package/lib/{load-patram-config.d.ts → config/schema.d.ts} +147 -191
- package/lib/config/schema.js +324 -0
- package/lib/{source-file-defaults.d.ts → config/source-file-defaults.d.ts} +0 -1
- package/lib/{source-file-defaults.js → config/source-file-defaults.js} +1 -1
- package/lib/config/validation.d.ts +27 -0
- package/lib/config/validation.js +615 -0
- package/lib/directive-validation-test-helpers.js +1 -1
- package/lib/{build-graph-identity.d.ts → graph/build-graph-identity.d.ts} +2 -2
- package/lib/{build-graph-identity.js → graph/build-graph-identity.js} +1 -1
- package/lib/{build-graph.d.ts → graph/build-graph.d.ts} +3 -3
- package/lib/{build-graph.js → graph/build-graph.js} +17 -13
- package/lib/{build-graph.types.d.ts → graph/build-graph.types.d.ts} +1 -1
- package/lib/graph/check-directive-metadata.d.ts +23 -0
- package/lib/{check-directive-metadata.js → graph/check-directive-metadata.js} +7 -7
- package/lib/graph/check-directive-path-target.d.ts +32 -0
- package/lib/{check-directive-path-target.js → graph/check-directive-path-target.js} +4 -4
- package/lib/graph/check-directive-value.d.ts +19 -0
- package/lib/{check-directive-value.js → graph/check-directive-value.js} +3 -3
- package/lib/graph/check-graph.d.ts +29 -0
- package/lib/{check-graph.js → graph/check-graph.js} +6 -6
- package/lib/graph/directive-diagnostics.d.ts +20 -0
- package/lib/{directive-diagnostics.js → graph/directive-diagnostics.js} +2 -2
- package/lib/graph/directive-type-rules.d.ts +18 -0
- package/lib/{directive-type-rules.js → graph/directive-type-rules.js} +3 -3
- package/lib/{document-node-identity.d.ts → graph/document-node-identity.d.ts} +2 -2
- package/lib/{document-node-identity.js → graph/document-node-identity.js} +2 -2
- package/lib/graph/inspect-reverse-references.d.ts +22 -0
- package/lib/graph/inspect-reverse-references.js +184 -0
- package/lib/{load-project-graph.d.ts → graph/load-project-graph.d.ts} +10 -10
- package/lib/{load-project-graph.js → graph/load-project-graph.js} +12 -12
- package/lib/{parse-where-clause.types.d.ts → graph/parse-where-clause.types.d.ts} +1 -1
- package/lib/{query-graph.d.ts → graph/query/execute.d.ts} +11 -11
- package/lib/{query-graph.js → graph/query/execute.js} +12 -12
- package/lib/{query-inspection.d.ts → graph/query/inspect.d.ts} +10 -8
- package/lib/{query-inspection.js → graph/query/inspect.js} +16 -17
- package/lib/{parse-where-clause.d.ts → graph/query/parse.d.ts} +6 -6
- package/lib/{parse-where-clause.js → graph/query/parse.js} +2 -2
- package/lib/graph/query/resolve.d.ts +30 -0
- package/lib/{resolve-where-clause.js → graph/query/resolve.js} +1 -1
- package/lib/graph/reverse-reference-test-helpers.d.ts +55 -0
- package/lib/graph/reverse-reference-test-helpers.js +76 -0
- package/lib/{command-output.js → output/command-output.js} +6 -5
- package/lib/{derived-summary.js → output/derived-summary.js} +7 -7
- package/lib/output/layout-incoming-references.js +105 -0
- package/lib/output/layout-incoming-summary-lines.js +16 -0
- package/lib/{layout-stored-queries.js → output/layout-stored-queries.js} +9 -9
- package/lib/{list-queries.js → output/list-queries.js} +1 -1
- package/lib/{render-check-output.js → output/render-check-output.js} +1 -1
- package/lib/{render-field-discovery.js → output/render-field-discovery.js} +3 -3
- package/lib/output/render-output-view.js +56 -0
- package/lib/{render-json-output.js → output/renderers/json.js} +92 -63
- package/lib/{render-plain-output.js → output/renderers/plain.js} +62 -7
- package/lib/{render-rich-output.js → output/renderers/rich.js} +69 -8
- package/lib/{resolve-check-target.js → output/resolve-check-target.js} +1 -1
- package/lib/{render-rich-source.js → output/rich-source/render.js} +6 -6
- package/lib/{show-document.js → output/show-document.js} +54 -16
- package/lib/{render-output-view.js → output/view-model/index.js} +56 -47
- package/lib/{write-paged-output.js → output/write-paged-output.js} +9 -5
- package/lib/{claim-helpers.d.ts → parse/claim-helpers.d.ts} +2 -2
- package/lib/{parse-jsdoc-claims.d.ts → parse/jsdoc/parse-jsdoc-claims.d.ts} +2 -2
- package/lib/{parse-jsdoc-claims.js → parse/jsdoc/parse-jsdoc-claims.js} +9 -9
- package/lib/{parse-jsdoc-prose.d.ts → parse/jsdoc/parse-jsdoc-prose.d.ts} +1 -1
- package/lib/{parse-jsdoc-prose.js → parse/jsdoc/parse-jsdoc-prose.js} +1 -1
- package/lib/{parse-markdown-claims.d.ts → parse/markdown/parse-markdown-claims.d.ts} +3 -3
- package/lib/{parse-markdown-claims.js → parse/markdown/parse-markdown-claims.js} +8 -8
- package/lib/{parse-markdown-directives.d.ts → parse/markdown/parse-markdown-directives.d.ts} +2 -2
- package/lib/{parse-markdown-directives.js → parse/markdown/parse-markdown-directives.js} +3 -3
- package/lib/{parse-claims.d.ts → parse/parse-claims.d.ts} +4 -13
- package/lib/{parse-claims.js → parse/parse-claims.js} +18 -26
- package/lib/{parse-claims.types.d.ts → parse/parse-claims.types.d.ts} +1 -1
- package/lib/{tagged-fenced-block-error.d.ts → parse/tagged-fenced/tagged-fenced-block-error.d.ts} +2 -2
- package/lib/{tagged-fenced-block-parser.d.ts → parse/tagged-fenced/tagged-fenced-block-parser.d.ts} +3 -3
- package/lib/{tagged-fenced-blocks.d.ts → parse/tagged-fenced/tagged-fenced-blocks.d.ts} +7 -7
- package/lib/{tagged-fenced-blocks.js → parse/tagged-fenced/tagged-fenced-blocks.js} +3 -3
- package/lib/{parse-yaml-claims.d.ts → parse/yaml/parse-yaml-claims.d.ts} +4 -4
- package/lib/{parse-yaml-claims.js → parse/yaml/parse-yaml-claims.js} +22 -13
- package/lib/patram.d.ts +29 -28
- package/lib/patram.js +5 -6
- package/lib/{discover-fields.js → scan/discover-fields.js} +9 -8
- package/lib/scan/list-repo-files.d.ts +16 -0
- package/lib/{list-source-files.js → scan/list-repo-files.js} +2 -35
- package/lib/{list-source-files.d.ts → scan/list-source-files.d.ts} +4 -11
- package/lib/scan/list-source-files.js +45 -0
- package/package.json +8 -7
- package/lib/build-graph.types.ts +0 -27
- package/lib/discover-fields.types.ts +0 -52
- package/lib/load-patram-config.js +0 -1215
- package/lib/load-patram-config.types.d.ts +0 -45
- package/lib/load-patram-config.types.ts +0 -56
- package/lib/output-view.types.d.ts +0 -80
- package/lib/output-view.types.ts +0 -96
- package/lib/overlay-graph.d.ts +0 -43
- package/lib/overlay-graph.js +0 -191
- package/lib/parse-claims.types.ts +0 -41
- package/lib/parse-cli-arguments.types.ts +0 -69
- package/lib/parse-where-clause.types.ts +0 -87
- package/lib/patram-cli.js +0 -528
- package/lib/patram-config.types.ts +0 -22
- package/lib/tagged-fenced-blocks.types.ts +0 -38
- /package/lib/{format-derived-summary-row.js → output/format-derived-summary-row.js} +0 -0
- /package/lib/{format-node-header.js → output/format-node-header.js} +0 -0
- /package/lib/{format-output-item-block.js → output/format-output-item-block.js} +0 -0
- /package/lib/{format-output-metadata.js → output/format-output-metadata.js} +0 -0
- /package/lib/{claim-helpers.js → parse/claim-helpers.js} +0 -0
- /package/lib/{parse-jsdoc-blocks.d.ts → parse/jsdoc/parse-jsdoc-blocks.d.ts} +0 -0
- /package/lib/{parse-jsdoc-blocks.js → parse/jsdoc/parse-jsdoc-blocks.js} +0 -0
- /package/lib/{tagged-fenced-block-error.js → parse/tagged-fenced/tagged-fenced-block-error.js} +0 -0
- /package/lib/{tagged-fenced-block-markdown.d.ts → parse/tagged-fenced/tagged-fenced-block-markdown.d.ts} +0 -0
- /package/lib/{tagged-fenced-block-markdown.js → parse/tagged-fenced/tagged-fenced-block-markdown.js} +0 -0
- /package/lib/{tagged-fenced-block-metadata.d.ts → parse/tagged-fenced/tagged-fenced-block-metadata.d.ts} +0 -0
- /package/lib/{tagged-fenced-block-metadata.js → parse/tagged-fenced/tagged-fenced-block-metadata.js} +0 -0
- /package/lib/{tagged-fenced-block-parser.js → parse/tagged-fenced/tagged-fenced-block-parser.js} +0 -0
- /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
|
-
}
|