@contractual/governance 0.1.0-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/dist/differs/index.d.ts +11 -0
  3. package/dist/differs/index.d.ts.map +1 -0
  4. package/dist/differs/index.js +11 -0
  5. package/dist/differs/index.js.map +1 -0
  6. package/dist/differs/json-schema/index.d.ts +8 -0
  7. package/dist/differs/json-schema/index.d.ts.map +1 -0
  8. package/dist/differs/json-schema/index.js +9 -0
  9. package/dist/differs/json-schema/index.js.map +1 -0
  10. package/dist/differs/openapi-diff.d.ts +22 -0
  11. package/dist/differs/openapi-diff.d.ts.map +1 -0
  12. package/dist/differs/openapi-diff.js +113 -0
  13. package/dist/differs/openapi-diff.js.map +1 -0
  14. package/dist/index.d.ts +22 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +44 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/linters/index.d.ts +9 -0
  19. package/dist/linters/index.d.ts.map +1 -0
  20. package/dist/linters/index.js +11 -0
  21. package/dist/linters/index.js.map +1 -0
  22. package/dist/linters/json-schema-ajv.d.ts +42 -0
  23. package/dist/linters/json-schema-ajv.d.ts.map +1 -0
  24. package/dist/linters/json-schema-ajv.js +146 -0
  25. package/dist/linters/json-schema-ajv.js.map +1 -0
  26. package/dist/linters/json-schema-rules.d.ts +42 -0
  27. package/dist/linters/json-schema-rules.d.ts.map +1 -0
  28. package/dist/linters/json-schema-rules.js +747 -0
  29. package/dist/linters/json-schema-rules.js.map +1 -0
  30. package/dist/linters/openapi-redocly.d.ts +15 -0
  31. package/dist/linters/openapi-redocly.d.ts.map +1 -0
  32. package/dist/linters/openapi-redocly.js +69 -0
  33. package/dist/linters/openapi-redocly.js.map +1 -0
  34. package/dist/registry.d.ts +48 -0
  35. package/dist/registry.d.ts.map +1 -0
  36. package/dist/registry.js +178 -0
  37. package/dist/registry.js.map +1 -0
  38. package/dist/runner.d.ts +32 -0
  39. package/dist/runner.d.ts.map +1 -0
  40. package/dist/runner.js +321 -0
  41. package/dist/runner.js.map +1 -0
  42. package/dist/tsconfig.build.tsbuildinfo +1 -0
  43. package/package.json +68 -0
