json-schema-compatibility-checker 1.0.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 (53) hide show
  1. package/README.md +2021 -0
  2. package/dist/chunk-2vpph7cy.js +5 -0
  3. package/dist/chunk-2vpph7cy.js.map +10 -0
  4. package/dist/chunk-cspd2zw4.js +6 -0
  5. package/dist/chunk-cspd2zw4.js.map +10 -0
  6. package/dist/chunk-e4501gq7.js +5 -0
  7. package/dist/chunk-e4501gq7.js.map +10 -0
  8. package/dist/chunk-g0pfcnm5.js +5 -0
  9. package/dist/chunk-g0pfcnm5.js.map +10 -0
  10. package/dist/chunk-h080ggvf.js +5 -0
  11. package/dist/chunk-h080ggvf.js.map +10 -0
  12. package/dist/chunk-pw49kj6f.js +5 -0
  13. package/dist/chunk-pw49kj6f.js.map +10 -0
  14. package/dist/chunk-v5tqyc67.js +5 -0
  15. package/dist/chunk-v5tqyc67.js.map +10 -0
  16. package/dist/chunk-vcwsxmk4.js +5 -0
  17. package/dist/chunk-vcwsxmk4.js.map +10 -0
  18. package/dist/chunk-w7qcey06.js +5 -0
  19. package/dist/chunk-w7qcey06.js.map +10 -0
  20. package/dist/condition-resolver.d.ts +26 -0
  21. package/dist/condition-resolver.js +4 -0
  22. package/dist/condition-resolver.js.map +9 -0
  23. package/dist/differ.d.ts +15 -0
  24. package/dist/differ.js +4 -0
  25. package/dist/differ.js.map +9 -0
  26. package/dist/format-validator.d.ts +78 -0
  27. package/dist/format-validator.js +4 -0
  28. package/dist/format-validator.js.map +9 -0
  29. package/dist/formatter.d.ts +22 -0
  30. package/dist/formatter.js +4 -0
  31. package/dist/formatter.js.map +9 -0
  32. package/dist/index.d.ts +4 -0
  33. package/dist/index.js +4 -0
  34. package/dist/index.js.map +9 -0
  35. package/dist/json-schema-compatibility-checker.d.ts +73 -0
  36. package/dist/json-schema-compatibility-checker.js +4 -0
  37. package/dist/json-schema-compatibility-checker.js.map +9 -0
  38. package/dist/merge-engine.d.ts +30 -0
  39. package/dist/merge-engine.js +4 -0
  40. package/dist/merge-engine.js.map +9 -0
  41. package/dist/normalizer.d.ts +18 -0
  42. package/dist/normalizer.js +4 -0
  43. package/dist/normalizer.js.map +9 -0
  44. package/dist/pattern-subset.d.ts +55 -0
  45. package/dist/pattern-subset.js +4 -0
  46. package/dist/pattern-subset.js.map +9 -0
  47. package/dist/subset-checker.d.ts +76 -0
  48. package/dist/subset-checker.js +4 -0
  49. package/dist/subset-checker.js.map +9 -0
  50. package/dist/types.d.ts +31 -0
  51. package/dist/types.js +3 -0
  52. package/dist/types.js.map +9 -0
  53. package/package.json +50 -0
