patram 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,10 @@
4
4
  * @import { MappingDefinition } from './patram-config.types.ts';
5
5
  */
6
6
 
7
- import { resolveTargetReference } from './build-graph-identity.js';
7
+ import {
8
+ createPathClassDiagnostics,
9
+ createPathExistenceDiagnostics,
10
+ } from './check-directive-path-target.js';
8
11
  import { createOriginDiagnostic } from './directive-diagnostics.js';
9
12
  import {
10
13
  formatQuotedList,
@@ -40,45 +43,31 @@ export function checkDirectiveValue(
40
43
  directive_name,
41
44
  mapping_definition,
42
45
  );
43
-
44
46
  if (!validation_field_name || typeof claim.value !== 'string') {
45
47
  return [];
46
48
  }
47
-
49
+ const directive_value = claim.value;
48
50
  if (validation_field_name === '$class') {
49
- return checkClassValue(claim, directive_name, repo_config);
50
- }
51
-
52
- if (isStructuralDirectiveField(validation_field_name)) {
53
- return [];
51
+ return checkClassValue(claim, directive_name, directive_value, repo_config);
54
52
  }
55
-
56
53
  const type_definition = repo_config.fields?.[validation_field_name];
57
-
58
- if (!type_definition) {
59
- return [];
60
- }
61
-
62
- if (type_definition.type === 'enum') {
63
- return checkEnumValue(claim, directive_name, type_definition.values);
64
- }
65
-
66
- const type_diagnostic = createInvalidTypeDiagnostic(
67
- claim,
68
- directive_name,
69
- type_definition,
70
- claim.value,
71
- );
72
-
73
- if (type_diagnostic) {
74
- return [type_diagnostic];
54
+ if (isStructuralDirectiveField(validation_field_name) || !type_definition) {
55
+ return collectUntypedPathDiagnostics(
56
+ claim,
57
+ directive_name,
58
+ mapping_definition,
59
+ document_entity_keys,
60
+ document_node_references,
61
+ document_paths,
62
+ );
75
63
  }
76
-
77
- return createPathClassDiagnostics(
64
+ return checkTypedDirectiveValue(
78
65
  claim,
79
66
  directive_name,
67
+ directive_value,
80
68
  mappings,
81
69
  repo_config,
70
+ mapping_definition,
82
71
  type_definition,
83
72
  document_entity_keys,
84
73
  document_node_references,
@@ -89,14 +78,12 @@ export function checkDirectiveValue(
89
78
  /**
90
79
  * @param {PatramClaim} claim
91
80
  * @param {string} directive_name
81
+ * @param {string} class_name
92
82
  * @param {PatramRepoConfig} repo_config
93
83
  * @returns {PatramDiagnostic[]}
94
84
  */
95
- function checkClassValue(claim, directive_name, repo_config) {
96
- if (
97
- typeof claim.value !== 'string' ||
98
- repo_config.classes?.[claim.value] !== undefined
99
- ) {
85
+ function checkClassValue(claim, directive_name, class_name, repo_config) {
86
+ if (repo_config.classes?.[class_name] !== undefined) {
100
87
  return [];
101
88
  }
102
89
 
@@ -112,44 +99,85 @@ function checkClassValue(claim, directive_name, repo_config) {
112
99
  /**
113
100
  * @param {PatramClaim} claim
114
101
  * @param {string} directive_name
115
- * @param {string[]} allowed_values
102
+ * @param {string} directive_value
103
+ * @param {Record<string, MappingDefinition>} mappings
104
+ * @param {PatramRepoConfig} repo_config
105
+ * @param {MappingDefinition | null} mapping_definition
106
+ * @param {DirectiveTypeConfig} type_definition
107
+ * @param {Map<string, string>} document_entity_keys
108
+ * @param {Map<string, import('./document-node-identity.js').DocumentNodeReference>} document_node_references
109
+ * @param {Set<string>} document_paths
116
110
  * @returns {PatramDiagnostic[]}
117
111
  */
118
- function checkEnumValue(claim, directive_name, allowed_values) {
119
- if (typeof claim.value !== 'string' || allowed_values.includes(claim.value)) {
120
- return [];
112
+ function checkTypedDirectiveValue(
113
+ claim,
114
+ directive_name,
115
+ directive_value,
116
+ mappings,
117
+ repo_config,
118
+ mapping_definition,
119
+ type_definition,
120
+ document_entity_keys,
121
+ document_node_references,
122
+ document_paths,
123
+ ) {
124
+ if (type_definition.type === 'enum') {
125
+ return checkEnumValue(
126
+ claim,
127
+ directive_name,
128
+ directive_value,
129
+ type_definition.values,
130
+ );
121
131
  }
122
132
 
123
- return [
124
- createOriginDiagnostic(
125
- claim,
126
- 'directive.invalid_enum',
127
- `Directive "${directive_name}" must be one of ${formatQuotedList(allowed_values)}.`,
128
- ),
129
- ];
133
+ if (!isDirectiveValueValid(type_definition, directive_value)) {
134
+ return [
135
+ createOriginDiagnostic(
136
+ claim,
137
+ 'directive.invalid_type',
138
+ getInvalidTypeMessage(directive_name, type_definition.type),
139
+ ),
140
+ ];
141
+ }
142
+
143
+ return collectPathDiagnostics(
144
+ claim,
145
+ directive_name,
146
+ mappings,
147
+ repo_config,
148
+ mapping_definition,
149
+ type_definition,
150
+ document_entity_keys,
151
+ document_node_references,
152
+ document_paths,
153
+ );
130
154
  }
131
155
 
132
156
  /**
133
157
  * @param {PatramClaim} claim
134
158
  * @param {string} directive_name
135
- * @param {Exclude<DirectiveTypeConfig, { type: 'enum' }>} type_definition
136
- * @param {string} directive_value
137
- * @returns {PatramDiagnostic | null}
159
+ * @param {MappingDefinition | null} mapping_definition
160
+ * @param {Map<string, string>} document_entity_keys
161
+ * @param {Map<string, import('./document-node-identity.js').DocumentNodeReference>} document_node_references
162
+ * @param {Set<string>} document_paths
163
+ * @returns {PatramDiagnostic[]}
138
164
  */
139
- function createInvalidTypeDiagnostic(
165
+ function collectUntypedPathDiagnostics(
140
166
  claim,
141
167
  directive_name,
142
- type_definition,
143
- directive_value,
168
+ mapping_definition,
169
+ document_entity_keys,
170
+ document_node_references,
171
+ document_paths,
144
172
  ) {
145
- if (isDirectiveValueValid(type_definition, directive_value)) {
146
- return null;
147
- }
148
-
149
- return createOriginDiagnostic(
173
+ return createPathExistenceDiagnostics(
150
174
  claim,
151
- 'directive.invalid_type',
152
- getInvalidTypeMessage(directive_name, type_definition.type),
175
+ directive_name,
176
+ mapping_definition,
177
+ undefined,
178
+ document_entity_keys,
179
+ document_node_references,
180
+ document_paths,
153
181
  );
154
182
  }
155
183
 
@@ -158,90 +186,70 @@ function createInvalidTypeDiagnostic(
158
186
  * @param {string} directive_name
159
187
  * @param {Record<string, MappingDefinition>} mappings
160
188
  * @param {PatramRepoConfig} repo_config
189
+ * @param {MappingDefinition | null} mapping_definition
161
190
  * @param {Exclude<DirectiveTypeConfig, { type: 'enum' }>} type_definition
162
191
  * @param {Map<string, string>} document_entity_keys
163
192
  * @param {Map<string, import('./document-node-identity.js').DocumentNodeReference>} document_node_references
164
193
  * @param {Set<string>} document_paths
165
194
  * @returns {PatramDiagnostic[]}
166
195
  */
167
- function createPathClassDiagnostics(
196
+ function collectPathDiagnostics(
168
197
  claim,
169
198
  directive_name,
170
199
  mappings,
171
200
  repo_config,
201
+ mapping_definition,
172
202
  type_definition,
173
203
  document_entity_keys,
174
204
  document_node_references,
175
205
  document_paths,
176
206
  ) {
177
- if (
178
- type_definition.type !== 'path' ||
179
- type_definition.path_class === undefined ||
180
- isDirectivePathInClass(
181
- mappings,
207
+ return createPathClassDiagnostics(
208
+ claim,
209
+ directive_name,
210
+ mappings,
211
+ repo_config,
212
+ type_definition,
213
+ document_entity_keys,
214
+ document_node_references,
215
+ document_paths,
216
+ ).concat(
217
+ createPathExistenceDiagnostics(
182
218
  claim,
183
- type_definition.path_class,
219
+ directive_name,
220
+ mapping_definition,
221
+ type_definition,
184
222
  document_entity_keys,
185
223
  document_node_references,
186
224
  document_paths,
187
- repo_config,
188
- )
189
- ) {
190
- return [];
191
- }
192
-
193
- return [
194
- createOriginDiagnostic(
195
- claim,
196
- 'directive.invalid_path_class',
197
- `Directive "${directive_name}" must point to path class "${type_definition.path_class}".`,
198
225
  ),
199
- ];
226
+ );
200
227
  }
201
228
 
202
229
  /**
203
- * @param {Record<string, MappingDefinition>} mappings
204
230
  * @param {PatramClaim} claim
205
- * @param {string} path_class_name
206
- * @param {Map<string, string>} document_entity_keys
207
- * @param {Map<string, import('./document-node-identity.js').DocumentNodeReference>} document_node_references
208
- * @param {Set<string>} document_paths
209
- * @param {PatramRepoConfig} repo_config
210
- * @returns {boolean}
231
+ * @param {string} directive_name
232
+ * @param {string} directive_value
233
+ * @param {string[]} allowed_values
234
+ * @returns {PatramDiagnostic[]}
211
235
  */
212
- function isDirectivePathInClass(
213
- mappings,
236
+ function checkEnumValue(
214
237
  claim,
215
- path_class_name,
216
- document_entity_keys,
217
- document_node_references,
218
- document_paths,
219
- repo_config,
238
+ directive_name,
239
+ directive_value,
240
+ allowed_values,
220
241
  ) {
221
- const path_class_definition = repo_config.path_classes?.[path_class_name];
222
-
223
- if (!path_class_definition) {
224
- return true;
225
- }
226
-
227
- const mapping_definition = resolveDirectiveMapping(mappings, claim);
228
- const target_kind = mapping_definition?.emit?.target_class ?? 'document';
229
- const resolved_target = resolveTargetReference(
230
- target_kind,
231
- 'path',
232
- claim,
233
- document_entity_keys,
234
- document_node_references,
235
- document_paths,
236
- );
237
-
238
- if (!resolved_target.path) {
239
- return false;
242
+ if (allowed_values.includes(directive_value)) {
243
+ return [];
240
244
  }
241
245
 
242
- return path_class_definition.prefixes.some((prefix) =>
243
- resolved_target.path?.startsWith(prefix),
244
- );
246
+ return [
247
+ createOriginDiagnostic(
248
+ claim,
249
+ 'directive.invalid_enum',
250
+ `Directive "${directive_name}" must be one of ${formatQuotedList(allowed_values)}.`,
251
+ ),
252
+ ];
245
253
  }
246
254
 
247
255
  /**
@@ -263,11 +271,7 @@ function resolveDirectiveMapping(mappings, claim) {
263
271
  * @returns {string}
264
272
  */
265
273
  function getDirectiveValidationFieldName(directive_name, mapping_definition) {
266
- if (mapping_definition?.node?.field) {
267
- return mapping_definition.node.field;
268
- }
269
-
270
- return directive_name;
274
+ return mapping_definition?.node?.field ?? directive_name;
271
275
  }
272
276
 
273
277
  /**
@@ -282,10 +286,3 @@ function isStructuralDirectiveField(field_name) {
282
286
  field_name === 'title'
283
287
  );
284
288
  }
285
-
286
- /**
287
- * @param {PatramClaim} claim
288
- * @param {string} code
289
- * @param {string} message
290
- * @returns {PatramDiagnostic}
291
- */
@@ -0,0 +1,87 @@
1
+ /**
2
+ * @import { MappingDefinition } from './patram-config.types.ts';
3
+ */
4
+
5
+ /**
6
+ * @returns {Record<string, { prefixes: string[] }>}
7
+ */
8
+ export function createDirectivePathClasses() {
9
+ return {
10
+ decision_docs: {
11
+ prefixes: ['docs/decisions/'],
12
+ },
13
+ plan_docs: {
14
+ prefixes: ['docs/plans/'],
15
+ },
16
+ task_docs: {
17
+ prefixes: ['docs/tasks/'],
18
+ },
19
+ };
20
+ }
21
+
22
+ /**
23
+ * @returns {Record<string, { from: string[], to: string[] }>}
24
+ */
25
+ export function createDirectiveRelations() {
26
+ return {
27
+ decided_by: {
28
+ from: ['document'],
29
+ to: ['document'],
30
+ },
31
+ tracked_in: {
32
+ from: ['document'],
33
+ to: ['document'],
34
+ },
35
+ };
36
+ }
37
+
38
+ /**
39
+ * @returns {Record<string, MappingDefinition>}
40
+ */
41
+ export function createMarkdownDirectiveMappings() {
42
+ return {
43
+ 'markdown.directive.decided_by': createRelationMapping('decided_by'),
44
+ 'markdown.directive.kind': createNodeMapping('$class'),
45
+ 'markdown.directive.status': createNodeMapping('status'),
46
+ 'markdown.directive.tracked_in': createRelationMapping('tracked_in'),
47
+ };
48
+ }
49
+
50
+ /**
51
+ * @returns {Record<string, MappingDefinition>}
52
+ */
53
+ export function createJsdocDirectiveMappings() {
54
+ return {
55
+ 'jsdoc.directive.decided_by': createRelationMapping('decided_by'),
56
+ 'jsdoc.directive.kind': createNodeMapping('$class'),
57
+ 'jsdoc.directive.status': createNodeMapping('status'),
58
+ 'jsdoc.directive.tracked_in': createRelationMapping('tracked_in'),
59
+ };
60
+ }
61
+
62
+ /**
63
+ * @param {string} field_name
64
+ * @returns {MappingDefinition}
65
+ */
66
+ function createNodeMapping(field_name) {
67
+ return {
68
+ node: {
69
+ class: 'document',
70
+ field: field_name,
71
+ },
72
+ };
73
+ }
74
+
75
+ /**
76
+ * @param {string} relation_name
77
+ * @returns {MappingDefinition}
78
+ */
79
+ function createRelationMapping(relation_name) {
80
+ return {
81
+ emit: {
82
+ relation: relation_name,
83
+ target: 'path',
84
+ target_class: 'document',
85
+ },
86
+ };
87
+ }
@@ -40,6 +40,14 @@ import { DEFAULT_INCLUDE_PATTERNS } from './source-file-defaults.js';
40
40
 
41
41
  const CONFIG_FILE_NAME = '.patram.json';
42
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']);
43
51
 
44
52
  /**
45
53
  * @typedef {object} PatramDiagnostic
@@ -224,6 +232,7 @@ const metadata_field_schema = z.discriminatedUnion('type', [
224
232
  */
225
233
  const class_field_rule_schema = z
226
234
  .object({
235
+ markdown_styles: z.array(z.string().min(1)).optional(),
227
236
  presence: z.enum(['required', 'optional', 'forbidden']),
228
237
  })
229
238
  .strict();
@@ -235,6 +244,8 @@ const class_schema_schema = z
235
244
  .object({
236
245
  document_path_class: z.string().min(1).optional(),
237
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(),
238
249
  unknown_fields: z.enum(['ignore', 'error']).optional(),
239
250
  })
240
251
  .strict();
@@ -926,17 +937,32 @@ function collectClassSchemaConfigDiagnostics(
926
937
  continue;
927
938
  }
928
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
+
929
951
  for (const field_name of Object.keys(schema_definition.fields)) {
930
952
  if (fields[field_name]) {
931
- continue;
953
+ collectMarkdownStylesDiagnostic(
954
+ diagnostics,
955
+ `classes.${class_name}.schema.fields.${field_name}.markdown_styles`,
956
+ schema_definition.fields[field_name].markdown_styles,
957
+ );
958
+ } else {
959
+ diagnostics.push(
960
+ createConfigDiagnostic(
961
+ `classes.${class_name}.schema.fields.${field_name}`,
962
+ `Unknown field "${field_name}".`,
963
+ ),
964
+ );
932
965
  }
933
-
934
- diagnostics.push(
935
- createConfigDiagnostic(
936
- `classes.${class_name}.schema.fields.${field_name}`,
937
- `Unknown field "${field_name}".`,
938
- ),
939
- );
940
966
  }
941
967
  }
942
968
 
@@ -963,6 +989,67 @@ function collectClassSchemaConfigDiagnostics(
963
989
  }
964
990
  }
965
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
+
966
1053
  /**
967
1054
  * @param {PatramRepoConfig['classes']} classes
968
1055
  * @returns {Record<string, ClassDefinition>}
@@ -10,7 +10,7 @@ import { resolve } from 'node:path';
10
10
  import { buildGraph } from './build-graph.js';
11
11
  import { listSourceFiles } from './list-source-files.js';
12
12
  import { loadPatramConfig } from './load-patram-config.js';
13
- import { parseSourceFile } from './parse-claims.js';
13
+ import { createParseOptions, parseSourceFile } from './parse-claims.js';
14
14
  import { resolvePatramGraphConfig } from './resolve-patram-graph-config.js';
15
15
 
16
16
  /**
@@ -68,6 +68,7 @@ export async function loadProjectGraph(project_directory) {
68
68
  project_directory,
69
69
  );
70
70
  const collect_result = await collectClaims(
71
+ repo_config,
71
72
  source_file_paths,
72
73
  project_directory,
73
74
  );
@@ -96,25 +97,34 @@ export async function loadProjectGraph(project_directory) {
96
97
  }
97
98
 
98
99
  /**
100
+ * @param {PatramRepoConfig} repo_config
99
101
  * @param {string[]} source_file_paths
100
102
  * @param {string} project_directory
101
103
  * @returns {Promise<{ claims: PatramClaim[], diagnostics: PatramDiagnostic[] }>}
102
104
  */
103
- async function collectClaims(source_file_paths, project_directory) {
105
+ async function collectClaims(
106
+ repo_config,
107
+ source_file_paths,
108
+ project_directory,
109
+ ) {
104
110
  /** @type {PatramClaim[]} */
105
111
  const claims = [];
106
112
  /** @type {PatramDiagnostic[]} */
107
113
  const diagnostics = [];
114
+ const parse_options = createParseOptions(repo_config);
108
115
 
109
116
  for (const source_file_path of source_file_paths) {
110
117
  const source_text = await readFile(
111
118
  resolve(project_directory, source_file_path),
112
119
  'utf8',
113
120
  );
114
- const parse_result = parseSourceFile({
115
- path: source_file_path,
116
- source: source_text,
117
- });
121
+ const parse_result = parseSourceFile(
122
+ {
123
+ path: source_file_path,
124
+ source: source_text,
125
+ },
126
+ parse_options,
127
+ );
118
128
 
119
129
  claims.push(...parse_result.claims);
120
130
  diagnostics.push(...parse_result.diagnostics);