@@ -0,0 +1,747 @@
1
+ /**
2
+ * JSON Schema Linting Rules
3
+ *
4
+ * Native TypeScript implementation of linting rules for JSON Schema.
5
+ * Based on best practices from Sourcemeta, JSON Schema org, and community discussions.
6
+ *
7
+ * @see https://github.com/sourcemeta/jsonschema
8
+ * @see https://github.com/orgs/json-schema-org/discussions/323
9
+ */
10
+ import { isSchemaObject } from '@contractual/differs.json-schema';
11
+ /**
12
+ * Keywords that only apply to specific types
13
+ */
14
+ const TYPE_SPECIFIC_KEYWORDS = {
15
+ string: ['minLength', 'maxLength', 'pattern', 'format'],
16
+ number: ['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf'],
17
+ integer: ['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf'],
18
+ array: [
19
+ 'items',
20
+ 'additionalItems',
21
+ 'prefixItems',
22
+ 'contains',
23
+ 'minItems',
24
+ 'maxItems',
25
+ 'uniqueItems',
26
+ 'minContains',
27
+ 'maxContains',
28
+ ],
29
+ object: [
30
+ 'properties',
31
+ 'additionalProperties',
32
+ 'required',
33
+ 'minProperties',
34
+ 'maxProperties',
35
+ 'patternProperties',
36
+ 'propertyNames',
37
+ ],
38
+ boolean: [],
39
+ null: [],
40
+ };
41
+ /**
42
+ * Known JSON Schema format values
43
+ */
44
+ const KNOWN_FORMATS = new Set([
45
+ // Dates and times (RFC 3339)
46
+ 'date-time',
47
+ 'date',
48
+ 'time',
49
+ 'duration',
50
+ // Email (RFC 5321/5322)
51
+ 'email',
52
+ 'idn-email',
53
+ // Hostnames (RFC 1123/5891)
54
+ 'hostname',
55
+ 'idn-hostname',
56
+ // IP addresses (RFC 2673/4291)
57
+ 'ipv4',
58
+ 'ipv6',
59
+ // URIs (RFC 3986/3987)
60
+ 'uri',
61
+ 'uri-reference',
62
+ 'iri',
63
+ 'iri-reference',
64
+ 'uri-template',
65
+ // JSON Pointer (RFC 6901)
66
+ 'json-pointer',
67
+ 'relative-json-pointer',
68
+ // Regex (ECMA 262)
69
+ 'regex',
70
+ // UUID (RFC 4122)
71
+ 'uuid',
72
+ ]);
73
+ /**
74
+ * All built-in linting rules
75
+ */
76
+ export const LINT_RULES = [
77
+ // ==========================================
78
+ // Schema Declaration Rules
79
+ // ==========================================
80
+ {
81
+ id: 'missing-schema',
82
+ description: 'Root schema should declare $schema',
83
+ severity: 'warning',
84
+ check: (schema, path, root) => {
85
+ // Only check at root
86
+ if (path !== '/' || schema !== root)
87
+ return [];
88
+ if (!schema.$schema) {
89
+ return [
90
+ {
91
+ path: '/',
92
+ message: 'No $schema declared. Consider adding "$schema": "https://json-schema.org/draft/2020-12/schema"',
93
+ rule: 'missing-schema',
94
+ severity: 'warning',
95
+ },
96
+ ];
97
+ }
98
+ return [];
99
+ },
100
+ },
101
+ {
102
+ id: 'schema-not-at-root',
103
+ description: '$schema should only appear at resource root (with $id) or document root',
104
+ severity: 'warning',
105
+ check: (schema, path, root) => {
106
+ if (path === '/' || schema === root)
107
+ return [];
108
+ if (schema.$schema && !schema.$id) {
109
+ return [
110
+ {
111
+ path,
112
+ message: '$schema without $id in non-root location. $schema should only appear at resource roots.',
113
+ rule: 'schema-not-at-root',
114
+ severity: 'warning',
115
+ },
116
+ ];
117
+ }
118
+ return [];
119
+ },
120
+ },
121
+ // ==========================================
122
+ // Metadata Rules
123
+ // ==========================================
124
+ {
125
+ id: 'missing-title',
126
+ description: 'Root schema should have a title',
127
+ severity: 'warning',
128
+ check: (schema, path, root) => {
129
+ if (path !== '/' || schema !== root)
130
+ return [];
131
+ if (!schema.title) {
132
+ return [
133
+ {
134
+ path: '/',
135
+ message: 'Root schema has no title. Consider adding one for documentation.',
136
+ rule: 'missing-title',
137
+ severity: 'warning',
138
+ },
139
+ ];
140
+ }
141
+ return [];
142
+ },
143
+ },
144
+ {
145
+ id: 'missing-description',
146
+ description: 'Root schema should have a description',
147
+ severity: 'warning',
148
+ check: (schema, path, root) => {
149
+ if (path !== '/' || schema !== root)
150
+ return [];
151
+ if (!schema.description) {
152
+ return [
153
+ {
154
+ path: '/',
155
+ message: 'Root schema has no description. Consider adding one for documentation.',
156
+ rule: 'missing-description',
157
+ severity: 'warning',
158
+ },
159
+ ];
160
+ }
161
+ return [];
162
+ },
163
+ },
164
+ // ==========================================
165
+ // Enum/Const Rules
166
+ // ==========================================
167
+ {
168
+ id: 'enum-to-const',
169
+ description: 'An enum with a single value should use const instead',
170
+ severity: 'warning',
171
+ check: (schema, path) => {
172
+ if (schema.enum && Array.isArray(schema.enum) && schema.enum.length === 1) {
173
+ return [
174
+ {
175
+ path,
176
+ message: `An 'enum' with a single value can be expressed as 'const'. Use: "const": ${JSON.stringify(schema.enum[0])}`,
177
+ rule: 'enum-to-const',
178
+ severity: 'warning',
179
+ },
180
+ ];
181
+ }
182
+ return [];
183
+ },
184
+ },
185
+ {
186
+ id: 'enum-with-type',
187
+ description: 'Using type with enum is redundant when enum values are all the same type',
188
+ severity: 'warning',
189
+ check: (schema, path) => {
190
+ if (!schema.enum || !schema.type)
191
+ return [];
192
+ const enumValues = schema.enum;
193
+ if (!Array.isArray(enumValues) || enumValues.length === 0)
194
+ return [];
195
+ // Determine types of all enum values
196
+ const enumTypes = new Set(enumValues.map((v) => {
197
+ if (v === null)
198
+ return 'null';
199
+ if (Array.isArray(v))
200
+ return 'array';
201
+ return typeof v;
202
+ }));
203
+ // Map JS types to JSON Schema types
204
+ const jsToSchema = {
205
+ string: 'string',
206
+ number: 'number',
207
+ boolean: 'boolean',
208
+ object: 'object',
209
+ null: 'null',
210
+ array: 'array',
211
+ };
212
+ const schemaTypes = new Set([...enumTypes].map((t) => jsToSchema[t] || t));
213
+ const declaredType = new Set(Array.isArray(schema.type) ? schema.type : [schema.type]);
214
+ // Check if type declaration matches enum value types exactly
215
+ const typesMatch = schemaTypes.size === declaredType.size &&
216
+ [...schemaTypes].every((t) => declaredType.has(t));
217
+ if (typesMatch) {
218
+ return [
219
+ {
220
+ path,
221
+ message: "'type' is redundant when 'enum' values already constrain the type",
222
+ rule: 'enum-with-type',
223
+ severity: 'warning',
224
+ },
225
+ ];
226
+ }
227
+ return [];
228
+ },
229
+ },
230
+ {
231
+ id: 'const-with-type',
232
+ description: 'Using type with const is redundant when const value determines the type',
233
+ severity: 'warning',
234
+ check: (schema, path) => {
235
+ if (schema.const === undefined || !schema.type)
236
+ return [];
237
+ const constValue = schema.const;
238
+ let constType;
239
+ if (constValue === null) {
240
+ constType = 'null';
241
+ }
242
+ else if (Array.isArray(constValue)) {
243
+ constType = 'array';
244
+ }
245
+ else {
246
+ constType = typeof constValue;
247
+ }
248
+ // Map JS type to JSON Schema type
249
+ const jsToSchema = {
250
+ string: 'string',
251
+ number: 'number',
252
+ boolean: 'boolean',
253
+ object: 'object',
254
+ null: 'null',
255
+ array: 'array',
256
+ };
257
+ const schemaConstType = jsToSchema[constType] || constType;
258
+ const declaredType = Array.isArray(schema.type) ? schema.type : [schema.type];
259
+ if (declaredType.length === 1 && declaredType[0] === schemaConstType) {
260
+ return [
261
+ {
262
+ path,
263
+ message: "'type' is redundant when 'const' value already determines the type",
264
+ rule: 'const-with-type',
265
+ severity: 'warning',
266
+ },
267
+ ];
268
+ }
269
+ return [];
270
+ },
271
+ },
272
+ // ==========================================
273
+ // Conditional Schema Rules
274
+ // ==========================================
275
+ {
276
+ id: 'if-without-then-else',
277
+ description: 'if without then or else is unnecessary',
278
+ severity: 'warning',
279
+ check: (schema, path) => {
280
+ if (schema.if && !schema.then && !schema.else) {
281
+ return [
282
+ {
283
+ path,
284
+ message: "'if' without 'then' or 'else' has no effect and can be removed",
285
+ rule: 'if-without-then-else',
286
+ severity: 'warning',
287
+ },
288
+ ];
289
+ }
290
+ return [];
291
+ },
292
+ },
293
+ {
294
+ id: 'then-else-without-if',
295
+ description: 'then or else without if is unnecessary',
296
+ severity: 'warning',
297
+ check: (schema, path) => {
298
+ const issues = [];
299
+ if (schema.then && !schema.if) {
300
+ issues.push({
301
+ path,
302
+ message: "'then' without 'if' has no effect and can be removed",
303
+ rule: 'then-else-without-if',
304
+ severity: 'warning',
305
+ });
306
+ }
307
+ if (schema.else && !schema.if) {
308
+ issues.push({
309
+ path,
310
+ message: "'else' without 'if' has no effect and can be removed",
311
+ rule: 'then-else-without-if',
312
+ severity: 'warning',
313
+ });
314
+ }
315
+ return issues;
316
+ },
317
+ },
318
+ // ==========================================
319
+ // Array Constraint Rules
320
+ // ==========================================
321
+ {
322
+ id: 'additional-items-redundant',
323
+ description: 'additionalItems is ignored when items is a schema (not tuple)',
324
+ severity: 'warning',
325
+ check: (schema, path) => {
326
+ // additionalItems only matters when items is an array (tuple validation)
327
+ // When items is a schema, additionalItems is ignored
328
+ if (schema.additionalItems !== undefined && schema.items && !Array.isArray(schema.items)) {
329
+ return [
330
+ {
331
+ path,
332
+ message: "'additionalItems' is ignored when 'items' is a schema (not a tuple). Remove 'additionalItems' or use tuple validation.",
333
+ rule: 'additional-items-redundant',
334
+ severity: 'warning',
335
+ },
336
+ ];
337
+ }
338
+ return [];
339
+ },
340
+ },
341
+ {
342
+ id: 'contains-required',
343
+ description: 'minContains or maxContains without contains is unnecessary',
344
+ severity: 'warning',
345
+ check: (schema, path) => {
346
+ const issues = [];
347
+ if (schema.minContains !== undefined && !schema.contains) {
348
+ issues.push({
349
+ path,
350
+ message: "'minContains' without 'contains' has no effect",
351
+ rule: 'contains-required',
352
+ severity: 'warning',
353
+ });
354
+ }
355
+ if (schema.maxContains !== undefined && !schema.contains) {
356
+ issues.push({
357
+ path,
358
+ message: "'maxContains' without 'contains' has no effect",
359
+ rule: 'contains-required',
360
+ severity: 'warning',
361
+ });
362
+ }
363
+ return issues;
364
+ },
365
+ },
366
+ // ==========================================
367
+ // Type Compatibility Rules
368
+ // ==========================================
369
+ {
370
+ id: 'type-incompatible-keywords',
371
+ description: 'Validation keywords that do not apply to the declared type',
372
+ severity: 'warning',
373
+ check: (schema, path) => {
374
+ if (!schema.type || Array.isArray(schema.type))
375
+ return [];
376
+ const declaredType = schema.type;
377
+ const applicableKeywords = TYPE_SPECIFIC_KEYWORDS[declaredType] || [];
378
+ const issues = [];
379
+ // Check for keywords that apply to other types
380
+ for (const [type, keywords] of Object.entries(TYPE_SPECIFIC_KEYWORDS)) {
381
+ if (type === declaredType)
382
+ continue;
383
+ for (const keyword of keywords) {
384
+ if (schema[keyword] !== undefined && !applicableKeywords.includes(keyword)) {
385
+ issues.push({
386
+ path,
387
+ message: `'${keyword}' applies to type '${type}' but schema declares type '${declaredType}'`,
388
+ rule: 'type-incompatible-keywords',
389
+ severity: 'warning',
390
+ });
391
+ }
392
+ }
393
+ }
394
+ return issues;
395
+ },
396
+ },
397
+ // ==========================================
398
+ // Range Validation Rules
399
+ // ==========================================
400
+ {
401
+ id: 'invalid-numeric-range',
402
+ description: 'maximum should be greater than or equal to minimum',
403
+ severity: 'error',
404
+ check: (schema, path) => {
405
+ const issues = [];
406
+ if (typeof schema.minimum === 'number' && typeof schema.maximum === 'number') {
407
+ if (schema.maximum < schema.minimum) {
408
+ issues.push({
409
+ path,
410
+ message: `'maximum' (${schema.maximum}) is less than 'minimum' (${schema.minimum})`,
411
+ rule: 'invalid-numeric-range',
412
+ severity: 'error',
413
+ });
414
+ }
415
+ }
416
+ // Handle exclusive bounds (draft-06+ where they are numbers)
417
+ if (typeof schema.exclusiveMinimum === 'number' &&
418
+ typeof schema.exclusiveMaximum === 'number') {
419
+ if (schema.exclusiveMaximum <= schema.exclusiveMinimum) {
420
+ issues.push({
421
+ path,
422
+ message: `'exclusiveMaximum' (${schema.exclusiveMaximum}) is not greater than 'exclusiveMinimum' (${schema.exclusiveMinimum})`,
423
+ rule: 'invalid-numeric-range',
424
+ severity: 'error',
425
+ });
426
+ }
427
+ }
428
+ return issues;
429
+ },
430
+ },
431
+ {
432
+ id: 'invalid-length-range',
433
+ description: 'maxLength should be greater than or equal to minLength',
434
+ severity: 'error',
435
+ check: (schema, path) => {
436
+ if (typeof schema.minLength === 'number' && typeof schema.maxLength === 'number') {
437
+ if (schema.maxLength < schema.minLength) {
438
+ return [
439
+ {
440
+ path,
441
+ message: `'maxLength' (${schema.maxLength}) is less than 'minLength' (${schema.minLength})`,
442
+ rule: 'invalid-length-range',
443
+ severity: 'error',
444
+ },
445
+ ];
446
+ }
447
+ }
448
+ return [];
449
+ },
450
+ },
451
+ {
452
+ id: 'invalid-items-range',
453
+ description: 'maxItems should be greater than or equal to minItems',
454
+ severity: 'error',
455
+ check: (schema, path) => {
456
+ if (typeof schema.minItems === 'number' && typeof schema.maxItems === 'number') {
457
+ if (schema.maxItems < schema.minItems) {
458
+ return [
459
+ {
460
+ path,
461
+ message: `'maxItems' (${schema.maxItems}) is less than 'minItems' (${schema.minItems})`,
462
+ rule: 'invalid-items-range',
463
+ severity: 'error',
464
+ },
465
+ ];
466
+ }
467
+ }
468
+ return [];
469
+ },
470
+ },
471
+ {
472
+ id: 'invalid-properties-range',
473
+ description: 'maxProperties should be greater than or equal to minProperties',
474
+ severity: 'error',
475
+ check: (schema, path) => {
476
+ if (typeof schema.minProperties === 'number' && typeof schema.maxProperties === 'number') {
477
+ if (schema.maxProperties < schema.minProperties) {
478
+ return [
479
+ {
480
+ path,
481
+ message: `'maxProperties' (${schema.maxProperties}) is less than 'minProperties' (${schema.minProperties})`,
482
+ rule: 'invalid-properties-range',
483
+ severity: 'error',
484
+ },
485
+ ];
486
+ }
487
+ }
488
+ return [];
489
+ },
490
+ },
491
+ // ==========================================
492
+ // Format Rules
493
+ // ==========================================
494
+ {
495
+ id: 'unknown-format',
496
+ description: 'Unknown format value may not be validated',
497
+ severity: 'warning',
498
+ check: (schema, path) => {
499
+ if (typeof schema.format === 'string' && !KNOWN_FORMATS.has(schema.format)) {
500
+ return [
501
+ {
502
+ path,
503
+ message: `Unknown format '${schema.format}'. Custom formats may not be validated by all implementations.`,
504
+ rule: 'unknown-format',
505
+ severity: 'warning',
506
+ },
507
+ ];
508
+ }
509
+ return [];
510
+ },
511
+ },
512
+ // ==========================================
513
+ // Empty Schema Rules
514
+ // ==========================================
515
+ {
516
+ id: 'empty-enum',
517
+ description: 'Empty enum matches nothing and is likely an error',
518
+ severity: 'error',
519
+ check: (schema, path) => {
520
+ if (schema.enum && Array.isArray(schema.enum) && schema.enum.length === 0) {
521
+ return [
522
+ {
523
+ path,
524
+ message: "Empty 'enum' array will never validate any value",
525
+ rule: 'empty-enum',
526
+ severity: 'error',
527
+ },
528
+ ];
529
+ }
530
+ return [];
531
+ },
532
+ },
533
+ {
534
+ id: 'empty-required',
535
+ description: 'Empty required array is unnecessary',
536
+ severity: 'warning',
537
+ check: (schema, path) => {
538
+ if (schema.required && Array.isArray(schema.required) && schema.required.length === 0) {
539
+ return [
540
+ {
541
+ path,
542
+ message: "Empty 'required' array can be removed",
543
+ rule: 'empty-required',
544
+ severity: 'warning',
545
+ },
546
+ ];
547
+ }
548
+ return [];
549
+ },
550
+ },
551
+ {
552
+ id: 'empty-allof-anyof-oneof',
553
+ description: 'Empty allOf/anyOf/oneOf is likely an error',
554
+ severity: 'error',
555
+ check: (schema, path) => {
556
+ const issues = [];
557
+ for (const keyword of ['allOf', 'anyOf', 'oneOf']) {
558
+ const value = schema[keyword];
559
+ if (value && Array.isArray(value) && value.length === 0) {
560
+ issues.push({
561
+ path,
562
+ message: `Empty '${keyword}' array ${keyword === 'anyOf' || keyword === 'oneOf' ? 'will never validate' : 'is redundant'}`,
563
+ rule: 'empty-allof-anyof-oneof',
564
+ severity: keyword === 'allOf' ? 'warning' : 'error',
565
+ });
566
+ }
567
+ }
568
+ return issues;
569
+ },
570
+ },
571
+ // ==========================================
572
+ // Required Properties Rules
573
+ // ==========================================
574
+ {
575
+ id: 'required-undefined-property',
576
+ description: 'Required property is not defined in properties',
577
+ severity: 'warning',
578
+ check: (schema, path) => {
579
+ if (!schema.required || !schema.properties)
580
+ return [];
581
+ const issues = [];
582
+ const definedProps = new Set(Object.keys(schema.properties));
583
+ for (const requiredProp of schema.required) {
584
+ if (!definedProps.has(requiredProp)) {
585
+ issues.push({
586
+ path,
587
+ message: `Required property '${requiredProp}' is not defined in 'properties'`,
588
+ rule: 'required-undefined-property',
589
+ severity: 'warning',
590
+ });
591
+ }
592
+ }
593
+ return issues;
594
+ },
595
+ },
596
+ {
597
+ id: 'duplicate-required',
598
+ description: 'Duplicate entries in required array',
599
+ severity: 'warning',
600
+ check: (schema, path) => {
601
+ if (!schema.required || !Array.isArray(schema.required))
602
+ return [];
603
+ const seen = new Set();
604
+ const duplicates = new Set();
605
+ for (const prop of schema.required) {
606
+ if (seen.has(prop)) {
607
+ duplicates.add(prop);
608
+ }
609
+ seen.add(prop);
610
+ }
611
+ if (duplicates.size > 0) {
612
+ return [
613
+ {
614
+ path,
615
+ message: `Duplicate entries in 'required': ${[...duplicates].join(', ')}`,
616
+ rule: 'duplicate-required',
617
+ severity: 'warning',
618
+ },
619
+ ];
620
+ }
621
+ return [];
622
+ },
623
+ },
624
+ ];
625
+ /**
626
+ * Run all lint rules against a schema, recursively walking all subschemas
627
+ */
628
+ export function runLintRules(schema, enabledRules, disabledRules) {
629
+ const issues = [];
630
+ const activeRules = LINT_RULES.filter((rule) => {
631
+ if (disabledRules?.has(rule.id))
632
+ return false;
633
+ if (enabledRules && !enabledRules.has(rule.id))
634
+ return false;
635
+ return true;
636
+ });
637
+ walkSchema(schema, '/', schema, (node, path, root) => {
638
+ for (const rule of activeRules) {
639
+ const ruleIssues = rule.check(node, path, root);
640
+ issues.push(...ruleIssues);
641
+ }
642
+ });
643
+ return issues;
644
+ }
645
+ /**
646
+ * Walk all subschemas in a JSON Schema document
647
+ */
648
+ function walkSchema(schema, path, root, visitor) {
649
+ if (!isSchemaObject(schema))
650
+ return;
651
+ visitor(schema, path, root);
652
+ // Properties
653
+ if (schema.properties) {
654
+ for (const [key, value] of Object.entries(schema.properties)) {
655
+ if (isSchemaObject(value)) {
656
+ walkSchema(value, `${path}/properties/${key}`, root, visitor);
657
+ }
658
+ }
659
+ }
660
+ // Additional properties
661
+ if (isSchemaObject(schema.additionalProperties)) {
662
+ walkSchema(schema.additionalProperties, `${path}/additionalProperties`, root, visitor);
663
+ }
664
+ // Pattern properties
665
+ if (schema.patternProperties) {
666
+ for (const [pattern, value] of Object.entries(schema.patternProperties)) {
667
+ if (isSchemaObject(value)) {
668
+ walkSchema(value, `${path}/patternProperties/${encodeURIComponent(pattern)}`, root, visitor);
669
+ }
670
+ }
671
+ }
672
+ // Property names
673
+ if (isSchemaObject(schema.propertyNames)) {
674
+ walkSchema(schema.propertyNames, `${path}/propertyNames`, root, visitor);
675
+ }
676
+ // Items (array or single schema)
677
+ if (schema.items) {
678
+ if (Array.isArray(schema.items)) {
679
+ schema.items.forEach((item, i) => {
680
+ if (isSchemaObject(item)) {
681
+ walkSchema(item, `${path}/items/${i}`, root, visitor);
682
+ }
683
+ });
684
+ }
685
+ else if (isSchemaObject(schema.items)) {
686
+ walkSchema(schema.items, `${path}/items`, root, visitor);
687
+ }
688
+ }
689
+ // Prefix items (2020-12)
690
+ if (schema.prefixItems && Array.isArray(schema.prefixItems)) {
691
+ schema.prefixItems.forEach((item, i) => {
692
+ if (isSchemaObject(item)) {
693
+ walkSchema(item, `${path}/prefixItems/${i}`, root, visitor);
694
+ }
695
+ });
696
+ }
697
+ // Contains
698
+ if (isSchemaObject(schema.contains)) {
699
+ walkSchema(schema.contains, `${path}/contains`, root, visitor);
700
+ }
701
+ // Conditional
702
+ if (isSchemaObject(schema.if)) {
703
+ walkSchema(schema.if, `${path}/if`, root, visitor);
704
+ }
705
+ if (isSchemaObject(schema.then)) {
706
+ walkSchema(schema.then, `${path}/then`, root, visitor);
707
+ }
708
+ if (isSchemaObject(schema.else)) {
709
+ walkSchema(schema.else, `${path}/else`, root, visitor);
710
+ }
711
+ // Composition
712
+ for (const keyword of ['allOf', 'anyOf', 'oneOf']) {
713
+ const arr = schema[keyword];
714
+ if (arr && Array.isArray(arr)) {
715
+ arr.forEach((item, i) => {
716
+ if (isSchemaObject(item)) {
717
+ walkSchema(item, `${path}/${keyword}/${i}`, root, visitor);
718
+ }
719
+ });
720
+ }
721
+ }
722
+ // Not
723
+ if (isSchemaObject(schema.not)) {
724
+ walkSchema(schema.not, `${path}/not`, root, visitor);
725
+ }
726
+ // Definitions ($defs)
727
+ if (schema.$defs) {
728
+ for (const [key, value] of Object.entries(schema.$defs)) {
729
+ if (isSchemaObject(value)) {
730
+ walkSchema(value, `${path}/$defs/${key}`, root, visitor);
731
+ }
732
+ }
733
+ }
734
+ }
735
+ /**
736
+ * Get all available rule IDs
737
+ */
738
+ export function getAvailableRules() {
739
+ return LINT_RULES.map((r) => r.id);
740
+ }
741
+ /**
742
+ * Get rule description by ID
743
+ */
744
+ export function getRuleDescription(ruleId) {
745
+ return LINT_RULES.find((r) => r.id === ruleId)?.description;
746
+ }
747
+ //# sourceMappingURL=json-schema-rules.js.map