package/README.md ADDED
@@ -0,0 +1,2021 @@
1
+ # JSON Schema Compatibility Checker
2
+
3
+ > Vérifiez la compatibilité structurelle entre JSON Schemas (Draft-07) grâce à une approche mathématique par intersection ensembliste.
4
+
5
+ ---
6
+
7
+ ## Sommaire
8
+
9
+ - [Introduction](#introduction)
10
+ - [Principe mathématique](#principe-mathématique)
11
+ - [Installation](#installation)
12
+ - [Démarrage rapide](#démarrage-rapide)
13
+ - [API Reference](#api-reference)
14
+ - [`isSubset(sub, sup)`](#issubsetsub-sup)
15
+ - [`check(sub, sup)`](#checksub-sup)
16
+ - [`isEqual(a, b)`](#isequala-b)
17
+ - [`intersect(a, b)`](#intersecta-b)
18
+ - [`canConnect(sourceOutput, targetInput)`](#canconnectsourceoutput-targetinput)
19
+ - [`resolveConditions(schema, data)`](#resolveconditionsschema-data)
20
+ - [`checkResolved(sub, sup, subData, supData?)`](#checkresolvedsub-sup-subdata-supdata)
21
+ - [`normalize(schema)`](#normalizeschema)
22
+ - [`formatResult(label, result)`](#formatresultlabel-result)
23
+ - [Guide des fonctionnalités](#guide-des-fonctionnalités)
24
+ - [1. Compatibilité de types](#1-compatibilité-de-types)
25
+ - [2. Champs requis (`required`)](#2-champs-requis-required)
26
+ - [3. Contraintes numériques](#3-contraintes-numériques)
27
+ - [4. Contraintes de chaînes](#4-contraintes-de-chaînes)
28
+ - [5. `enum` et `const`](#5-enum-et-const)
29
+ - [6. Contraintes de tableaux](#6-contraintes-de-tableaux)
30
+ - [7. `additionalProperties`](#7-additionalproperties)
31
+ - [8. Objets imbriqués](#8-objets-imbriqués)
32
+ - [9. `anyOf` / `oneOf`](#9-anyof--oneof)
33
+ - [10. Négation (`not`)](#10-négation-not)
34
+ - [11. Formats (`format`)](#11-formats-format)
35
+ - [12. Patterns regex (`pattern`)](#12-patterns-regex-pattern)
36
+ - [13. Conditions `if` / `then` / `else`](#13-conditions-if--then--else)
37
+ - [14. `allOf` avec conditions](#14-allof-avec-conditions)
38
+ - [Fonctions utilitaires](#fonctions-utilitaires)
39
+ - [`isPatternSubset(sub, sup)`](#ispatternsubsetsub-sup)
40
+ - [`arePatternsEquivalent(a, b)`](#arepatternsEquivalenta-b)
41
+ - [`isTrivialPattern(pattern)`](#istrivialpatternpattern)
42
+ - [Cas d'usage concrets](#cas-dusage-concrets)
43
+ - [Connexion de nœuds dans un orchestrateur](#connexion-de-nœuds-dans-un-orchestrateur)
44
+ - [Validation de réponse API](#validation-de-réponse-api)
45
+ - [Union discriminée](#union-discriminée)
46
+ - [Formulaire conditionnel](#formulaire-conditionnel)
47
+ - [Types exportés](#types-exportés)
48
+ - [Limitations connues](#limitations-connues)
49
+ - [Architecture interne](#architecture-interne)
50
+
51
+ ---
52
+
53
+ ## Introduction
54
+
55
+ **JSON Schema Compatibility Checker** est une librairie TypeScript qui permet de vérifier la compatibilité structurelle entre deux JSON Schemas au format Draft-07.
56
+
57
+ ### Pourquoi cette librairie ?
58
+
59
+ Dans les systèmes de type workflow, orchestration de nœuds, ou intégration d'API, une question revient constamment :
60
+
61
+ > *"Est-ce que la sortie du composant A est compatible avec l'entrée du composant B ?"*
62
+
63
+ Autrement dit : **toute donnée produite par A sera-t-elle acceptée par B ?**
64
+
65
+ Cette librairie répond à cette question en vérifiant si un schema est un **sous-ensemble** d'un autre, avec un diagnostic structurel détaillé en cas d'incompatibilité.
66
+
67
+ ### Ce que fait la librairie
68
+
69
+ - ✅ Vérifie si un schema est un sous-ensemble d'un autre (`sub ⊆ sup`)
70
+ - ✅ Produit un diagnostic détaillé avec les différences structurelles
71
+ - ✅ Calcule l'intersection de deux schemas
72
+ - ✅ Résout les conditions `if/then/else` avec des données discriminantes
73
+ - ✅ Gère `anyOf`, `oneOf`, `not`, `format`, `pattern`, `dependencies`, etc.
74
+ - ✅ Compare des patterns regex par échantillonnage
75
+ - ✅ Fournit un formatage lisible des résultats pour le debug
76
+
77
+ ---
78
+
79
+ ## Principe mathématique
80
+
81
+ Le cœur de la librairie repose sur un principe ensembliste simple :
82
+
83
+ ```
84
+ A ⊆ B ⟺ A ∩ B ≡ A
85
+ ```
86
+
87
+ **Un schema A est sous-ensemble de B si et seulement si l'intersection de A et B est structurellement identique à A.**
88
+
89
+ En JSON Schema, cela se traduit par :
90
+
91
+ | Concept mathématique | Traduction JSON Schema |
92
+ |---|---|
93
+ | `A ∩ B` | `allOf([A, B])` résolu via merge |
94
+ | `≡` (équivalence) | Comparaison structurelle profonde |
95
+
96
+ Si après le merge (intersection), le résultat est identique au schema original `A`, alors `A` n'a pas été "restreint" par `B` — ce qui signifie que `A` est déjà inclus dans `B`.
97
+
98
+ Si le merge produit un résultat différent de `A`, les différences structurelles constituent le **diagnostic** de l'incompatibilité.
99
+
100
+ ---
101
+
102
+ ## Installation
103
+
104
+ ```bash
105
+ bun add json-schema-compatibility-checker
106
+ ```
107
+
108
+ > **Prérequis** : TypeScript ≥ 5, runtime compatible ESM (Bun, Node 18+).
109
+
110
+ ---
111
+
112
+ ## Démarrage rapide
113
+
114
+ L'exemple le plus simple : vérifier si un schema strict est compatible avec un schema plus permissif.
115
+
116
+ ```ts
117
+ import { JsonSchemaCompatibilityChecker } from "json-schema-compatibility-checker";
118
+
119
+ const checker = new JsonSchemaCompatibilityChecker();
120
+
121
+ // Schema strict : exige name ET age
122
+ const strict = {
123
+ type: "object",
124
+ properties: {
125
+ name: { type: "string" },
126
+ age: { type: "number" },
127
+ },
128
+ required: ["name", "age"],
129
+ };
130
+
131
+ // Schema permissif : exige seulement name
132
+ const loose = {
133
+ type: "object",
134
+ properties: {
135
+ name: { type: "string" },
136
+ },
137
+ required: ["name"],
138
+ };
139
+
140
+ // Un objet valide pour strict est-il toujours valide pour loose ?
141
+ console.log(checker.isSubset(strict, loose)); // true ✅
142
+
143
+ // L'inverse est-il vrai ?
144
+ console.log(checker.isSubset(loose, strict)); // false ❌
145
+ // → Un objet { name: "Alice" } (sans age) est valide pour loose mais pas pour strict
146
+ ```
147
+
148
+ ---
149
+
150
+ ## API Reference
151
+
152
+ Toutes les méthodes sont exposées par la classe `JsonSchemaCompatibilityChecker`.
153
+
154
+ ```ts
155
+ import { JsonSchemaCompatibilityChecker } from "json-schema-compatibility-checker";
156
+
157
+ const checker = new JsonSchemaCompatibilityChecker();
158
+ ```
159
+
160
+ ---
161
+
162
+ ### `isSubset(sub, sup)`
163
+
164
+ ```ts
165
+ isSubset(sub: JSONSchema7Definition, sup: JSONSchema7Definition): boolean
166
+ ```
167
+
168
+ Vérifie si `sub ⊆ sup` — c'est-à-dire si **toute valeur valide pour `sub` est aussi valide pour `sup`**.
169
+
170
+ Retourne un simple `boolean`.
171
+
172
+ ```ts
173
+ // integer est un sous-ensemble de number
174
+ checker.isSubset({ type: "integer" }, { type: "number" });
175
+ // → true
176
+
177
+ // number n'est PAS un sous-ensemble de integer
178
+ checker.isSubset({ type: "number" }, { type: "integer" });
179
+ // → false
180
+
181
+ // string n'est PAS un sous-ensemble de number
182
+ checker.isSubset({ type: "string" }, { type: "number" });
183
+ // → false
184
+ ```
185
+
186
+ #### Schemas booléens
187
+
188
+ ```ts
189
+ // false (aucune valeur) est sous-ensemble de tout
190
+ checker.isSubset(false, true); // → true
191
+ checker.isSubset(false, { type: "string" }); // → true
192
+
193
+ // true (toutes les valeurs) n'est sous-ensemble de rien de spécifique
194
+ checker.isSubset(true, { type: "string" }); // → false
195
+
196
+ // Tout schema est sous-ensemble de true
197
+ checker.isSubset({ type: "string" }, true); // → true
198
+ ```
199
+
200
+ ---
201
+
202
+ ### `check(sub, sup)`
203
+
204
+ ```ts
205
+ check(sub: JSONSchema7Definition, sup: JSONSchema7Definition): SubsetResult
206
+ ```
207
+
208
+ Comme `isSubset`, mais retourne un **résultat détaillé** avec les différences structurelles.
209
+
210
+ ```ts
211
+ interface SubsetResult {
212
+ isSubset: boolean;
213
+ merged: JSONSchema7Definition | null; // Résultat de l'intersection
214
+ diffs: SchemaDiff[]; // Différences structurelles
215
+ }
216
+
217
+ interface SchemaDiff {
218
+ path: string; // Chemin JSON-path vers la divergence
219
+ type: "added" | "removed" | "changed";
220
+ expected: unknown; // Valeur dans le schema original (sub)
221
+ actual: unknown; // Valeur dans le schema mergé
222
+ }
223
+ ```
224
+
225
+ #### Exemple — Check compatible
226
+
227
+ ```ts
228
+ const result = checker.check(
229
+ { type: "string", minLength: 5 },
230
+ { type: "string" }
231
+ );
232
+
233
+ console.log(result.isSubset); // true
234
+ console.log(result.diffs); // [] (aucune différence)
235
+ console.log(result.merged); // { type: "string", minLength: 5 }
236
+ ```
237
+
238
+ #### Exemple — Check incompatible avec diagnostic
239
+
240
+ ```ts
241
+ const sub = {
242
+ type: "object",
243
+ properties: { name: { type: "string" } },
244
+ required: ["name"],
245
+ };
246
+
247
+ const sup = {
248
+ type: "object",
249
+ properties: {
250
+ name: { type: "string" },
251
+ age: { type: "number" },
252
+ },
253
+ required: ["name", "age"],
254
+ };
255
+
256
+ const result = checker.check(sub, sup);
257
+
258
+ console.log(result.isSubset); // false
259
+ console.log(result.diffs);
260
+ // [
261
+ // { path: "required", type: "changed", expected: ["name"], actual: ["name", "age"] },
262
+ // { path: "properties.age", type: "added", expected: undefined, actual: { type: "number" } }
263
+ // ]
264
+ ```
265
+
266
+ #### Exemple — Types incompatibles
267
+
268
+ ```ts
269
+ const result = checker.check({ type: "string" }, { type: "number" });
270
+
271
+ console.log(result.isSubset); // false
272
+ console.log(result.merged); // null (intersection impossible)
273
+ console.log(result.diffs); // [{ path: "$", type: "changed", expected: ..., actual: "Incompatible..." }]
274
+ ```
275
+
276
+ ---
277
+
278
+ ### `isEqual(a, b)`
279
+
280
+ ```ts
281
+ isEqual(a: JSONSchema7Definition, b: JSONSchema7Definition): boolean
282
+ ```
283
+
284
+ Vérifie l'**égalité structurelle** entre deux schemas après normalisation.
285
+
286
+ ```ts
287
+ checker.isEqual(
288
+ { type: "string", minLength: 1 },
289
+ { type: "string", minLength: 1 }
290
+ );
291
+ // → true
292
+
293
+ checker.isEqual(
294
+ { type: "string" },
295
+ { type: "number" }
296
+ );
297
+ // → false
298
+ ```
299
+
300
+ ---
301
+
302
+ ### `intersect(a, b)`
303
+
304
+ ```ts
305
+ intersect(
306
+ a: JSONSchema7Definition,
307
+ b: JSONSchema7Definition
308
+ ): JSONSchema7Definition | null
309
+ ```
310
+
311
+ Calcule l'**intersection** de deux schemas (merge `allOf`). Retourne `null` si les schemas sont incompatibles.
312
+
313
+ #### Exemple — Intersection de contraintes numériques
314
+
315
+ ```ts
316
+ const result = checker.intersect(
317
+ { type: "number", minimum: 5, maximum: 10 },
318
+ { type: "number", minimum: 0, maximum: 100 }
319
+ );
320
+ // → { type: "number", minimum: 5, maximum: 10 }
321
+ // L'intersection conserve les contraintes les plus restrictives
322
+ ```
323
+
324
+ #### Exemple — Intersection de propriétés d'objets
325
+
326
+ ```ts
327
+ const result = checker.intersect(
328
+ {
329
+ type: "object",
330
+ properties: { a: { type: "string" } },
331
+ required: ["a"],
332
+ },
333
+ {
334
+ type: "object",
335
+ properties: { b: { type: "number" } },
336
+ required: ["b"],
337
+ }
338
+ );
339
+ // → {
340
+ // type: "object",
341
+ // properties: { a: { type: "string" }, b: { type: "number" } },
342
+ // required: ["a", "b"]
343
+ // }
344
+ ```
345
+
346
+ #### Exemple — Intersection d'enums
347
+
348
+ ```ts
349
+ const result = checker.intersect(
350
+ { type: "string", enum: ["a", "b", "c"] },
351
+ { type: "string", enum: ["b", "c", "d"] }
352
+ );
353
+ // → { type: "string", enum: ["b", "c"] }
354
+ // Seules les valeurs communes sont conservées
355
+ ```
356
+
357
+ #### Exemple — Types incompatibles
358
+
359
+ ```ts
360
+ checker.intersect({ type: "string" }, { type: "number" });
361
+ // → null (aucune valeur ne peut être à la fois string ET number)
362
+ ```
363
+
364
+ ---
365
+
366
+ ### `canConnect(sourceOutput, targetInput)`
367
+
368
+ ```ts
369
+ canConnect(
370
+ sourceOutput: JSONSchema7Definition,
371
+ targetInput: JSONSchema7Definition
372
+ ): ConnectionResult
373
+ ```
374
+
375
+ Vérifie si la **sortie d'un nœud source** peut alimenter l'**entrée d'un nœud cible**. Sémantiquement : `sourceOutput ⊆ targetInput`.
376
+
377
+ ```ts
378
+ interface ConnectionResult extends SubsetResult {
379
+ direction: string; // "sourceOutput ⊆ targetInput"
380
+ }
381
+ ```
382
+
383
+ ```ts
384
+ const nodeAOutput = {
385
+ type: "object",
386
+ properties: {
387
+ id: { type: "string" },
388
+ total: { type: "number", minimum: 0 },
389
+ customer: {
390
+ type: "object",
391
+ properties: {
392
+ email: { type: "string", format: "email" },
393
+ name: { type: "string" },
394
+ },
395
+ required: ["email", "name"],
396
+ },
397
+ },
398
+ required: ["id", "total", "customer"],
399
+ };
400
+
401
+ const nodeBInput = {
402
+ type: "object",
403
+ properties: {
404
+ id: { type: "string" },
405
+ total: { type: "number" },
406
+ customer: {
407
+ type: "object",
408
+ properties: { email: { type: "string" } },
409
+ required: ["email"],
410
+ },
411
+ },
412
+ required: ["id", "total", "customer"],
413
+ };
414
+
415
+ const result = checker.canConnect(nodeAOutput, nodeBInput);
416
+
417
+ console.log(result.isSubset); // true ✅
418
+ console.log(result.direction); // "sourceOutput ⊆ targetInput"
419
+ console.log(result.diffs); // []
420
+ ```
421
+
422
+ ---
423
+
424
+ ### `resolveConditions(schema, data)`
425
+
426
+ ```ts
427
+ resolveConditions(
428
+ schema: JSONSchema7,
429
+ data: Record<string, unknown>
430
+ ): ResolvedConditionResult
431
+ ```
432
+
433
+ Résout les `if/then/else` d'un schema en évaluant le `if` contre des données partielles (discriminants). Le schema résultant est un schema "aplati" sans `if/then/else`.
434
+
435
+ ```ts
436
+ interface ResolvedConditionResult {
437
+ resolved: JSONSchema7; // Schema avec if/then/else résolus
438
+ branch: "then" | "else" | null; // Branche appliquée
439
+ discriminant: Record<string, unknown>; // Discriminant utilisé
440
+ }
441
+ ```
442
+
443
+ ```ts
444
+ const formSchema = {
445
+ type: "object",
446
+ properties: {
447
+ accountType: { type: "string", enum: ["personal", "business"] },
448
+ email: { type: "string", format: "email" },
449
+ companyName: { type: "string" },
450
+ firstName: { type: "string" },
451
+ },
452
+ required: ["accountType", "email"],
453
+ if: {
454
+ properties: { accountType: { const: "business" } },
455
+ required: ["accountType"],
456
+ },
457
+ then: {
458
+ required: ["companyName"],
459
+ },
460
+ else: {
461
+ required: ["firstName"],
462
+ },
463
+ };
464
+
465
+ // Résoudre pour un compte business
466
+ const business = checker.resolveConditions(formSchema, {
467
+ accountType: "business",
468
+ });
469
+ console.log(business.branch); // "then"
470
+ console.log(business.resolved.required);
471
+ // → ["accountType", "email", "companyName"]
472
+
473
+ // Résoudre pour un compte personnel
474
+ const personal = checker.resolveConditions(formSchema, {
475
+ accountType: "personal",
476
+ });
477
+ console.log(personal.branch); // "else"
478
+ console.log(personal.resolved.required);
479
+ // → ["accountType", "email", "firstName"]
480
+ ```
481
+
482
+ ---
483
+
484
+ ### `checkResolved(sub, sup, subData, supData?)`
485
+
486
+ ```ts
487
+ checkResolved(
488
+ sub: JSONSchema7,
489
+ sup: JSONSchema7,
490
+ subData: Record<string, unknown>,
491
+ supData?: Record<string, unknown>
492
+ ): SubsetResult & {
493
+ resolvedSub: ResolvedConditionResult;
494
+ resolvedSup: ResolvedConditionResult;
495
+ }
496
+ ```
497
+
498
+ Raccourci : résout les conditions des deux schemas **puis** vérifie `sub ⊆ sup`. Utile quand le superset contient des `if/then/else` et que vous connaissez les valeurs discriminantes.
499
+
500
+ ```ts
501
+ const conditionalSup = {
502
+ type: "object",
503
+ properties: {
504
+ kind: { type: "string" },
505
+ value: {},
506
+ },
507
+ required: ["kind", "value"],
508
+ if: {
509
+ properties: { kind: { const: "text" } },
510
+ required: ["kind"],
511
+ },
512
+ then: {
513
+ properties: { value: { type: "string" } },
514
+ },
515
+ else: {
516
+ properties: { value: { type: "number" } },
517
+ },
518
+ };
519
+
520
+ const sub = {
521
+ type: "object",
522
+ properties: {
523
+ kind: { const: "text" },
524
+ value: { type: "string", minLength: 1 },
525
+ },
526
+ required: ["kind", "value"],
527
+ };
528
+
529
+ // Sans résolution : false (le if/then/else brut ne matche pas)
530
+ console.log(checker.isSubset(sub, conditionalSup)); // false
531
+
532
+ // Avec résolution : true !
533
+ const result = checker.checkResolved(sub, conditionalSup, { kind: "text" });
534
+ console.log(result.isSubset); // true ✅
535
+ console.log(result.resolvedSup.branch); // "then"
536
+ ```
537
+
538
+ ---
539
+
540
+ ### `normalize(schema)`
541
+
542
+ ```ts
543
+ normalize(def: JSONSchema7Definition): JSONSchema7Definition
544
+ ```
545
+
546
+ Normalise un schema : infère `type` depuis `const`/`enum`, résout la double négation `not(not(X)) → X`, et normalise récursivement tous les sous-schemas.
547
+
548
+ ```ts
549
+ // Infère le type depuis const
550
+ checker.normalize({ const: "hello" });
551
+ // → { const: "hello", type: "string" }
552
+
553
+ // Infère le type depuis enum
554
+ checker.normalize({ enum: [1, 2, 3] });
555
+ // → { enum: [1, 2, 3], type: "integer" }
556
+
557
+ // Convertit enum à un seul élément en const
558
+ checker.normalize({ enum: ["only"] });
559
+ // → { const: "only", type: "string" }
560
+
561
+ // Résout la double négation
562
+ checker.normalize({ not: { not: { type: "string" } } });
563
+ // → { type: "string" }
564
+ ```
565
+
566
+ ---
567
+
568
+ ### `formatResult(label, result)`
569
+
570
+ ```ts
571
+ formatResult(label: string, result: SubsetResult): string
572
+ ```
573
+
574
+ Formate un `SubsetResult` en chaîne lisible pour les logs / le debug.
575
+
576
+ ```ts
577
+ const result = checker.check(
578
+ { type: "number", minimum: 0, maximum: 100 },
579
+ { type: "number", minimum: 5, maximum: 10 }
580
+ );
581
+
582
+ console.log(checker.formatResult("range check", result));
583
+ // ❌ range check: false
584
+ // Diffs:
585
+ // ~ minimum: 0 → 5
586
+ // ~ maximum: 100 → 10
587
+ ```
588
+
589
+ ```ts
590
+ const result2 = checker.check(
591
+ { type: "string", minLength: 5 },
592
+ { type: "string" }
593
+ );
594
+
595
+ console.log(checker.formatResult("strict ⊆ loose", result2));
596
+ // ✅ strict ⊆ loose: true
597
+ ```
598
+
599
+ Les différentes icônes dans le diff :
600
+ - `+` — contrainte **ajoutée** par le merge (absente dans sub, présente dans l'intersection)
601
+ - `-` — contrainte **supprimée** par le merge
602
+ - `~` — contrainte **modifiée** (valeur différente entre sub et l'intersection)
603
+
604
+ ---
605
+
606
+ ## Guide des fonctionnalités
607
+
608
+ Cette section présente les fonctionnalités supportées, du plus simple au plus complexe, avec des exemples illustratifs.
609
+
610
+ ---
611
+
612
+ ### 1. Compatibilité de types
613
+
614
+ La librairie comprend le système de types JSON Schema et ses relations d'inclusion.
615
+
616
+ ```ts
617
+ // integer ⊆ number (tout entier est un nombre)
618
+ checker.isSubset({ type: "integer" }, { type: "number" }); // true
619
+
620
+ // number ⊄ integer (1.5 est un nombre mais pas un entier)
621
+ checker.isSubset({ type: "number" }, { type: "integer" }); // false
622
+
623
+ // Types incompatibles
624
+ checker.isSubset({ type: "string" }, { type: "number" }); // false
625
+ checker.isSubset({ type: "boolean" }, { type: "string" }); // false
626
+
627
+ // Identité
628
+ checker.isSubset({ type: "string" }, { type: "string" }); // true
629
+
630
+ // L'intersection integer ∩ number = integer
631
+ checker.intersect({ type: "integer" }, { type: "number" });
632
+ // → { type: "integer" }
633
+ ```
634
+
635
+ ---
636
+
637
+ ### 2. Champs requis (`required`)
638
+
639
+ Un schema qui exige **plus** de champs est un sous-ensemble d'un schema qui en exige **moins**.
640
+
641
+ ```ts
642
+ const strict = {
643
+ type: "object",
644
+ properties: {
645
+ name: { type: "string" },
646
+ age: { type: "number" },
647
+ },
648
+ required: ["name", "age"],
649
+ };
650
+
651
+ const loose = {
652
+ type: "object",
653
+ properties: {
654
+ name: { type: "string" },
655
+ },
656
+ required: ["name"],
657
+ };
658
+
659
+ // Plus de champs requis → plus restrictif → sous-ensemble
660
+ checker.isSubset(strict, loose); // true
661
+
662
+ // Moins de champs requis → plus permissif → PAS sous-ensemble
663
+ checker.isSubset(loose, strict); // false
664
+
665
+ // Le diagnostic montre exactement ce qui manque
666
+ const result = checker.check(loose, strict);
667
+ console.log(result.diffs);
668
+ // [
669
+ // { path: "required", type: "changed", expected: ["name"], actual: ["name", "age"] },
670
+ // { path: "properties.age", type: "added", expected: undefined, actual: { type: "number" } }
671
+ // ]
672
+ ```
673
+
674
+ ---
675
+
676
+ ### 3. Contraintes numériques
677
+
678
+ La librairie gère `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum` et `multipleOf`.
679
+
680
+ ```ts
681
+ // Plage stricte ⊆ plage large
682
+ checker.isSubset(
683
+ { type: "number", minimum: 5, maximum: 10 },
684
+ { type: "number", minimum: 0, maximum: 100 }
685
+ ); // true
686
+
687
+ // Plage large ⊄ plage stricte
688
+ checker.isSubset(
689
+ { type: "number", minimum: 0, maximum: 100 },
690
+ { type: "number", minimum: 5, maximum: 10 }
691
+ ); // false
692
+
693
+ // exclusiveMinimum
694
+ checker.isSubset(
695
+ { type: "number", exclusiveMinimum: 5 },
696
+ { type: "number", exclusiveMinimum: 0 }
697
+ ); // true (x > 5 implique x > 0)
698
+
699
+ // multipleOf : 6 est multiple de 3
700
+ checker.isSubset(
701
+ { type: "number", multipleOf: 6 },
702
+ { type: "number", multipleOf: 3 }
703
+ ); // true
704
+
705
+ // multipleOf : 3 n'est PAS multiple de 6
706
+ checker.isSubset(
707
+ { type: "number", multipleOf: 3 },
708
+ { type: "number", multipleOf: 6 }
709
+ ); // false
710
+
711
+ // L'intersection conserve les contraintes les plus restrictives
712
+ checker.intersect(
713
+ { type: "number", minimum: 5, maximum: 10 },
714
+ { type: "number", minimum: 0, maximum: 100 }
715
+ );
716
+ // → { type: "number", minimum: 5, maximum: 10 }
717
+ ```
718
+
719
+ ---
720
+
721
+ ### 4. Contraintes de chaînes
722
+
723
+ Gestion de `minLength`, `maxLength` et `pattern`.
724
+
725
+ ```ts
726
+ const strict = {
727
+ type: "string",
728
+ minLength: 3,
729
+ maxLength: 10,
730
+ pattern: "^[a-z]+$",
731
+ };
732
+
733
+ const loose = {
734
+ type: "string",
735
+ minLength: 1,
736
+ maxLength: 100,
737
+ };
738
+
739
+ // Plus de contraintes → sous-ensemble
740
+ checker.isSubset(strict, loose); // true
741
+
742
+ // Moins de contraintes → PAS sous-ensemble
743
+ checker.isSubset(loose, strict); // false
744
+ ```
745
+
746
+ Pour les patterns regex, voir la section [12. Patterns regex](#12-patterns-regex-pattern).
747
+
748
+ ---
749
+
750
+ ### 5. `enum` et `const`
751
+
752
+ #### Enum
753
+
754
+ ```ts
755
+ // Petit enum ⊆ grand enum (toutes les valeurs du petit sont dans le grand)
756
+ checker.isSubset(
757
+ { type: "string", enum: ["a", "b"] },
758
+ { type: "string", enum: ["a", "b", "c", "d"] }
759
+ ); // true
760
+
761
+ // Grand enum ⊄ petit enum
762
+ checker.isSubset(
763
+ { type: "string", enum: ["a", "b", "c", "d"] },
764
+ { type: "string", enum: ["a", "b"] }
765
+ ); // false
766
+
767
+ // Enum d'une seule valeur ⊆ type
768
+ checker.isSubset(
769
+ { type: "string", enum: ["hello"] },
770
+ { type: "string" }
771
+ ); // true
772
+
773
+ // Intersection d'enums = valeurs communes
774
+ checker.intersect(
775
+ { type: "string", enum: ["a", "b", "c"] },
776
+ { type: "string", enum: ["b", "c", "d"] }
777
+ );
778
+ // → { type: "string", enum: ["b", "c"] }
779
+ ```
780
+
781
+ #### Const
782
+
783
+ ```ts
784
+ // const string ⊆ type string
785
+ checker.isSubset({ const: "hello" }, { type: "string" }); // true
786
+
787
+ // const number ⊆ type number
788
+ checker.isSubset({ const: 42 }, { type: "number" }); // true
789
+
790
+ // const string ⊄ type number (types incompatibles)
791
+ checker.isSubset({ const: "hello" }, { type: "number" }); // false
792
+ ```
793
+
794
+ > **Normalisation** : un `enum` à un seul élément est automatiquement converti en `const` lors de la normalisation. `{ enum: ["x"] }` ≡ `{ const: "x" }`.
795
+
796
+ ---
797
+
798
+ ### 6. Contraintes de tableaux
799
+
800
+ Gestion de `items`, `minItems`, `maxItems`, `uniqueItems`.
801
+
802
+ ```ts
803
+ const strict = {
804
+ type: "array",
805
+ items: { type: "string", minLength: 1 },
806
+ minItems: 1,
807
+ maxItems: 5,
808
+ };
809
+
810
+ const loose = {
811
+ type: "array",
812
+ items: { type: "string" },
813
+ };
814
+
815
+ // Tableau plus contraint ⊆ tableau moins contraint
816
+ checker.isSubset(strict, loose); // true
817
+
818
+ // L'inverse est faux
819
+ checker.isSubset(loose, strict); // false
820
+
821
+ // uniqueItems: true est plus restrictif que sans uniqueItems
822
+ checker.isSubset(
823
+ { type: "array", items: { type: "number" }, uniqueItems: true },
824
+ { type: "array", items: { type: "number" } }
825
+ ); // true
826
+ ```
827
+
828
+ ---
829
+
830
+ ### 7. `additionalProperties`
831
+
832
+ `additionalProperties: false` ferme un objet : seules les propriétés listées dans `properties` sont autorisées.
833
+
834
+ ```ts
835
+ const closed = {
836
+ type: "object",
837
+ properties: { name: { type: "string" } },
838
+ required: ["name"],
839
+ additionalProperties: false,
840
+ };
841
+
842
+ const open = {
843
+ type: "object",
844
+ properties: {
845
+ name: { type: "string" },
846
+ age: { type: "number" },
847
+ },
848
+ required: ["name"],
849
+ };
850
+
851
+ // Fermé ⊆ ouvert (un objet sans propriétés supplémentaires est valide partout)
852
+ checker.isSubset(closed, open); // true
853
+
854
+ // Ouvert ⊄ fermé (un objet avec age serait rejeté par closed)
855
+ checker.isSubset(open, closed); // false
856
+
857
+ // Le diagnostic montre la contrainte
858
+ const result = checker.check(open, closed);
859
+ const addPropDiff = result.diffs.find(d => d.path === "additionalProperties");
860
+ console.log(addPropDiff); // { path: "additionalProperties", type: "added", ... }
861
+ ```
862
+
863
+ ---
864
+
865
+ ### 8. Objets imbriqués
866
+
867
+ La vérification de sous-ensemble est **récursive** : elle descend dans toutes les propriétés imbriquées.
868
+
869
+ ```ts
870
+ const deep = {
871
+ type: "object",
872
+ properties: {
873
+ user: {
874
+ type: "object",
875
+ properties: {
876
+ profile: {
877
+ type: "object",
878
+ properties: {
879
+ name: { type: "string" },
880
+ bio: { type: "string" },
881
+ },
882
+ required: ["name", "bio"],
883
+ },
884
+ },
885
+ required: ["profile"],
886
+ },
887
+ },
888
+ required: ["user"],
889
+ };
890
+
891
+ const shallow = {
892
+ type: "object",
893
+ properties: {
894
+ user: {
895
+ type: "object",
896
+ properties: {
897
+ profile: {
898
+ type: "object",
899
+ properties: { name: { type: "string" } },
900
+ required: ["name"],
901
+ },
902
+ },
903
+ required: ["profile"],
904
+ },
905
+ },
906
+ required: ["user"],
907
+ };
908
+
909
+ // Plus profond et plus exigeant → sous-ensemble du moins exigeant
910
+ checker.isSubset(deep, shallow); // true
911
+ checker.isSubset(shallow, deep); // false
912
+
913
+ // Les chemins de diff sont complets
914
+ const result = checker.check(shallow, deep);
915
+ const bioDiff = result.diffs.find(
916
+ d => d.path === "properties.user.properties.profile.properties.bio"
917
+ );
918
+ console.log(bioDiff?.type); // "added"
919
+ ```
920
+
921
+ ---
922
+
923
+ ### 9. `anyOf` / `oneOf`
924
+
925
+ La librairie supporte `anyOf` et `oneOf` avec distinction dans les chemins de diff.
926
+
927
+ #### anyOf
928
+
929
+ ```ts
930
+ const sub = {
931
+ anyOf: [{ type: "string" }, { type: "number" }],
932
+ };
933
+ const sup = {
934
+ anyOf: [{ type: "string" }, { type: "number" }, { type: "boolean" }],
935
+ };
936
+
937
+ // Chaque branche de sub doit matcher une branche de sup
938
+ checker.isSubset(sub, sup); // true
939
+ checker.isSubset(sup, sub); // false
940
+
941
+ // Atomic ⊆ anyOf (si au moins une branche accepte)
942
+ checker.isSubset(
943
+ { type: "string", minLength: 1 },
944
+ { anyOf: [{ type: "string" }, { type: "number" }] }
945
+ ); // true
946
+
947
+ // Atomic ⊄ anyOf (si aucune branche n'accepte)
948
+ checker.isSubset(
949
+ { type: "boolean" },
950
+ { anyOf: [{ type: "string" }, { type: "number" }] }
951
+ ); // false
952
+ ```
953
+
954
+ #### oneOf
955
+
956
+ Le `oneOf` est traité comme `anyOf` pour la vérification de sous-ensemble (chaque branche doit être acceptée). La différence apparaît dans les **chemins de diff**.
957
+
958
+ ```ts
959
+ // Les chemins de diff utilisent le bon label
960
+ const result = checker.check(
961
+ { oneOf: [{ type: "string" }, { type: "number" }, { type: "boolean" }] },
962
+ { oneOf: [{ type: "string" }, { type: "number" }] }
963
+ );
964
+
965
+ result.diffs[0].path; // "oneOf[2]" (et non "anyOf[2]")
966
+ ```
967
+
968
+ #### Unions discriminées
969
+
970
+ ```ts
971
+ const sub = {
972
+ oneOf: [
973
+ {
974
+ type: "object",
975
+ properties: { kind: { const: "a" }, value: { type: "string" } },
976
+ required: ["kind", "value"],
977
+ },
978
+ {
979
+ type: "object",
980
+ properties: { kind: { const: "b" }, value: { type: "number" } },
981
+ required: ["kind", "value"],
982
+ },
983
+ ],
984
+ };
985
+
986
+ const sup = {
987
+ type: "object",
988
+ properties: { kind: { type: "string" } },
989
+ required: ["kind"],
990
+ };
991
+
992
+ // Chaque branche de l'union discriminée est sous-ensemble du sup
993
+ checker.isSubset(sub, sup); // true
994
+ ```
995
+
996
+ > **Note** : La librairie ne vérifie **pas** l'exclusivité sémantique de `oneOf` (le fait qu'exactement une branche doit matcher). Elle traite `oneOf` comme `anyOf` pour la vérification de sous-ensemble.
997
+
998
+ ---
999
+
1000
+ ### 10. Négation (`not`)
1001
+
1002
+ La librairie gère le mot-clé `not` avec un raisonnement étendu.
1003
+
1004
+ #### Cas de base
1005
+
1006
+ ```ts
1007
+ // number ⊆ not(string) → true (un nombre n'est jamais une string)
1008
+ checker.isSubset(
1009
+ { type: "number" },
1010
+ { not: { type: "string" } }
1011
+ ); // true
1012
+
1013
+ // string ⊄ not(string) → false
1014
+ checker.isSubset(
1015
+ { type: "string" },
1016
+ { not: { type: "string" } }
1017
+ ); // false
1018
+ ```
1019
+
1020
+ #### not avec `const` et `enum`
1021
+
1022
+ ```ts
1023
+ // status: "active" est compatible avec not(status: "deleted")
1024
+ checker.isSubset(
1025
+ {
1026
+ type: "object",
1027
+ properties: { status: { const: "active" } },
1028
+ required: ["status"],
1029
+ },
1030
+ {
1031
+ not: {
1032
+ type: "object",
1033
+ properties: { status: { const: "deleted" } },
1034
+ required: ["status"],
1035
+ },
1036
+ }
1037
+ ); // true
1038
+
1039
+ // enum disjoint du not.enum → compatible
1040
+ checker.isSubset(
1041
+ { enum: [1, 2] },
1042
+ { not: { enum: [3, 4] } }
1043
+ ); // true
1044
+
1045
+ // enum qui chevauche not.enum → incompatible
1046
+ checker.isSubset(
1047
+ { enum: [1, 2, 3] },
1048
+ { not: { enum: [3, 4] } }
1049
+ ); // false
1050
+ ```
1051
+
1052
+ #### not avec `anyOf` / `oneOf`
1053
+
1054
+ ```ts
1055
+ // number est compatible avec not(anyOf([string, null]))
1056
+ checker.isSubset(
1057
+ { type: "number" },
1058
+ { not: { anyOf: [{ type: "string" }, { type: "null" }] } }
1059
+ ); // true
1060
+
1061
+ // string est INcompatible avec not(anyOf([string, null]))
1062
+ checker.isSubset(
1063
+ { type: "string" },
1064
+ { not: { anyOf: [{ type: "string" }, { type: "null" }] } }
1065
+ ); // false
1066
+ ```
1067
+
1068
+ #### Double négation
1069
+
1070
+ La normalisation résout automatiquement `not(not(X))` en `X` :
1071
+
1072
+ ```ts
1073
+ // not(not(string)) normalise en string
1074
+ checker.normalize({ not: { not: { type: "string" } } });
1075
+ // → { type: "string" }
1076
+
1077
+ // Donc not(not(string)) ⊆ string
1078
+ checker.isSubset(
1079
+ { not: { not: { type: "string", minLength: 3 } } },
1080
+ { type: "string" }
1081
+ ); // true
1082
+ ```
1083
+
1084
+ #### `not` dans sub comme restriction
1085
+
1086
+ Quand `not` apparaît dans le sub, c'est une **restriction** (exclut des valeurs), donc le sub reste un sous-ensemble du sup :
1087
+
1088
+ ```ts
1089
+ // { type: "string", not: { const: "foo" } } ⊆ { type: "string" }
1090
+ // "Toutes les strings sauf foo" est sous-ensemble de "toutes les strings"
1091
+ checker.isSubset(
1092
+ { type: "string", not: { const: "foo" } },
1093
+ { type: "string" }
1094
+ ); // true
1095
+ ```
1096
+
1097
+ ---
1098
+
1099
+ ### 11. Formats (`format`)
1100
+
1101
+ La librairie connaît les formats JSON Schema Draft-07 et leur hiérarchie d'inclusion.
1102
+
1103
+ #### Formats supportés
1104
+
1105
+ `date-time`, `date`, `time`, `email`, `idn-email`, `hostname`, `idn-hostname`, `ipv4`, `ipv6`, `uri`, `uri-reference`, `iri`, `iri-reference`, `uri-template`, `uuid`, `json-pointer`, `relative-json-pointer`, `regex`.
1106
+
1107
+ #### Hiérarchie des formats
1108
+
1109
+ ```
1110
+ email ⊆ idn-email
1111
+ hostname ⊆ idn-hostname
1112
+ uri ⊆ iri
1113
+ uri-reference ⊆ iri-reference
1114
+ ```
1115
+
1116
+ #### Exemples
1117
+
1118
+ ```ts
1119
+ // format ⊆ type (email ⊆ string) → géré nativement par le merge
1120
+ checker.isSubset(
1121
+ { type: "string", format: "email" },
1122
+ { type: "string" }
1123
+ ); // true
1124
+
1125
+ // type ⊄ format (string ⊄ email) → le format ajoute une contrainte
1126
+ checker.isSubset(
1127
+ { type: "string" },
1128
+ { type: "string", format: "email" }
1129
+ ); // false
1130
+
1131
+ // Hiérarchie : email ⊆ idn-email
1132
+ checker.isSubset(
1133
+ { type: "string", format: "email" },
1134
+ { type: "string", format: "idn-email" }
1135
+ ); // true
1136
+
1137
+ // Hiérarchie inverse : idn-email ⊄ email
1138
+ checker.isSubset(
1139
+ { type: "string", format: "idn-email" },
1140
+ { type: "string", format: "email" }
1141
+ ); // false
1142
+
1143
+ // Formats incompatibles : email ∩ ipv4 = ∅
1144
+ checker.intersect(
1145
+ { type: "string", format: "email" },
1146
+ { type: "string", format: "ipv4" }
1147
+ ); // null
1148
+
1149
+ // Même format : email ∩ email = email
1150
+ checker.intersect(
1151
+ { type: "string", format: "email" },
1152
+ { type: "string", format: "email" }
1153
+ );
1154
+ // → { type: "string", format: "email" }
1155
+ ```
1156
+
1157
+ #### Formats dans les conditions
1158
+
1159
+ Les formats sont aussi évalués dans les conditions `if/then/else` via `class-validator` :
1160
+
1161
+ ```ts
1162
+ const schema = {
1163
+ type: "object",
1164
+ properties: {
1165
+ contactMethod: { type: "string" },
1166
+ contactValue: { type: "string" },
1167
+ },
1168
+ if: {
1169
+ properties: { contactValue: { format: "email" } },
1170
+ },
1171
+ then: { required: ["contactValue"] },
1172
+ };
1173
+
1174
+ const result = checker.resolveConditions(schema, {
1175
+ contactValue: "test@example.com", // valide pour format: email
1176
+ });
1177
+ console.log(result.branch); // "then"
1178
+ ```
1179
+
1180
+ ---
1181
+
1182
+ ### 12. Patterns regex (`pattern`)
1183
+
1184
+ Les patterns regex sont comparés via une approche par **échantillonnage** (sampling) pour détecter les inclusions.
1185
+
1186
+ #### Mêmes patterns
1187
+
1188
+ ```ts
1189
+ // Même pattern → toujours sous-ensemble
1190
+ checker.isSubset(
1191
+ { type: "string", pattern: "^[a-z]+$" },
1192
+ { type: "string", pattern: "^[a-z]+$" }
1193
+ ); // true
1194
+ ```
1195
+
1196
+ #### Pattern plus restrictif ⊆ pattern plus permissif
1197
+
1198
+ ```ts
1199
+ // ^[a-z]{3}$ ⊆ ^[a-z]+$ (3 lettres ⊆ 1+ lettres)
1200
+ checker.isSubset(
1201
+ { type: "string", pattern: "^[a-z]{3}$" },
1202
+ { type: "string", pattern: "^[a-z]+$" }
1203
+ ); // true
1204
+
1205
+ // L'inverse est faux
1206
+ checker.isSubset(
1207
+ { type: "string", pattern: "^[a-z]+$" },
1208
+ { type: "string", pattern: "^[a-z]{3}$" }
1209
+ ); // false
1210
+ ```
1211
+
1212
+ #### Patterns incompatibles
1213
+
1214
+ ```ts
1215
+ // Lettres ⊄ chiffres
1216
+ checker.isSubset(
1217
+ { type: "string", pattern: "^[a-z]+$" },
1218
+ { type: "string", pattern: "^[0-9]+$" }
1219
+ ); // false
1220
+ ```
1221
+
1222
+ #### Pattern vs pas de pattern
1223
+
1224
+ ```ts
1225
+ // Sub avec pattern, sup sans pattern → sous-ensemble (sub plus restrictif)
1226
+ checker.isSubset(
1227
+ { type: "string", pattern: "^[a-z]+$" },
1228
+ { type: "string" }
1229
+ ); // true
1230
+
1231
+ // Sub sans pattern, sup avec pattern → PAS sous-ensemble
1232
+ checker.isSubset(
1233
+ { type: "string" },
1234
+ { type: "string", pattern: "^[a-z]+$" }
1235
+ ); // false
1236
+ ```
1237
+
1238
+ #### Patterns dans les propriétés imbriquées
1239
+
1240
+ ```ts
1241
+ // Pattern sur une propriété imbriquée
1242
+ checker.isSubset(
1243
+ {
1244
+ type: "object",
1245
+ properties: { code: { type: "string", pattern: "^FR[0-9]{5}$" } },
1246
+ required: ["code"],
1247
+ },
1248
+ {
1249
+ type: "object",
1250
+ properties: { code: { type: "string", pattern: "^[A-Z]{2}[0-9]+$" } },
1251
+ required: ["code"],
1252
+ }
1253
+ ); // true (FR + 5 chiffres ⊆ 2 majuscules + chiffres)
1254
+ ```
1255
+
1256
+ > **Note** : la comparaison de patterns utilise un échantillonnage avec 200 samples par défaut. C'est une heuristique, pas une preuve formelle. Les faux positifs sont possibles mais très improbables. Les faux négatifs (counter-examples concrets) sont certains.
1257
+
1258
+ ---
1259
+
1260
+ ### 13. Conditions `if` / `then` / `else`
1261
+
1262
+ La librairie peut résoudre les conditions JSON Schema en évaluant le `if` contre des données partielles.
1263
+
1264
+ #### Résolution simple
1265
+
1266
+ ```ts
1267
+ const schema = {
1268
+ type: "object",
1269
+ properties: {
1270
+ status: { type: "string" },
1271
+ activatedAt: { type: "string", format: "date-time" },
1272
+ },
1273
+ required: ["status"],
1274
+ if: {
1275
+ properties: { status: { const: "active" } },
1276
+ required: ["status"],
1277
+ },
1278
+ then: {
1279
+ required: ["activatedAt"],
1280
+ },
1281
+ };
1282
+
1283
+ // Si status = "active" → branche then appliquée
1284
+ const active = checker.resolveConditions(schema, { status: "active" });
1285
+ console.log(active.branch); // "then"
1286
+ console.log(active.resolved.required); // ["status", "activatedAt"]
1287
+
1288
+ // Si status ≠ "active" → branche else (ou pas de branche supplémentaire)
1289
+ const inactive = checker.resolveConditions(schema, { status: "inactive" });
1290
+ console.log(inactive.branch); // "else"
1291
+ console.log(inactive.resolved.required); // ["status"]
1292
+ ```
1293
+
1294
+ #### Résolution avec des conditions sur `enum`
1295
+
1296
+ ```ts
1297
+ const schema = {
1298
+ type: "object",
1299
+ properties: {
1300
+ tier: { type: "string" },
1301
+ limit: { type: "number" },
1302
+ },
1303
+ required: ["tier"],
1304
+ if: {
1305
+ properties: { tier: { enum: ["premium", "enterprise"] } },
1306
+ required: ["tier"],
1307
+ },
1308
+ then: {
1309
+ properties: { limit: { type: "number", minimum: 1000 } },
1310
+ required: ["limit"],
1311
+ },
1312
+ else: {
1313
+ properties: { limit: { type: "number", maximum: 100 } },
1314
+ },
1315
+ };
1316
+
1317
+ const premium = checker.resolveConditions(schema, { tier: "premium" });
1318
+ console.log(premium.branch); // "then"
1319
+ // limit requis avec minimum 1000
1320
+
1321
+ const free = checker.resolveConditions(schema, { tier: "free" });
1322
+ console.log(free.branch); // "else"
1323
+ // limit optionnel avec maximum 100
1324
+ ```
1325
+
1326
+ #### Conditions imbriquées dans les propriétés
1327
+
1328
+ La résolution est **récursive** : les conditions à l'intérieur des propriétés sont aussi résolues.
1329
+
1330
+ ```ts
1331
+ const schema = {
1332
+ type: "object",
1333
+ properties: {
1334
+ config: {
1335
+ type: "object",
1336
+ properties: {
1337
+ mode: { type: "string", enum: ["fast", "safe"] },
1338
+ retries: { type: "number" },
1339
+ timeout: { type: "number" },
1340
+ },
1341
+ required: ["mode"],
1342
+ if: {
1343
+ properties: { mode: { const: "safe" } },
1344
+ required: ["mode"],
1345
+ },
1346
+ then: {
1347
+ required: ["retries", "timeout"],
1348
+ properties: {
1349
+ retries: { type: "number", minimum: 3 },
1350
+ timeout: { type: "number", minimum: 1000 },
1351
+ },
1352
+ },
1353
+ },
1354
+ },
1355
+ required: ["config"],
1356
+ };
1357
+
1358
+ const result = checker.resolveConditions(schema, {
1359
+ config: { mode: "safe" },
1360
+ });
1361
+
1362
+ // La condition dans config a été résolue
1363
+ const configProp = result.resolved.properties?.config;
1364
+ console.log(configProp?.required); // ["mode", "retries", "timeout"]
1365
+ ```
1366
+
1367
+ #### Évaluation avancée du `if`
1368
+
1369
+ Le `if` est évalué contre les données avec support complet de :
1370
+
1371
+ | Mot-clé | Description |
1372
+ |---|---|
1373
+ | `properties` avec `const` | Correspondance exacte d'une valeur |
1374
+ | `properties` avec `enum` | Valeur dans une liste |
1375
+ | `properties` avec `type` | Vérification du type |
1376
+ | `required` | Présence des clés |
1377
+ | `allOf` | Toutes les conditions doivent matcher |
1378
+ | `anyOf` | Au moins une condition doit matcher |
1379
+ | `oneOf` | Exactement une condition doit matcher |
1380
+ | `not` | Inversion du résultat |
1381
+ | `format` | Validation sémantique via `class-validator` |
1382
+ | Contraintes numériques | `minimum`, `maximum`, `exclusiveMinimum`, etc. |
1383
+ | Contraintes string | `minLength`, `maxLength` |
1384
+ | Contraintes array | `minItems`, `maxItems`, `uniqueItems` |
1385
+
1386
+ ---
1387
+
1388
+ ### 14. `allOf` avec conditions
1389
+
1390
+ Les conditions peuvent apparaître dans un `allOf`. Chaque entrée contenant un `if/then/else` est résolue individuellement.
1391
+
1392
+ ```ts
1393
+ const schema = {
1394
+ type: "object",
1395
+ properties: {
1396
+ name: { type: "string" },
1397
+ age: { type: "number" },
1398
+ role: { type: "string", enum: ["admin", "user", "guest"] },
1399
+ },
1400
+ required: ["name"],
1401
+ allOf: [
1402
+ {
1403
+ if: {
1404
+ properties: { age: { type: "number", exclusiveMinimum: 20 } },
1405
+ required: ["age"],
1406
+ },
1407
+ then: {
1408
+ required: ["email"],
1409
+ properties: { email: { type: "string" } },
1410
+ },
1411
+ },
1412
+ {
1413
+ if: {
1414
+ properties: { role: { const: "admin" } },
1415
+ required: ["role"],
1416
+ },
1417
+ then: {
1418
+ properties: {
1419
+ permissions: { type: "array", items: { type: "string" } },
1420
+ },
1421
+ required: ["permissions"],
1422
+ },
1423
+ },
1424
+ ],
1425
+ };
1426
+
1427
+ // Les deux conditions matchent
1428
+ const result = checker.resolveConditions(schema, {
1429
+ name: "Alice",
1430
+ age: 25,
1431
+ role: "admin",
1432
+ });
1433
+
1434
+ console.log(result.resolved.required);
1435
+ // → ["name", "email", "permissions"]
1436
+ // email requis car age > 20, permissions requis car role = admin
1437
+
1438
+ console.log(result.resolved.properties?.email);
1439
+ // → { type: "string" }
1440
+
1441
+ console.log(result.resolved.properties?.permissions);
1442
+ // → { type: "array", items: { type: "string" } }
1443
+ ```
1444
+
1445
+ #### `allOf` combiné avec un `if/then/else` au niveau racine
1446
+
1447
+ ```ts
1448
+ const schema = {
1449
+ type: "object",
1450
+ properties: {
1451
+ kind: { type: "string" },
1452
+ value: {},
1453
+ },
1454
+ required: ["kind"],
1455
+ // Condition racine
1456
+ if: {
1457
+ properties: { kind: { const: "numeric" } },
1458
+ required: ["kind"],
1459
+ },
1460
+ then: {
1461
+ properties: { value: { type: "number" } },
1462
+ },
1463
+ else: {
1464
+ properties: { value: { type: "string" } },
1465
+ },
1466
+ // Condition dans allOf
1467
+ allOf: [
1468
+ {
1469
+ if: {
1470
+ properties: { kind: { const: "numeric" } },
1471
+ required: ["kind"],
1472
+ },
1473
+ then: {
1474
+ properties: { precision: { type: "number" } },
1475
+ },
1476
+ },
1477
+ ],
1478
+ };
1479
+
1480
+ const result = checker.resolveConditions(schema, { kind: "numeric" });
1481
+
1482
+ // Les deux conditions (racine + allOf) sont résolues
1483
+ console.log(result.resolved.properties?.value); // { type: "number" }
1484
+ console.log(result.resolved.properties?.precision); // { type: "number" }
1485
+ ```
1486
+
1487
+ ---
1488
+
1489
+ ## Fonctions utilitaires
1490
+
1491
+ En plus de la classe principale, la librairie exporte des fonctions utilitaires pour travailler avec les patterns regex.
1492
+
1493
+ ```ts
1494
+ import {
1495
+ isPatternSubset,
1496
+ arePatternsEquivalent,
1497
+ isTrivialPattern,
1498
+ } from "json-schema-compatibility-checker";
1499
+ ```
1500
+
1501
+ ---
1502
+
1503
+ ### `isPatternSubset(sub, sup)`
1504
+
1505
+ ```ts
1506
+ isPatternSubset(
1507
+ subPattern: string,
1508
+ supPattern: string,
1509
+ sampleCount?: number // défaut: 200
1510
+ ): boolean | null
1511
+ ```
1512
+
1513
+ Vérifie si le langage du pattern `sub` est un sous-ensemble du langage du pattern `sup` via **échantillonnage**.
1514
+
1515
+ **Contrat ternaire :**
1516
+ - `true` — toutes les strings échantillonnées de sub matchent sup (confiance haute)
1517
+ - `false` — au moins une string de sub ne matche PAS sup (certain, c'est un contre-exemple)
1518
+ - `null` — impossible de déterminer (pattern invalide, génération échouée)
1519
+
1520
+ ```ts
1521
+ import { isPatternSubset } from "json-schema-compatibility-checker";
1522
+
1523
+ isPatternSubset("^[a-z]{3}$", "^[a-z]+$"); // true — 3 lettres ⊆ 1+ lettres
1524
+ isPatternSubset("^[a-z]+$", "^[0-9]+$"); // false — lettres ⊄ chiffres
1525
+ isPatternSubset("^[a-z]+$", "^[a-z]{3}$"); // false — "ab" matche sub mais pas sup
1526
+ isPatternSubset("invalid[", "^[a-z]+$"); // null — pattern invalide
1527
+
1528
+ // Cas réalistes
1529
+ isPatternSubset("^SKU-[0-9]{6}$", "^[A-Z]+-[0-9]+$"); // true
1530
+ isPatternSubset("^FR[0-9]{5}$", "^[A-Z]{2}[0-9]+$"); // true
1531
+ isPatternSubset("^(75|92|93|94)[0-9]{3}$", "^[0-9]{5}$"); // true
1532
+ ```
1533
+
1534
+ ---
1535
+
1536
+ ### `arePatternsEquivalent(a, b)`
1537
+
1538
+ ```ts
1539
+ arePatternsEquivalent(
1540
+ patternA: string,
1541
+ patternB: string,
1542
+ sampleCount?: number // défaut: 200
1543
+ ): boolean | null
1544
+ ```
1545
+
1546
+ Vérifie si deux patterns acceptent le **même langage** via un échantillonnage bidirectionnel (`A ⊆ B` ET `B ⊆ A`).
1547
+
1548
+ ```ts
1549
+ import { arePatternsEquivalent } from "json-schema-compatibility-checker";
1550
+
1551
+ arePatternsEquivalent("^[a-z]+$", "^[a-z]+$"); // true — identiques
1552
+ arePatternsEquivalent("^[a-z]+$", "^[a-z]{3}$"); // false — cardinalité différente
1553
+ arePatternsEquivalent("^[a-f]+$", "^[a-z]+$"); // false — a-f ⊆ a-z mais pas l'inverse
1554
+ ```
1555
+
1556
+ ---
1557
+
1558
+ ### `isTrivialPattern(pattern)`
1559
+
1560
+ ```ts
1561
+ isTrivialPattern(pattern: string): boolean
1562
+ ```
1563
+
1564
+ Vérifie si un pattern est **universellement permissif** (matche toute string). Utile pour détecter les patterns qui n'ajoutent aucune contrainte réelle.
1565
+
1566
+ ```ts
1567
+ import { isTrivialPattern } from "json-schema-compatibility-checker";
1568
+
1569
+ isTrivialPattern(".*"); // true
1570
+ isTrivialPattern(".+"); // true
1571
+ isTrivialPattern("^.*$"); // true
1572
+ isTrivialPattern("^.+$"); // true
1573
+ isTrivialPattern(""); // true (pattern vide)
1574
+
1575
+ isTrivialPattern("^[a-z]+$"); // false
1576
+ isTrivialPattern("^[0-9]{3}$"); // false
1577
+ isTrivialPattern("abc"); // false
1578
+ ```
1579
+
1580
+ ---
1581
+
1582
+ ## Cas d'usage concrets
1583
+
1584
+ ### Connexion de nœuds dans un orchestrateur
1585
+
1586
+ Le cas d'usage principal de la librairie : dans un système d'orchestration visuel (style n8n, Node-RED, Zapier), vérifier que la sortie d'un nœud est compatible avec l'entrée du suivant.
1587
+
1588
+ ```ts
1589
+ const checker = new JsonSchemaCompatibilityChecker();
1590
+
1591
+ // Nœud A : API qui retourne des utilisateurs paginés
1592
+ const nodeAOutput = {
1593
+ type: "object",
1594
+ properties: {
1595
+ items: {
1596
+ type: "array",
1597
+ items: {
1598
+ type: "object",
1599
+ properties: {
1600
+ id: { type: "integer", minimum: 1 },
1601
+ name: { type: "string", minLength: 1, maxLength: 255 },
1602
+ tags: {
1603
+ type: "array",
1604
+ items: { type: "string" },
1605
+ uniqueItems: true,
1606
+ },
1607
+ },
1608
+ required: ["id", "name"],
1609
+ },
1610
+ },
1611
+ page: { type: "integer", minimum: 1 },
1612
+ pageSize: { type: "integer", minimum: 1, maximum: 100 },
1613
+ totalPages: { type: "integer", minimum: 0 },
1614
+ },
1615
+ required: ["items", "page", "pageSize", "totalPages"],
1616
+ };
1617
+
1618
+ // Nœud B : traitement qui attend une liste avec pagination
1619
+ const nodeBInput = {
1620
+ type: "object",
1621
+ properties: {
1622
+ items: {
1623
+ type: "array",
1624
+ items: {
1625
+ type: "object",
1626
+ properties: {
1627
+ id: { type: "number" },
1628
+ name: { type: "string" },
1629
+ },
1630
+ required: ["id"],
1631
+ },
1632
+ },
1633
+ page: { type: "number" },
1634
+ totalPages: { type: "number" },
1635
+ },
1636
+ required: ["items"],
1637
+ };
1638
+
1639
+ const connection = checker.canConnect(nodeAOutput, nodeBInput);
1640
+ console.log(connection.isSubset); // true ✅
1641
+ console.log(connection.direction); // "sourceOutput ⊆ targetInput"
1642
+
1643
+ // Si incompatible, le diagnostic explique pourquoi
1644
+ if (!connection.isSubset) {
1645
+ console.log(checker.formatResult("NodeA → NodeB", connection));
1646
+ }
1647
+ ```
1648
+
1649
+ ---
1650
+
1651
+ ### Validation de réponse API
1652
+
1653
+ Vérifier qu'une réponse API réelle est compatible avec ce qu'un consommateur attend.
1654
+
1655
+ ```ts
1656
+ const apiResponse = {
1657
+ type: "object",
1658
+ properties: {
1659
+ status: { type: "integer", minimum: 100, maximum: 599 },
1660
+ data: {
1661
+ type: "object",
1662
+ properties: {
1663
+ users: {
1664
+ type: "array",
1665
+ items: {
1666
+ type: "object",
1667
+ properties: {
1668
+ id: { type: "string", format: "uuid" },
1669
+ email: { type: "string", format: "email" },
1670
+ name: { type: "string", minLength: 1 },
1671
+ role: { type: "string", enum: ["admin", "user", "viewer"] },
1672
+ },
1673
+ required: ["id", "email", "name", "role"],
1674
+ },
1675
+ },
1676
+ total: { type: "integer", minimum: 0 },
1677
+ },
1678
+ required: ["users", "total"],
1679
+ },
1680
+ },
1681
+ required: ["status", "data"],
1682
+ };
1683
+
1684
+ const consumerExpects = {
1685
+ type: "object",
1686
+ properties: {
1687
+ status: { type: "integer" },
1688
+ data: {
1689
+ type: "object",
1690
+ properties: {
1691
+ users: {
1692
+ type: "array",
1693
+ items: {
1694
+ type: "object",
1695
+ properties: {
1696
+ id: { type: "string" },
1697
+ email: { type: "string" },
1698
+ },
1699
+ required: ["id", "email"],
1700
+ },
1701
+ },
1702
+ },
1703
+ required: ["users"],
1704
+ },
1705
+ },
1706
+ required: ["data"],
1707
+ };
1708
+
1709
+ const result = checker.canConnect(apiResponse, consumerExpects);
1710
+ console.log(result.isSubset); // true ✅
1711
+ // L'API retourne plus de données que ce que le consommateur attend,
1712
+ // mais TOUTES les données requises sont présentes et du bon type.
1713
+ ```
1714
+
1715
+ ---
1716
+
1717
+ ### Union discriminée
1718
+
1719
+ Vérifier qu'une union discriminée (`oneOf` avec un champ discriminant) est compatible avec un schema d'entrée flexible.
1720
+
1721
+ ```ts
1722
+ const output = {
1723
+ oneOf: [
1724
+ {
1725
+ type: "object",
1726
+ properties: {
1727
+ type: { const: "success" },
1728
+ data: { type: "object" },
1729
+ },
1730
+ required: ["type", "data"],
1731
+ },
1732
+ {
1733
+ type: "object",
1734
+ properties: {
1735
+ type: { const: "error" },
1736
+ message: { type: "string" },
1737
+ code: { type: "integer" },
1738
+ },
1739
+ required: ["type", "message"],
1740
+ },
1741
+ ],
1742
+ };
1743
+
1744
+ const input = {
1745
+ type: "object",
1746
+ properties: {
1747
+ type: { type: "string" },
1748
+ },
1749
+ required: ["type"],
1750
+ };
1751
+
1752
+ // Chaque branche de l'union a un champ "type" de type string
1753
+ checker.isSubset(output, input); // true ✅
1754
+ ```
1755
+
1756
+ ---
1757
+
1758
+ ### Formulaire conditionnel
1759
+
1760
+ Valider qu'un formulaire rempli par l'utilisateur est compatible avec un schema conditionnel.
1761
+
1762
+ ```ts
1763
+ const formSchema = {
1764
+ type: "object",
1765
+ properties: {
1766
+ accountType: { type: "string", enum: ["personal", "business"] },
1767
+ email: { type: "string", format: "email" },
1768
+ companyName: { type: "string" },
1769
+ taxId: { type: "string" },
1770
+ firstName: { type: "string" },
1771
+ lastName: { type: "string" },
1772
+ },
1773
+ required: ["accountType", "email"],
1774
+ if: {
1775
+ properties: { accountType: { const: "business" } },
1776
+ required: ["accountType"],
1777
+ },
1778
+ then: { required: ["companyName", "taxId"] },
1779
+ else: { required: ["firstName", "lastName"] },
1780
+ };
1781
+
1782
+ // Output d'un formulaire "business" rempli
1783
+ const businessOutput = {
1784
+ type: "object",
1785
+ properties: {
1786
+ accountType: { const: "business", type: "string", enum: ["personal", "business"] },
1787
+ email: { type: "string", format: "email" },
1788
+ companyName: { type: "string", minLength: 1 },
1789
+ taxId: { type: "string", minLength: 1 },
1790
+ },
1791
+ required: ["accountType", "email", "companyName", "taxId"],
1792
+ additionalProperties: false,
1793
+ };
1794
+
1795
+ // Sans résolution, le if/then/else brut cause un faux négatif
1796
+ checker.isSubset(businessOutput, formSchema); // false ❌
1797
+
1798
+ // Avec résolution, le schéma conditionnel est aplati
1799
+ const result = checker.checkResolved(businessOutput, formSchema, {
1800
+ accountType: "business",
1801
+ });
1802
+ console.log(result.isSubset); // true ✅
1803
+ console.log(result.resolvedSup.branch); // "then"
1804
+
1805
+ // Output d'un formulaire "personal" rempli
1806
+ const personalOutput = {
1807
+ type: "object",
1808
+ properties: {
1809
+ accountType: { const: "personal", type: "string", enum: ["personal", "business"] },
1810
+ email: { type: "string", format: "email" },
1811
+ firstName: { type: "string", minLength: 1 },
1812
+ lastName: { type: "string", minLength: 1 },
1813
+ },
1814
+ required: ["accountType", "email", "firstName", "lastName"],
1815
+ additionalProperties: false,
1816
+ };
1817
+
1818
+ const personalResult = checker.checkResolved(personalOutput, formSchema, {
1819
+ accountType: "personal",
1820
+ });
1821
+ console.log(personalResult.isSubset); // true ✅
1822
+ console.log(personalResult.resolvedSup.branch); // "else"
1823
+ ```
1824
+
1825
+ ---
1826
+
1827
+ ## Types exportés
1828
+
1829
+ ```ts
1830
+ import type {
1831
+ SubsetResult,
1832
+ ConnectionResult,
1833
+ ResolvedConditionResult,
1834
+ SchemaDiff,
1835
+ BranchType,
1836
+ BranchResult,
1837
+ } from "json-schema-compatibility-checker";
1838
+ ```
1839
+
1840
+ ### `SchemaDiff`
1841
+
1842
+ ```ts
1843
+ interface SchemaDiff {
1844
+ /** Chemin JSON-path-like vers la divergence (ex: "properties.user.required") */
1845
+ path: string;
1846
+ /** Type de divergence */
1847
+ type: "added" | "removed" | "changed";
1848
+ /** Valeur dans le schema original (sub) */
1849
+ expected: unknown;
1850
+ /** Valeur dans le schema mergé (intersection) */
1851
+ actual: unknown;
1852
+ }
1853
+ ```
1854
+
1855
+ ### `SubsetResult`
1856
+
1857
+ ```ts
1858
+ interface SubsetResult {
1859
+ /** true si sub ⊆ sup */
1860
+ isSubset: boolean;
1861
+ /** Le schema résultant de l'intersection allOf(sub, sup), ou null si incompatible */
1862
+ merged: JSONSchema7Definition | null;
1863
+ /** Différences structurelles détectées entre sub et l'intersection */
1864
+ diffs: SchemaDiff[];
1865
+ }
1866
+ ```
1867
+
1868
+ ### `ConnectionResult`
1869
+
1870
+ ```ts
1871
+ interface ConnectionResult extends SubsetResult {
1872
+ /** Direction lisible du check */
1873
+ direction: string;
1874
+ }
1875
+ ```
1876
+
1877
+ ### `ResolvedConditionResult`
1878
+
1879
+ ```ts
1880
+ interface ResolvedConditionResult {
1881
+ /** Le schema avec les if/then/else résolus (aplatis) */
1882
+ resolved: JSONSchema7;
1883
+ /** La branche qui a été appliquée ("then" | "else" | null si pas de condition) */
1884
+ branch: "then" | "else" | null;
1885
+ /** Le discriminant utilisé pour résoudre */
1886
+ discriminant: Record<string, unknown>;
1887
+ }
1888
+ ```
1889
+
1890
+ ---
1891
+
1892
+ ## Limitations connues
1893
+
1894
+ ### 1. Cross-keyword constraints
1895
+
1896
+ La librairie utilise une comparaison **structurelle** : elle compare les mots-clés individuellement. Elle ne peut pas raisonner sur des relations entre mots-clés différents mais sémantiquement liés.
1897
+
1898
+ ```ts
1899
+ // Sémantiquement, {exclusiveMinimum: 5} ⊆ {minimum: 0} est VRAI (x>5 implique x≥0)
1900
+ // Mais le merge ajoute minimum:0, ce qui rend merged ≠ sub structurellement
1901
+ checker.isSubset(
1902
+ { type: "number", exclusiveMinimum: 5 },
1903
+ { type: "number", minimum: 0 }
1904
+ ); // false (faux négatif)
1905
+ ```
1906
+
1907
+ ### 2. `oneOf` — exclusivité non vérifiée
1908
+
1909
+ La librairie traite `oneOf` comme `anyOf` pour la vérification de sous-ensemble. L'exclusivité sémantique (exactement une branche doit matcher) n'est **pas** vérifiée.
1910
+
1911
+ ```ts
1912
+ const overlapping = {
1913
+ oneOf: [
1914
+ { type: "string", minLength: 1 }, // branches qui se chevauchent
1915
+ { type: "string", maxLength: 100 },
1916
+ ],
1917
+ };
1918
+ // En strict oneOf, "abc" matcherait les DEUX branches → rejeté
1919
+ // La librairie ne détecte pas ce chevauchement
1920
+ ```
1921
+
1922
+ ### 3. Patterns regex — approche probabiliste
1923
+
1924
+ La comparaison de patterns regex utilise un **échantillonnage** (200 samples par défaut). C'est une heuristique, pas une preuve formelle.
1925
+
1926
+ - **Faux négatifs** certains : si un counter-example est trouvé, l'exclusion est garantie
1927
+ - **Faux positifs** possibles : si tous les échantillons passent, ce n'est pas une preuve formelle (mais très improbable avec 200 samples)
1928
+ - Les patterns avec backreferences complexes peuvent poser problème
1929
+
1930
+ ### 4. `if/then/else` — nécessite des données discriminantes
1931
+
1932
+ Les schemas avec `if/then/else` ne peuvent pas être comparés directement via `isSubset` car le merge brut ajoute les mots-clés conditionnels. Il faut utiliser `checkResolved()` avec les données discriminantes.
1933
+
1934
+ ### 5. `$ref` — non supporté
1935
+
1936
+ Les références `$ref` ne sont pas résolues par la librairie. Il faut dé-référencer le schema avant de l'utiliser.
1937
+
1938
+ ### 6. `patternProperties` — support partiel
1939
+
1940
+ Les `patternProperties` sont normalisés et comparés structurellement, mais la comparaison sémantique des patterns comme clés n'est pas effectuée.
1941
+
1942
+ ---
1943
+
1944
+ ## Architecture interne
1945
+
1946
+ La librairie est organisée en modules spécialisés, orchestrés par la façade `JsonSchemaCompatibilityChecker` :
1947
+
1948
+ ```
1949
+ ┌──────────────────────────────────────────────────┐
1950
+ │ JsonSchemaCompatibilityChecker │
1951
+ │ (Façade) │
1952
+ ├──────────────────────────────────────────────────┤
1953
+ │ │
1954
+ │ ┌──────────────┐ ┌──────────────────────────┐ │
1955
+ │ │ Normalizer │ │ Condition Resolver │ │
1956
+ │ │ │ │ │ │
1957
+ │ │ - Infer type │ │ - Evaluate if │ │
1958
+ │ │ - enum→const │ │ - Merge then/else │ │
1959
+ │ │ - not(not(X))│ │ - Recurse in allOf │ │
1960
+ │ │ - Recurse │ │ - Nested properties │ │
1961
+ │ └──────────────┘ └──────────────────────────┘ │
1962
+ │ │
1963
+ │ ┌──────────────┐ ┌──────────────────────────┐ │
1964
+ │ │ Merge Engine │ │ Subset Checker │ │
1965
+ │ │ │ │ │ │
1966
+ │ │ - allOf merge│ │ - Atomic: A∩B ≡ A ? │ │
1967
+ │ │ - Conflict │ │ - Branched sub (anyOf) │ │
1968
+ │ │ detection │ │ - Branched sup (anyOf) │ │
1969
+ │ │ - Compare │ │ - evaluateNot │ │
1970
+ │ └──────────────┘ │ - stripNotFromSup │ │
1971
+ │ │ - stripPatternFromSup │ │
1972
+ │ ┌──────────────┐ └──────────────────────────┘ │
1973
+ │ │ Differ │ │
1974
+ │ │ │ ┌──────────────────────────┐ │
1975
+ │ │ - computeDiff│ │ Pattern Subset │ │
1976
+ │ │ - Recurse │ │ │ │
1977
+ │ │ - Properties │ │ - isPatternSubset │ │
1978
+ │ └──────────────┘ │ - arePatternsEquivalent │ │
1979
+ │ │ - isTrivialPattern │ │
1980
+ │ ┌──────────────┐ └──────────────────────────┘ │
1981
+ │ │ Formatter │ │
1982
+ │ │ │ ┌──────────────────────────┐ │
1983
+ │ │ - formatResult│ │ Format Validator │ │
1984
+ │ │ - Diff lines │ │ │ │
1985
+ │ └──────────────┘ │ - validateFormat │ │
1986
+ │ │ - isFormatSubset │ │
1987
+ │ │ - Format hierarchy │ │
1988
+ │ └──────────────────────────┘ │
1989
+ └──────────────────────────────────────────────────┘
1990
+ ```
1991
+
1992
+ ### Flux de vérification `isSubset(sub, sup)`
1993
+
1994
+ ```
1995
+ 1. Normalize(sub), Normalize(sup)
1996
+ 2. Detect branches (anyOf/oneOf) in sub and sup
1997
+ 3. For each branch combination:
1998
+ a. evaluateNot() — pre-check not compatibility
1999
+ b. stripNotFromSup() — remove compatible not constraints
2000
+ c. stripPatternFromSup() — handle pattern inclusion via sampling
2001
+ d. engine.merge(sub, sup) — compute intersection
2002
+ e. normalize(merged)
2003
+ f. engine.isEqual(normalized_sub, normalized_merged) ?
2004
+ → true: sub ⊆ sup ✅
2005
+ → false: compute diffs, sub ⊄ sup ❌
2006
+ ```
2007
+
2008
+ ### Dépendances
2009
+
2010
+ | Package | Usage |
2011
+ |---|---|
2012
+ | `@x0k/json-schema-merge` | Merge engine pour `allOf` resolution |
2013
+ | `lodash` | Utilitaires (isEqual, mapValues, union, etc.) |
2014
+ | `class-validator` | Validation des formats (email, URL, UUID, etc.) |
2015
+ | `randexp` | Génération de strings pour le sampling de patterns |
2016
+
2017
+ ---
2018
+
2019
+ ## Licence
2020
+
2021
+ Projet privé.