patram 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
- /** @import * as $k$$l$document$j$node$j$identity$k$js from './document-node-identity.js'; */
2
1
  /**
2
+ * @import { DocumentNodeReference } from './document-node-identity.js';
3
3
  * @import { GraphNode } from './build-graph.types.ts';
4
4
  * @import { PatramClaim } from './parse-claims.types.ts';
5
5
  */
@@ -48,7 +48,7 @@ export function resolveNodeKey(node_mapping, claim, document_entity_keys) {
48
48
  * @param {'path' | 'value'} target_type
49
49
  * @param {PatramClaim} claim
50
50
  * @param {Map<string, string>} document_entity_keys
51
- * @param {Map<string, import('./document-node-identity.js').DocumentNodeReference>} document_node_references
51
+ * @param {Map<string, DocumentNodeReference>} document_node_references
52
52
  * @param {Set<string>} document_paths
53
53
  * @returns {{ class_name: string, key: string, path?: string }}
54
54
  */
@@ -124,7 +124,7 @@ function resolveValueTargetReference(target_kind, claim) {
124
124
  * @param {string} target_class
125
125
  * @param {PatramClaim} claim
126
126
  * @param {Map<string, string>} document_entity_keys
127
- * @param {Map<string, import('./document-node-identity.js').DocumentNodeReference>} document_node_references
127
+ * @param {Map<string, DocumentNodeReference>} document_node_references
128
128
  * @param {Set<string>} document_paths
129
129
  * @returns {{ class_name: string, key: string, path?: string }}
130
130
  */
@@ -171,7 +171,7 @@ function resolvePathTargetReference(
171
171
 
172
172
  /**
173
173
  * @param {string} target_path
174
- * @param {Map<string, import('./document-node-identity.js').DocumentNodeReference>} document_node_references
174
+ * @param {Map<string, DocumentNodeReference>} document_node_references
175
175
  * @returns {{ class_name: string, key: string, path?: string }}
176
176
  */
177
177
  function resolveDocumentTargetReference(target_path, document_node_references) {
@@ -201,7 +201,14 @@ function resolveDocumentTargetReference(target_path, document_node_references) {
201
201
  function resolveDirectiveAwareTargetPath(claim, raw_target, document_paths) {
202
202
  const normalized_raw_target = normalizeRepoRelativePath(raw_target);
203
203
 
204
- if (claim.type === 'directive' && document_paths.has(normalized_raw_target)) {
204
+ if (
205
+ claim.type === 'directive' &&
206
+ shouldKeepDirectiveTargetRepoRelative(
207
+ raw_target,
208
+ normalized_raw_target,
209
+ document_paths,
210
+ )
211
+ ) {
205
212
  return normalized_raw_target;
206
213
  }
207
214
 
@@ -212,6 +219,40 @@ function resolveDirectiveAwareTargetPath(claim, raw_target, document_paths) {
212
219
  return normalizeRepoRelativePath(posix.join(source_directory, raw_target));
213
220
  }
214
221
 
222
+ /**
223
+ * @param {string} raw_target
224
+ * @param {string} normalized_raw_target
225
+ * @param {Set<string>} document_paths
226
+ * @returns {boolean}
227
+ */
228
+ function shouldKeepDirectiveTargetRepoRelative(
229
+ raw_target,
230
+ normalized_raw_target,
231
+ document_paths,
232
+ ) {
233
+ if (raw_target.startsWith('./') || raw_target.startsWith('../')) {
234
+ return false;
235
+ }
236
+
237
+ if (document_paths.has(normalized_raw_target)) {
238
+ return true;
239
+ }
240
+
241
+ const target_root_segment = normalized_raw_target.split('/')[0];
242
+
243
+ if (!target_root_segment) {
244
+ return false;
245
+ }
246
+
247
+ for (const document_path of document_paths) {
248
+ if (document_path.split('/')[0] === target_root_segment) {
249
+ return true;
250
+ }
251
+ }
252
+
253
+ return false;
254
+ }
255
+
215
256
  /**
216
257
  * @param {PatramClaim} claim
217
258
  * @returns {string}
@@ -246,8 +287,8 @@ function getDocumentEntityMapKey(document_path, class_name) {
246
287
  }
247
288
 
248
289
  /**
249
- * @param {import('./document-node-identity.js').DocumentNodeReference | undefined} document_node_reference
250
- * @returns {document_node_reference is $k$$l$document$j$node$j$identity$k$js.DocumentNodeReference}
290
+ * @param {DocumentNodeReference | undefined} document_node_reference
291
+ * @returns {document_node_reference is DocumentNodeReference}
251
292
  */
252
293
  function documentNodeReferenceIsPromoted(document_node_reference) {
253
294
  return (
@@ -30,7 +30,7 @@ import {
30
30
  * Uses Term: ../docs/reference/terms/graph.md
31
31
  * Uses Term: ../docs/reference/terms/mapping.md
32
32
  * Uses Term: ../docs/reference/terms/relation.md
33
- * Tracked in: ../docs/plans/v0/source-anchor-dogfood.md
33
+ * Tracked in: ../docs/plans/v0/source-anchor-dogfooding.md
34
34
  * Decided by: ../docs/decisions/graph-materialization.md
35
35
  * Implements: ../docs/tasks/v0/materialize-graph.md
36
36
  * @patram
@@ -771,7 +771,7 @@ function getFieldDefinition(patram_config, field_name) {
771
771
  * @returns {{ fields?: Record<string, { presence: 'required' | 'optional' | 'forbidden' }>, unknown_fields?: 'ignore' | 'error' } | undefined}
772
772
  */
773
773
  function getClassSchema(patram_config, class_name) {
774
- return patram_config.class_schemas?.[class_name];
774
+ return patram_config.classes[class_name]?.schema;
775
775
  }
776
776
 
777
777
  /**
@@ -17,6 +17,7 @@ import {
17
17
  createDocumentDiagnostic,
18
18
  createOriginDiagnostic,
19
19
  } from './directive-diagnostics.js';
20
+ import { formatQuotedList } from './directive-type-rules.js';
20
21
  import { resolvePatramGraphConfig } from './resolve-patram-graph-config.js';
21
22
 
22
23
  /**
@@ -44,11 +45,7 @@ export function checkDirectiveMetadata(
44
45
  claims,
45
46
  existing_file_paths,
46
47
  ) {
47
- if (
48
- claims.length === 0 ||
49
- (repo_config.fields === undefined &&
50
- repo_config.class_schemas === undefined)
51
- ) {
48
+ if (claims.length === 0 || !hasDirectiveValidationConfig(repo_config)) {
52
49
  return [];
53
50
  }
54
51
 
@@ -93,6 +90,19 @@ export function checkDirectiveMetadata(
93
90
  return diagnostics;
94
91
  }
95
92
 
93
+ /**
94
+ * @param {PatramRepoConfig} repo_config
95
+ * @returns {boolean}
96
+ */
97
+ function hasDirectiveValidationConfig(repo_config) {
98
+ return (
99
+ repo_config.fields !== undefined ||
100
+ hasMarkdownStyleValidationConfig(repo_config) ||
101
+ hasConfiguredClassSchemas(repo_config) ||
102
+ hasPathTargetDirectiveMappings(repo_config)
103
+ );
104
+ }
105
+
96
106
  /**
97
107
  * @param {PatramClaim[]} claims
98
108
  * @returns {Map<string, PatramClaim[]>}
@@ -143,9 +153,11 @@ function collectDocumentDiagnostics(
143
153
  document_paths,
144
154
  ) {
145
155
  const document_kind = resolveDocumentKind(graph, document_path);
146
- const schema_definition = repo_config.class_schemas?.[document_kind];
156
+ const schema_definition = repo_config.classes?.[document_kind]?.schema;
147
157
  /** @type {Map<string, number>} */
148
158
  const directive_counts = new Map();
159
+ /** @type {Set<string>} */
160
+ const seen_markdown_styles = new Set();
149
161
 
150
162
  for (const claim of document_claims) {
151
163
  if (!claim.name) {
@@ -160,6 +172,7 @@ function collectDocumentDiagnostics(
160
172
  document_kind,
161
173
  schema_definition,
162
174
  directive_counts,
175
+ seen_markdown_styles,
163
176
  document_entity_keys,
164
177
  document_node_references,
165
178
  document_paths,
@@ -185,6 +198,7 @@ function collectDocumentDiagnostics(
185
198
  * @param {string} document_kind
186
199
  * @param {MetadataSchemaConfig | undefined} schema_definition
187
200
  * @param {Map<string, number>} directive_counts
201
+ * @param {Set<string>} seen_markdown_styles
188
202
  * @param {Map<string, string>} document_entity_keys
189
203
  * @param {Map<string, import('./document-node-identity.js').DocumentNodeReference>} document_node_references
190
204
  * @param {Set<string>} document_paths
@@ -197,6 +211,7 @@ function collectClaimDiagnostics(
197
211
  document_kind,
198
212
  schema_definition,
199
213
  directive_counts,
214
+ seen_markdown_styles,
200
215
  document_entity_keys,
201
216
  document_node_references,
202
217
  document_paths,
@@ -227,6 +242,12 @@ function collectClaimDiagnostics(
227
242
  schema_definition,
228
243
  next_count,
229
244
  ),
245
+ ...collectMarkdownStyleDiagnostics(
246
+ claim,
247
+ document_kind,
248
+ schema_definition,
249
+ seen_markdown_styles,
250
+ ),
230
251
  ...checkDirectiveValue(
231
252
  claim,
232
253
  claim.name,
@@ -240,6 +261,68 @@ function collectClaimDiagnostics(
240
261
  ];
241
262
  }
242
263
 
264
+ /**
265
+ * @param {PatramRepoConfig} repo_config
266
+ * @returns {boolean}
267
+ */
268
+ function hasConfiguredClassSchemas(repo_config) {
269
+ for (const class_definition of Object.values(repo_config.classes ?? {})) {
270
+ if (class_definition.schema) {
271
+ return true;
272
+ }
273
+ }
274
+
275
+ return false;
276
+ }
277
+
278
+ /**
279
+ * @param {PatramRepoConfig} repo_config
280
+ * @returns {boolean}
281
+ */
282
+ function hasMarkdownStyleValidationConfig(repo_config) {
283
+ for (const class_definition of Object.values(repo_config.classes ?? {})) {
284
+ const schema_definition = class_definition.schema;
285
+
286
+ if (!schema_definition) {
287
+ continue;
288
+ }
289
+
290
+ if (
291
+ schema_definition.markdown_styles !== undefined ||
292
+ schema_definition.mixed_styles !== undefined
293
+ ) {
294
+ return true;
295
+ }
296
+
297
+ for (const field_rule of Object.values(schema_definition.fields)) {
298
+ if (field_rule.markdown_styles !== undefined) {
299
+ return true;
300
+ }
301
+ }
302
+ }
303
+
304
+ return false;
305
+ }
306
+
307
+ /**
308
+ * @param {PatramRepoConfig} repo_config
309
+ * @returns {boolean}
310
+ */
311
+ function hasPathTargetDirectiveMappings(repo_config) {
312
+ for (const [mapping_name, mapping_definition] of Object.entries(
313
+ repo_config.mappings ?? {},
314
+ )) {
315
+ if (
316
+ mapping_name.includes('.directive.') &&
317
+ mapping_definition.emit?.target === 'path'
318
+ ) {
319
+ return true;
320
+ }
321
+ }
322
+
323
+ return false;
324
+ }
325
+
243
326
  /**
244
327
  * @param {PatramRepoConfig} repo_config
245
328
  * @param {string} document_path
@@ -532,3 +615,89 @@ function collectPlacementDiagnostics(
532
615
  ),
533
616
  ];
534
617
  }
618
+
619
+ /**
620
+ * @param {PatramClaim} claim
621
+ * @param {string} document_kind
622
+ * @param {MetadataSchemaConfig | undefined} schema_definition
623
+ * @param {Set<string>} seen_markdown_styles
624
+ * @returns {PatramDiagnostic[]}
625
+ */
626
+ function collectMarkdownStyleDiagnostics(
627
+ claim,
628
+ document_kind,
629
+ schema_definition,
630
+ seen_markdown_styles,
631
+ ) {
632
+ const markdown_style = resolveClaimMarkdownStyle(claim);
633
+
634
+ if (!markdown_style || !claim.name) {
635
+ return [];
636
+ }
637
+
638
+ /** @type {PatramDiagnostic[]} */
639
+ const diagnostics = [];
640
+ const allowed_styles =
641
+ schema_definition?.fields[claim.name]?.markdown_styles ??
642
+ schema_definition?.markdown_styles;
643
+
644
+ if (allowed_styles && !allowed_styles.includes(markdown_style)) {
645
+ diagnostics.push(
646
+ createOriginDiagnostic(
647
+ claim,
648
+ 'directive.invalid_style',
649
+ `Directive "${claim.name}" uses markdown style "${markdown_style}" but only ${formatQuotedList(allowed_styles)} are allowed.`,
650
+ ),
651
+ );
652
+ }
653
+
654
+ if (
655
+ shouldReportMixedStyles(
656
+ schema_definition,
657
+ seen_markdown_styles,
658
+ markdown_style,
659
+ )
660
+ ) {
661
+ diagnostics.push(
662
+ createOriginDiagnostic(
663
+ claim,
664
+ 'document.mixed_styles',
665
+ `Document mixes markdown directive style "${markdown_style}" with ${formatQuotedList([...seen_markdown_styles])} while class "${document_kind}" sets mixed_styles="error".`,
666
+ ),
667
+ );
668
+ }
669
+
670
+ seen_markdown_styles.add(markdown_style);
671
+
672
+ return diagnostics;
673
+ }
674
+
675
+ /**
676
+ * @param {PatramClaim} claim
677
+ * @returns {string | null}
678
+ */
679
+ function resolveClaimMarkdownStyle(claim) {
680
+ if (claim.parser !== 'markdown' || claim.markdown_style === undefined) {
681
+ return null;
682
+ }
683
+
684
+ return claim.markdown_style;
685
+ }
686
+
687
+ /**
688
+ * @param {MetadataSchemaConfig | undefined} schema_definition
689
+ * @param {Set<string>} seen_markdown_styles
690
+ * @param {string} markdown_style
691
+ * @returns {boolean}
692
+ */
693
+ function shouldReportMixedStyles(
694
+ schema_definition,
695
+ seen_markdown_styles,
696
+ markdown_style,
697
+ ) {
698
+ return (
699
+ schema_definition?.mixed_styles === 'error' &&
700
+ seen_markdown_styles.size === 1 &&
701
+ !seen_markdown_styles.has(markdown_style)
702
+ );
703
+ }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * @import { DirectiveTypeConfig, PatramDiagnostic, PatramRepoConfig } from './load-patram-config.types.ts';
3
+ * @import { PatramClaim } from './parse-claims.types.ts';
4
+ * @import { MappingDefinition } from './patram-config.types.ts';
5
+ */
6
+
7
+ import { isPathLikeTarget } from './claim-helpers.js';
8
+ import { resolveTargetReference } from './build-graph-identity.js';
9
+ import { createOriginDiagnostic } from './directive-diagnostics.js';
10
+
11
+ /**
12
+ * @param {PatramClaim} claim
13
+ * @param {string} directive_name
14
+ * @param {MappingDefinition | null} mapping_definition
15
+ * @param {Exclude<DirectiveTypeConfig, { type: 'enum' }> | undefined} type_definition
16
+ * @param {Map<string, string>} document_entity_keys
17
+ * @param {Map<string, import('./document-node-identity.js').DocumentNodeReference>} document_node_references
18
+ * @param {Set<string>} document_paths
19
+ * @returns {PatramDiagnostic[]}
20
+ */
21
+ export function createPathExistenceDiagnostics(
22
+ claim,
23
+ directive_name,
24
+ mapping_definition,
25
+ type_definition,
26
+ document_entity_keys,
27
+ document_node_references,
28
+ document_paths,
29
+ ) {
30
+ if (
31
+ typeof claim.value !== 'string' ||
32
+ !isPathLikeTarget(claim.value) ||
33
+ !shouldCheckDirectivePathExistence(mapping_definition, type_definition)
34
+ ) {
35
+ return [];
36
+ }
37
+
38
+ const resolved_target = resolveDirectiveTargetPath(
39
+ claim,
40
+ mapping_definition,
41
+ document_entity_keys,
42
+ document_node_references,
43
+ document_paths,
44
+ );
45
+
46
+ if (!resolved_target || document_paths.has(resolved_target)) {
47
+ return [];
48
+ }
49
+
50
+ return [
51
+ createOriginDiagnostic(
52
+ claim,
53
+ 'directive.path_not_found',
54
+ `Directive "${directive_name}" points to missing file "${resolved_target}".`,
55
+ ),
56
+ ];
57
+ }
58
+
59
+ /**
60
+ * @param {PatramClaim} claim
61
+ * @param {string} directive_name
62
+ * @param {Record<string, MappingDefinition>} mappings
63
+ * @param {PatramRepoConfig} repo_config
64
+ * @param {Exclude<DirectiveTypeConfig, { type: 'enum' }>} type_definition
65
+ * @param {Map<string, string>} document_entity_keys
66
+ * @param {Map<string, import('./document-node-identity.js').DocumentNodeReference>} document_node_references
67
+ * @param {Set<string>} document_paths
68
+ * @returns {PatramDiagnostic[]}
69
+ */
70
+ export function createPathClassDiagnostics(
71
+ claim,
72
+ directive_name,
73
+ mappings,
74
+ repo_config,
75
+ type_definition,
76
+ document_entity_keys,
77
+ document_node_references,
78
+ document_paths,
79
+ ) {
80
+ if (type_definition.type !== 'path' || !type_definition.path_class) {
81
+ return [];
82
+ }
83
+
84
+ const path_class_definition =
85
+ repo_config.path_classes?.[type_definition.path_class];
86
+
87
+ if (!path_class_definition) {
88
+ return [];
89
+ }
90
+
91
+ const mapping_definition = resolveDirectiveMapping(mappings, claim);
92
+ const resolved_target = resolveDirectiveTargetPath(
93
+ claim,
94
+ mapping_definition,
95
+ document_entity_keys,
96
+ document_node_references,
97
+ document_paths,
98
+ );
99
+
100
+ if (
101
+ !resolved_target ||
102
+ path_class_definition.prefixes.some((prefix) =>
103
+ resolved_target.startsWith(prefix),
104
+ )
105
+ ) {
106
+ return [];
107
+ }
108
+
109
+ return [
110
+ createOriginDiagnostic(
111
+ claim,
112
+ 'directive.invalid_path_class',
113
+ `Directive "${directive_name}" must point to path class "${type_definition.path_class}".`,
114
+ ),
115
+ ];
116
+ }
117
+
118
+ /**
119
+ * @param {PatramClaim} claim
120
+ * @param {MappingDefinition | null} mapping_definition
121
+ * @param {Map<string, string>} document_entity_keys
122
+ * @param {Map<string, import('./document-node-identity.js').DocumentNodeReference>} document_node_references
123
+ * @param {Set<string>} document_paths
124
+ * @returns {string | undefined}
125
+ */
126
+ function resolveDirectiveTargetPath(
127
+ claim,
128
+ mapping_definition,
129
+ document_entity_keys,
130
+ document_node_references,
131
+ document_paths,
132
+ ) {
133
+ const target_kind = mapping_definition?.emit?.target_class ?? 'document';
134
+ const resolved_target = resolveTargetReference(
135
+ target_kind,
136
+ 'path',
137
+ claim,
138
+ document_entity_keys,
139
+ document_node_references,
140
+ document_paths,
141
+ );
142
+
143
+ return resolved_target.path;
144
+ }
145
+
146
+ /**
147
+ * @param {Record<string, MappingDefinition>} mappings
148
+ * @param {PatramClaim} claim
149
+ * @returns {MappingDefinition | null}
150
+ */
151
+ function resolveDirectiveMapping(mappings, claim) {
152
+ if (!claim.name || !claim.parser) {
153
+ return null;
154
+ }
155
+
156
+ return mappings[`${claim.parser}.directive.${claim.name}`] ?? null;
157
+ }
158
+
159
+ /**
160
+ * @param {MappingDefinition | null} mapping_definition
161
+ * @param {Exclude<DirectiveTypeConfig, { type: 'enum' }> | undefined} type_definition
162
+ * @returns {boolean}
163
+ */
164
+ function shouldCheckDirectivePathExistence(
165
+ mapping_definition,
166
+ type_definition,
167
+ ) {
168
+ if (type_definition?.type === 'path') {
169
+ return true;
170
+ }
171
+
172
+ return mapping_definition?.emit?.target === 'path';
173
+ }