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.
@@ -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}
@@ -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
@@ -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
- !hasConfiguredClassSchemas(repo_config))
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[]>}
@@ -146,6 +156,8 @@ function collectDocumentDiagnostics(
146
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,
@@ -254,6 +275,54 @@ function hasConfiguredClassSchemas(repo_config) {
254
275
  return false;
255
276
  }
256
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
+
257
326
  /**
258
327
  * @param {PatramRepoConfig} repo_config
259
328
  * @param {string} document_path
@@ -546,3 +615,89 @@ function collectPlacementDiagnostics(
546
615
  ),
547
616
  ];
548
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
+ }