flowspec2 1.0.0__py3-none-any.whl

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 (92) hide show
  1. flowspec2/__init__.py +105 -0
  2. flowspec2/authoring/__init__.py +275 -0
  3. flowspec2/authoring/authoring-evidence-signature.schema.json +34 -0
  4. flowspec2/authoring/authoring-evidence.schema.json +455 -0
  5. flowspec2/authoring/authoring-operational-evidence.schema.json +160 -0
  6. flowspec2/authoring/benchmark.py +1409 -0
  7. flowspec2/authoring/corpus/await_correction.case.json +220 -0
  8. flowspec2/authoring/corpus/flowspec2.ctk.json +769 -0
  9. flowspec2/authoring/corpus/gated_derive.case.json +100 -0
  10. flowspec2/authoring/corpus/linear.case.json +55 -0
  11. flowspec2/authoring/corpus/manifest.json +31 -0
  12. flowspec2/authoring/corpus/subflow.case.json +66 -0
  13. flowspec2/authoring/corpus/terminal.case.json +94 -0
  14. flowspec2/authoring/corpus.py +307 -0
  15. flowspec2/authoring/ctk.py +836 -0
  16. flowspec2/authoring/detached_signature.py +120 -0
  17. flowspec2/authoring/evidence.py +311 -0
  18. flowspec2/authoring/evidence_signature.py +187 -0
  19. flowspec2/authoring/evidence_verification.py +229 -0
  20. flowspec2/authoring/gemini.py +215 -0
  21. flowspec2/authoring/operational-corpus.json +61 -0
  22. flowspec2/authoring/operational.py +956 -0
  23. flowspec2/authoring/operational_providers.py +167 -0
  24. flowspec2/authoring/presentation_review.py +1013 -0
  25. flowspec2/authoring/presentation_review_signature.py +305 -0
  26. flowspec2/authoring/projection.py +299 -0
  27. flowspec2/authoring/provider_prompt.py +83 -0
  28. flowspec2/backends/__init__.py +82 -0
  29. flowspec2/backends/http.py +189 -0
  30. flowspec2/checker.py +238 -0
  31. flowspec2/cli.py +789 -0
  32. flowspec2/cli_parser.py +345 -0
  33. flowspec2/clock.py +23 -0
  34. flowspec2/compat/__init__.py +47 -0
  35. flowspec2/compat/models.py +82 -0
  36. flowspec2/compat/open_workflow.py +616 -0
  37. flowspec2/compat/rasa.py +19 -0
  38. flowspec2/compat/rasa_export.py +876 -0
  39. flowspec2/compat/rasa_import.py +992 -0
  40. flowspec2/compat/rasa_shared.py +270 -0
  41. flowspec2/compat/schemas/open-workflow-conversation-1.schema.json +138 -0
  42. flowspec2/compat/schemas/vendor/open-workflow-1.0.3.LICENSE +201 -0
  43. flowspec2/compat/schemas/vendor/open-workflow-1.0.3.provenance.json +13 -0
  44. flowspec2/compat/schemas/vendor/open-workflow-1.0.3.workflow.yaml +1956 -0
  45. flowspec2/compat/tool_profiles.py +149 -0
  46. flowspec2/compat/yaml.py +147 -0
  47. flowspec2/compiler.py +957 -0
  48. flowspec2/compiler_contracts.py +17 -0
  49. flowspec2/compiler_resume_contracts.py +594 -0
  50. flowspec2/compiler_schema_relations.py +1830 -0
  51. flowspec2/compiler_tool_contracts.py +245 -0
  52. flowspec2/compiler_value_contracts.py +447 -0
  53. flowspec2/derive.py +32 -0
  54. flowspec2/diagnostics.py +94 -0
  55. flowspec2/domains.py +489 -0
  56. flowspec2/experimental/__init__.py +57 -0
  57. flowspec2/experimental/flowspec-3-draft.schema.json +923 -0
  58. flowspec2/experimental/v3-preview-loss-policy.json +161 -0
  59. flowspec2/experimental/v3_lowering.py +928 -0
  60. flowspec2/experimental/v3_preview.py +2460 -0
  61. flowspec2/flowspec-2.schema.json +635 -0
  62. flowspec2/interactive.py +300 -0
  63. flowspec2/ir.py +1459 -0
  64. flowspec2/json_codec.py +120 -0
  65. flowspec2/llm.py +366 -0
  66. flowspec2/models.py +121 -0
  67. flowspec2/nodes.py +1537 -0
  68. flowspec2/observability.py +151 -0
  69. flowspec2/predicates.py +89 -0
  70. flowspec2/profiles.py +118 -0
  71. flowspec2/py.typed +0 -0
  72. flowspec2/runtime.py +1059 -0
  73. flowspec2/schema.py +54 -0
  74. flowspec2/schema_contracts.py +203 -0
  75. flowspec2/semantic_derive_contracts.py +530 -0
  76. flowspec2/semantic_path_contracts.py +528 -0
  77. flowspec2/semantic_predicate_contracts.py +355 -0
  78. flowspec2/semantic_schema_contracts.py +137 -0
  79. flowspec2/semantic_source_contracts.py +21 -0
  80. flowspec2/semantic_state_contracts.py +450 -0
  81. flowspec2/semantic_support.py +40 -0
  82. flowspec2/semantics.py +410 -0
  83. flowspec2/state_migration.py +1034 -0
  84. flowspec2/subflows/__init__.py +1117 -0
  85. flowspec2/subflows/address.py +328 -0
  86. flowspec2/subflows/identification.py +1031 -0
  87. flowspec2/tools.py +1154 -0
  88. flowspec2-1.0.0.dist-info/METADATA +482 -0
  89. flowspec2-1.0.0.dist-info/RECORD +92 -0
  90. flowspec2-1.0.0.dist-info/WHEEL +4 -0
  91. flowspec2-1.0.0.dist-info/entry_points.txt +2 -0
  92. flowspec2-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,1830 @@
1
+ """JSON Schema relation and path proofs used by compiler contract validation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from collections.abc import Mapping
8
+ from decimal import ROUND_CEILING, ROUND_FLOOR, Decimal
9
+ from typing import Any, Final, cast
10
+
11
+ from jsonschema import Draft202012Validator, FormatChecker
12
+
13
+ from .schema_contracts import schema_reference_target
14
+
15
+ CONTRACT_INVALID: Final[int] = -1
16
+ CONTRACT_UNKNOWN: Final[int] = 0
17
+ CONTRACT_VALID: Final[int] = 1
18
+ _VALUE_TYPE_ATOMS: Final[frozenset[str]] = frozenset(
19
+ {
20
+ "array",
21
+ "boolean",
22
+ "integer",
23
+ "non_integer_number",
24
+ "null",
25
+ "object",
26
+ "string",
27
+ }
28
+ )
29
+ _NumericBoundary = tuple[Decimal, bool]
30
+ _NumericInterval = tuple[_NumericBoundary | None, _NumericBoundary | None]
31
+ _SCHEMA_ANNOTATION_KEYWORDS: Final[frozenset[str]] = frozenset(
32
+ {
33
+ "$comment",
34
+ "default",
35
+ "deprecated",
36
+ "description",
37
+ "examples",
38
+ "readOnly",
39
+ "title",
40
+ "writeOnly",
41
+ }
42
+ )
43
+
44
+
45
+ def mutable_contract_json(contract_fragment: Any) -> Any:
46
+ if isinstance(contract_fragment, Mapping):
47
+ return {
48
+ property_name: mutable_contract_json(property_value)
49
+ for property_name, property_value in contract_fragment.items()
50
+ }
51
+ if isinstance(contract_fragment, (list, tuple)):
52
+ return [mutable_contract_json(element) for element in contract_fragment]
53
+ return contract_fragment
54
+
55
+
56
+ def standalone_schema(
57
+ schema: Any,
58
+ root_schema: Any,
59
+ reference_stack: tuple[str, ...] = (),
60
+ ) -> Any:
61
+ """Inline safe local references so contracts from different roots can be composed."""
62
+
63
+ if isinstance(schema, Mapping):
64
+ standalone_mapping = {
65
+ keyword: standalone_schema(keyword_value, root_schema, reference_stack)
66
+ for keyword, keyword_value in schema.items()
67
+ if keyword != "$ref"
68
+ }
69
+ if (reference := schema.get("$ref")) is None:
70
+ return standalone_mapping
71
+ reference_text = cast(str, reference)
72
+ if reference_text in reference_stack:
73
+ raise ValueError("cyclic local schema reference cannot be materialized")
74
+ referenced_schema = standalone_schema(
75
+ schema_reference_target(root_schema, reference_text),
76
+ root_schema,
77
+ (*reference_stack, reference_text),
78
+ )
79
+ return combined_contract([standalone_mapping or True, referenced_schema])
80
+ if isinstance(schema, (list, tuple)):
81
+ return [standalone_schema(element, root_schema, reference_stack) for element in schema]
82
+ return schema
83
+
84
+
85
+ def schema_accepts_known_instance(
86
+ schema: Any,
87
+ root_schema: Any,
88
+ instance: object,
89
+ ) -> bool:
90
+ mutable_root_schema = mutable_contract_json(root_schema)
91
+ validator = Draft202012Validator(
92
+ mutable_root_schema,
93
+ format_checker=FormatChecker(),
94
+ )
95
+ return validator.evolve(schema=mutable_contract_json(schema)).is_valid(cast(Any, instance))
96
+
97
+
98
+ def combined_contract(contract_fragments: list[Any], keyword: str = "allOf") -> Any:
99
+ effective_fragments = [
100
+ contract_fragment
101
+ for contract_fragment in contract_fragments
102
+ if contract_fragment is not True
103
+ ]
104
+ if not effective_fragments:
105
+ return True
106
+ if len(effective_fragments) == 1:
107
+ return effective_fragments[0]
108
+ return {keyword: effective_fragments}
109
+
110
+
111
+ def schema_property_contract(
112
+ schema: Any,
113
+ property_name: str,
114
+ root_schema: Any,
115
+ bound_keys: frozenset[str],
116
+ reference_stack: tuple[str, ...] = (),
117
+ ) -> Any:
118
+ if schema is False:
119
+ return False
120
+ if schema is True or not isinstance(schema, Mapping):
121
+ return True
122
+
123
+ property_contracts: list[Any] = []
124
+ properties = schema.get("properties")
125
+ pattern_properties = schema.get("patternProperties")
126
+ has_property_dispatch = (
127
+ isinstance(properties, Mapping)
128
+ or isinstance(pattern_properties, Mapping)
129
+ or "additionalProperties" in schema
130
+ )
131
+ if has_property_dispatch:
132
+ matching_contracts: list[Any] = []
133
+ if isinstance(properties, Mapping) and property_name in properties:
134
+ matching_contracts.append(properties[property_name])
135
+ if isinstance(pattern_properties, Mapping):
136
+ matching_contracts.extend(
137
+ property_contract
138
+ for property_pattern, property_contract in pattern_properties.items()
139
+ if re.search(property_pattern, property_name) is not None
140
+ )
141
+ if not matching_contracts:
142
+ matching_contracts.append(schema.get("additionalProperties", True))
143
+ property_contracts.append(combined_contract(matching_contracts))
144
+
145
+ if (reference := schema.get("$ref")) is not None:
146
+ reference_text = cast(str, reference)
147
+ property_contracts.append(
148
+ True
149
+ if reference_text in reference_stack
150
+ else schema_property_contract(
151
+ schema_reference_target(root_schema, reference_text),
152
+ property_name,
153
+ root_schema,
154
+ bound_keys,
155
+ (*reference_stack, reference_text),
156
+ )
157
+ )
158
+ if isinstance((all_of := schema.get("allOf")), (list, tuple)):
159
+ property_contracts.extend(
160
+ schema_property_contract(
161
+ branch,
162
+ property_name,
163
+ root_schema,
164
+ bound_keys,
165
+ reference_stack,
166
+ )
167
+ for branch in all_of
168
+ )
169
+ for union_keyword in ("anyOf", "oneOf"):
170
+ branches = schema.get(union_keyword)
171
+ if isinstance(branches, (list, tuple)):
172
+ property_contracts.append(
173
+ combined_contract(
174
+ [
175
+ schema_property_contract(
176
+ branch,
177
+ property_name,
178
+ root_schema,
179
+ bound_keys,
180
+ reference_stack,
181
+ )
182
+ for branch in branches
183
+ ],
184
+ "anyOf",
185
+ )
186
+ )
187
+ if "if" in schema:
188
+ property_contracts.append(
189
+ combined_contract(
190
+ [
191
+ schema_property_contract(
192
+ schema.get("then", True),
193
+ property_name,
194
+ root_schema,
195
+ bound_keys,
196
+ reference_stack,
197
+ ),
198
+ schema_property_contract(
199
+ schema.get("else", True),
200
+ property_name,
201
+ root_schema,
202
+ bound_keys,
203
+ reference_stack,
204
+ ),
205
+ ],
206
+ "anyOf",
207
+ )
208
+ )
209
+ dependent_schemas = schema.get("dependentSchemas")
210
+ if isinstance(dependent_schemas, Mapping):
211
+ property_contracts.extend(
212
+ schema_property_contract(
213
+ dependent_schema,
214
+ property_name,
215
+ root_schema,
216
+ bound_keys,
217
+ reference_stack,
218
+ )
219
+ for trigger_property, dependent_schema in dependent_schemas.items()
220
+ if trigger_property in bound_keys
221
+ )
222
+ return combined_contract(property_contracts)
223
+
224
+
225
+ def _instance_type_atom(schema_instance: Any) -> str:
226
+ if isinstance(schema_instance, Mapping):
227
+ return "object"
228
+ if isinstance(schema_instance, list):
229
+ return "array"
230
+ if isinstance(schema_instance, bool):
231
+ return "boolean"
232
+ if schema_instance is None:
233
+ return "null"
234
+ if isinstance(schema_instance, int) or (
235
+ isinstance(schema_instance, float) and schema_instance.is_integer()
236
+ ):
237
+ return "integer"
238
+ if isinstance(schema_instance, float):
239
+ return "non_integer_number"
240
+ return "string"
241
+
242
+
243
+ def _atoms_for_schema_type(schema_type: str) -> frozenset[str]:
244
+ if schema_type == "number":
245
+ return frozenset({"integer", "non_integer_number"})
246
+ if schema_type == "integer":
247
+ return frozenset({"integer"})
248
+ return frozenset({schema_type})
249
+
250
+
251
+ def schema_type_atoms(
252
+ schema: Any,
253
+ root_schema: Any,
254
+ reference_stack: tuple[str, ...] = (),
255
+ ) -> frozenset[str]:
256
+ if schema is False:
257
+ return frozenset()
258
+ if schema is True or not isinstance(schema, Mapping):
259
+ return _VALUE_TYPE_ATOMS
260
+
261
+ possible_atoms = _VALUE_TYPE_ATOMS
262
+ if (schema_types := schema.get("type")) is not None:
263
+ declared_types = (schema_types,) if isinstance(schema_types, str) else tuple(schema_types)
264
+ possible_atoms &= frozenset().union(
265
+ *(_atoms_for_schema_type(schema_type) for schema_type in declared_types)
266
+ )
267
+ if "const" in schema:
268
+ possible_atoms &= frozenset({_instance_type_atom(schema["const"])})
269
+ if isinstance((enum_members := schema.get("enum")), (list, tuple)):
270
+ possible_atoms &= frozenset(
271
+ _instance_type_atom(enum_member) for enum_member in enum_members
272
+ )
273
+ if (reference := schema.get("$ref")) is not None:
274
+ reference_text = cast(str, reference)
275
+ if reference_text not in reference_stack:
276
+ possible_atoms &= schema_type_atoms(
277
+ schema_reference_target(root_schema, reference_text),
278
+ root_schema,
279
+ (*reference_stack, reference_text),
280
+ )
281
+ if isinstance((all_of := schema.get("allOf")), (list, tuple)):
282
+ for branch in all_of:
283
+ possible_atoms &= schema_type_atoms(branch, root_schema, reference_stack)
284
+ for union_keyword in ("anyOf", "oneOf"):
285
+ branches = schema.get(union_keyword)
286
+ if isinstance(branches, (list, tuple)):
287
+ branch_atoms = frozenset().union(
288
+ *(schema_type_atoms(branch, root_schema, reference_stack) for branch in branches)
289
+ )
290
+ possible_atoms &= branch_atoms
291
+ if "if" in schema:
292
+ conditional_atoms = schema_type_atoms(
293
+ schema.get("then", True), root_schema, reference_stack
294
+ ) | schema_type_atoms(schema.get("else", True), root_schema, reference_stack)
295
+ possible_atoms &= conditional_atoms
296
+ return possible_atoms
297
+
298
+
299
+ def canonical_contract_instance(schema_instance: Any) -> str:
300
+ return json.dumps(
301
+ schema_instance,
302
+ allow_nan=False,
303
+ ensure_ascii=False,
304
+ separators=(",", ":"),
305
+ sort_keys=True,
306
+ )
307
+
308
+
309
+ def finite_schema_instances(
310
+ schema: Any,
311
+ root_schema: Any,
312
+ reference_stack: tuple[str, ...] = (),
313
+ ) -> dict[str, Any] | None:
314
+ if schema is False:
315
+ return {}
316
+ if schema is True or not isinstance(schema, Mapping):
317
+ return None
318
+
319
+ finite_instances: dict[str, Any] | None = None
320
+
321
+ def intersect_instances(candidate_instances: dict[str, Any]) -> None:
322
+ nonlocal finite_instances
323
+ finite_instances = (
324
+ candidate_instances
325
+ if finite_instances is None
326
+ else {
327
+ serialized_instance: schema_instance
328
+ for serialized_instance, schema_instance in finite_instances.items()
329
+ if serialized_instance in candidate_instances
330
+ }
331
+ )
332
+
333
+ if "const" in schema:
334
+ constant_instance = schema["const"]
335
+ intersect_instances({canonical_contract_instance(constant_instance): constant_instance})
336
+ if isinstance((enum_members := schema.get("enum")), (list, tuple)):
337
+ intersect_instances(
338
+ {canonical_contract_instance(enum_member): enum_member for enum_member in enum_members}
339
+ )
340
+ if (schema_types := schema.get("type")) is not None:
341
+ declared_types = {schema_types} if isinstance(schema_types, str) else set(schema_types)
342
+ if declared_types and declared_types <= {"boolean", "null"}:
343
+ finite_type_instances = [
344
+ *([None] if "null" in declared_types else []),
345
+ *([False, True] if "boolean" in declared_types else []),
346
+ ]
347
+ intersect_instances(
348
+ {
349
+ canonical_contract_instance(type_instance): type_instance
350
+ for type_instance in finite_type_instances
351
+ }
352
+ )
353
+ if (reference := schema.get("$ref")) is not None:
354
+ reference_text = cast(str, reference)
355
+ if (
356
+ reference_text not in reference_stack
357
+ and (
358
+ referenced_instances := finite_schema_instances(
359
+ schema_reference_target(root_schema, reference_text),
360
+ root_schema,
361
+ (*reference_stack, reference_text),
362
+ )
363
+ )
364
+ is not None
365
+ ):
366
+ intersect_instances(referenced_instances)
367
+ if isinstance((all_of := schema.get("allOf")), (list, tuple)):
368
+ for branch in all_of:
369
+ if (
370
+ branch_instances := finite_schema_instances(branch, root_schema, reference_stack)
371
+ ) is not None:
372
+ intersect_instances(branch_instances)
373
+ for union_keyword in ("anyOf", "oneOf"):
374
+ branches = schema.get(union_keyword)
375
+ if not isinstance(branches, (list, tuple)):
376
+ continue
377
+ branch_instance_sets = [
378
+ finite_schema_instances(branch, root_schema, reference_stack) for branch in branches
379
+ ]
380
+ if all(branch_instances is not None for branch_instances in branch_instance_sets):
381
+ intersect_instances(
382
+ {
383
+ serialized_instance: schema_instance
384
+ for branch_instances in branch_instance_sets
385
+ if branch_instances is not None
386
+ for serialized_instance, schema_instance in branch_instances.items()
387
+ }
388
+ )
389
+ if "if" in schema:
390
+ conditional_instance_sets = [
391
+ finite_schema_instances(
392
+ schema.get(conditional_keyword, True),
393
+ root_schema,
394
+ reference_stack,
395
+ )
396
+ for conditional_keyword in ("then", "else")
397
+ ]
398
+ if all(
399
+ conditional_instances is not None for conditional_instances in conditional_instance_sets
400
+ ):
401
+ intersect_instances(
402
+ {
403
+ serialized_instance: schema_instance
404
+ for conditional_instances in conditional_instance_sets
405
+ if conditional_instances is not None
406
+ for serialized_instance, schema_instance in conditional_instances.items()
407
+ }
408
+ )
409
+
410
+ if finite_instances is None:
411
+ return None
412
+ return {
413
+ serialized_instance: schema_instance
414
+ for serialized_instance, schema_instance in finite_instances.items()
415
+ if schema_accepts_known_instance(schema, root_schema, schema_instance)
416
+ }
417
+
418
+
419
+ def _stronger_lower_boundary(
420
+ current_boundary: _NumericBoundary | None,
421
+ candidate_boundary: _NumericBoundary,
422
+ ) -> _NumericBoundary:
423
+ if current_boundary is None or candidate_boundary[0] > current_boundary[0]:
424
+ return candidate_boundary
425
+ if candidate_boundary[0] == current_boundary[0]:
426
+ return candidate_boundary[0], current_boundary[1] and candidate_boundary[1]
427
+ return current_boundary
428
+
429
+
430
+ def _stronger_upper_boundary(
431
+ current_boundary: _NumericBoundary | None,
432
+ candidate_boundary: _NumericBoundary,
433
+ ) -> _NumericBoundary:
434
+ if current_boundary is None or candidate_boundary[0] < current_boundary[0]:
435
+ return candidate_boundary
436
+ if candidate_boundary[0] == current_boundary[0]:
437
+ return candidate_boundary[0], current_boundary[1] and candidate_boundary[1]
438
+ return current_boundary
439
+
440
+
441
+ def _schema_numeric_interval(
442
+ schema: Any,
443
+ root_schema: Any,
444
+ reference_stack: tuple[str, ...] = (),
445
+ ) -> _NumericInterval:
446
+ if not isinstance(schema, Mapping):
447
+ return None, None
448
+ lower_boundary: _NumericBoundary | None = None
449
+ upper_boundary: _NumericBoundary | None = None
450
+ if isinstance((minimum := schema.get("minimum")), (int, float)):
451
+ lower_boundary = _stronger_lower_boundary(lower_boundary, (Decimal(str(minimum)), True))
452
+ if isinstance((exclusive_minimum := schema.get("exclusiveMinimum")), (int, float)):
453
+ lower_boundary = _stronger_lower_boundary(
454
+ lower_boundary, (Decimal(str(exclusive_minimum)), False)
455
+ )
456
+ if isinstance((maximum := schema.get("maximum")), (int, float)):
457
+ upper_boundary = _stronger_upper_boundary(upper_boundary, (Decimal(str(maximum)), True))
458
+ if isinstance((exclusive_maximum := schema.get("exclusiveMaximum")), (int, float)):
459
+ upper_boundary = _stronger_upper_boundary(
460
+ upper_boundary, (Decimal(str(exclusive_maximum)), False)
461
+ )
462
+
463
+ nested_schemas: list[Any] = []
464
+ if (reference := schema.get("$ref")) is not None:
465
+ reference_text = cast(str, reference)
466
+ if reference_text not in reference_stack:
467
+ nested_schemas.append(schema_reference_target(root_schema, reference_text))
468
+ reference_stack = (*reference_stack, reference_text)
469
+ if isinstance((all_of := schema.get("allOf")), (list, tuple)):
470
+ nested_schemas.extend(all_of)
471
+ for nested_schema in nested_schemas:
472
+ nested_lower, nested_upper = _schema_numeric_interval(
473
+ nested_schema, root_schema, reference_stack
474
+ )
475
+ if nested_lower is not None:
476
+ lower_boundary = _stronger_lower_boundary(lower_boundary, nested_lower)
477
+ if nested_upper is not None:
478
+ upper_boundary = _stronger_upper_boundary(upper_boundary, nested_upper)
479
+ return lower_boundary, upper_boundary
480
+
481
+
482
+ def _numeric_intervals_overlap(
483
+ source_interval: _NumericInterval,
484
+ parameter_interval: _NumericInterval,
485
+ *,
486
+ integer_only: bool,
487
+ ) -> bool:
488
+ lower_boundary: _NumericBoundary | None = None
489
+ upper_boundary: _NumericBoundary | None = None
490
+ for candidate_lower in (source_interval[0], parameter_interval[0]):
491
+ if candidate_lower is not None:
492
+ lower_boundary = _stronger_lower_boundary(lower_boundary, candidate_lower)
493
+ for candidate_upper in (source_interval[1], parameter_interval[1]):
494
+ if candidate_upper is not None:
495
+ upper_boundary = _stronger_upper_boundary(upper_boundary, candidate_upper)
496
+ if lower_boundary is not None and upper_boundary is not None:
497
+ if lower_boundary[0] > upper_boundary[0]:
498
+ return False
499
+ if lower_boundary[0] == upper_boundary[0] and not (lower_boundary[1] and upper_boundary[1]):
500
+ return False
501
+ if not integer_only:
502
+ return True
503
+
504
+ minimum_integer = (
505
+ None
506
+ if lower_boundary is None
507
+ else int(lower_boundary[0].to_integral_value(rounding=ROUND_CEILING))
508
+ + (
509
+ 1
510
+ if not lower_boundary[1] and lower_boundary[0] == lower_boundary[0].to_integral_value()
511
+ else 0
512
+ )
513
+ )
514
+ maximum_integer = (
515
+ None
516
+ if upper_boundary is None
517
+ else int(upper_boundary[0].to_integral_value(rounding=ROUND_FLOOR))
518
+ - (
519
+ 1
520
+ if not upper_boundary[1] and upper_boundary[0] == upper_boundary[0].to_integral_value()
521
+ else 0
522
+ )
523
+ )
524
+ return minimum_integer is None or maximum_integer is None or minimum_integer <= maximum_integer
525
+
526
+
527
+ def _schemas_are_clearly_disjoint(
528
+ source_schema: Any,
529
+ source_root_schema: Any,
530
+ parameter_schema: Any,
531
+ parameter_root_schema: Any,
532
+ ) -> bool:
533
+ source_instances = finite_schema_instances(source_schema, source_root_schema)
534
+ if source_instances is not None:
535
+ return not any(
536
+ schema_accepts_known_instance(parameter_schema, parameter_root_schema, source_instance)
537
+ for source_instance in source_instances.values()
538
+ )
539
+ parameter_instances = finite_schema_instances(parameter_schema, parameter_root_schema)
540
+ if parameter_instances is not None:
541
+ return not any(
542
+ schema_accepts_known_instance(source_schema, source_root_schema, parameter_instance)
543
+ for parameter_instance in parameter_instances.values()
544
+ )
545
+
546
+ shared_type_atoms = schema_type_atoms(source_schema, source_root_schema) & schema_type_atoms(
547
+ parameter_schema, parameter_root_schema
548
+ )
549
+ if not shared_type_atoms:
550
+ return True
551
+ numeric_atoms = frozenset({"integer", "non_integer_number"})
552
+ if shared_type_atoms - numeric_atoms:
553
+ return False
554
+ return not _numeric_intervals_overlap(
555
+ _schema_numeric_interval(source_schema, source_root_schema),
556
+ _schema_numeric_interval(parameter_schema, parameter_root_schema),
557
+ integer_only=shared_type_atoms == {"integer"},
558
+ )
559
+
560
+
561
+ def _schema_assertion_json(schema: Any) -> Any:
562
+ if isinstance(schema, Mapping):
563
+ return {
564
+ keyword: _schema_assertion_json(keyword_value)
565
+ for keyword, keyword_value in schema.items()
566
+ if keyword not in _SCHEMA_ANNOTATION_KEYWORDS
567
+ }
568
+ if isinstance(schema, (list, tuple)):
569
+ return [_schema_assertion_json(element) for element in schema]
570
+ return schema
571
+
572
+
573
+ def schema_without_keyword(schema: Mapping[str, Any], keyword: str) -> Any:
574
+ remaining_schema = {
575
+ schema_keyword: schema_value
576
+ for schema_keyword, schema_value in schema.items()
577
+ if schema_keyword != keyword
578
+ }
579
+ return remaining_schema or True
580
+
581
+
582
+ def _schema_length_bound(
583
+ schema: Any,
584
+ root_schema: Any,
585
+ keyword: str,
586
+ reference_stack: tuple[str, ...] = (),
587
+ ) -> int | None:
588
+ if not isinstance(schema, Mapping):
589
+ return None
590
+ candidate_bounds: list[int | None] = [
591
+ cast(int, schema[keyword])
592
+ if isinstance(schema.get(keyword), int) and not isinstance(schema[keyword], bool)
593
+ else None
594
+ ]
595
+ if (reference := schema.get("$ref")) is not None:
596
+ reference_text = cast(str, reference)
597
+ if reference_text not in reference_stack:
598
+ candidate_bounds.append(
599
+ _schema_length_bound(
600
+ schema_reference_target(root_schema, reference_text),
601
+ root_schema,
602
+ keyword,
603
+ (*reference_stack, reference_text),
604
+ )
605
+ )
606
+ if isinstance((all_of := schema.get("allOf")), (list, tuple)):
607
+ candidate_bounds.extend(
608
+ _schema_length_bound(branch, root_schema, keyword, reference_stack) for branch in all_of
609
+ )
610
+ concrete_bounds = [bound for bound in candidate_bounds if bound is not None]
611
+ if not concrete_bounds:
612
+ return None
613
+ return max(concrete_bounds) if keyword == "minLength" else min(concrete_bounds)
614
+
615
+
616
+ def _schema_object_count_bound(
617
+ schema: Any,
618
+ root_schema: Any,
619
+ keyword: str,
620
+ reference_stack: tuple[str, ...] = (),
621
+ ) -> int | None:
622
+ if not isinstance(schema, Mapping):
623
+ return None
624
+ candidate_bounds: list[int | None] = [
625
+ cast(int, schema[keyword])
626
+ if isinstance(schema.get(keyword), int) and not isinstance(schema[keyword], bool)
627
+ else None
628
+ ]
629
+ if keyword == "minProperties":
630
+ candidate_bounds.append(
631
+ len(
632
+ {
633
+ property_name
634
+ for property_name in schema.get("required", ())
635
+ if isinstance(property_name, str)
636
+ }
637
+ )
638
+ )
639
+ if (reference := schema.get("$ref")) is not None:
640
+ reference_text = cast(str, reference)
641
+ if reference_text not in reference_stack:
642
+ candidate_bounds.append(
643
+ _schema_object_count_bound(
644
+ schema_reference_target(root_schema, reference_text),
645
+ root_schema,
646
+ keyword,
647
+ (*reference_stack, reference_text),
648
+ )
649
+ )
650
+ if isinstance((all_of := schema.get("allOf")), (list, tuple)):
651
+ candidate_bounds.extend(
652
+ _schema_object_count_bound(branch, root_schema, keyword, reference_stack)
653
+ for branch in all_of
654
+ )
655
+ concrete_bounds = [bound for bound in candidate_bounds if bound is not None]
656
+ if not concrete_bounds:
657
+ return None
658
+ return max(concrete_bounds) if keyword == "minProperties" else min(concrete_bounds)
659
+
660
+
661
+ def _schema_string_assertions(
662
+ schema: Any,
663
+ root_schema: Any,
664
+ keyword: str,
665
+ reference_stack: tuple[str, ...] = (),
666
+ ) -> frozenset[str]:
667
+ if not isinstance(schema, Mapping):
668
+ return frozenset()
669
+ assertions = {assertion for assertion in (schema.get(keyword),) if isinstance(assertion, str)}
670
+ if (reference := schema.get("$ref")) is not None:
671
+ reference_text = cast(str, reference)
672
+ if reference_text not in reference_stack:
673
+ assertions.update(
674
+ _schema_string_assertions(
675
+ schema_reference_target(root_schema, reference_text),
676
+ root_schema,
677
+ keyword,
678
+ (*reference_stack, reference_text),
679
+ )
680
+ )
681
+ if isinstance((all_of := schema.get("allOf")), (list, tuple)):
682
+ for branch in all_of:
683
+ assertions.update(
684
+ _schema_string_assertions(branch, root_schema, keyword, reference_stack)
685
+ )
686
+ return frozenset(assertions)
687
+
688
+
689
+ def _lower_boundary_contains(
690
+ source_boundary: _NumericBoundary | None,
691
+ target_boundary: _NumericBoundary | None,
692
+ ) -> bool:
693
+ if target_boundary is None:
694
+ return True
695
+ if source_boundary is None or source_boundary[0] < target_boundary[0]:
696
+ return False
697
+ if source_boundary[0] > target_boundary[0]:
698
+ return True
699
+ return target_boundary[1] or not source_boundary[1]
700
+
701
+
702
+ def _upper_boundary_contains(
703
+ source_boundary: _NumericBoundary | None,
704
+ target_boundary: _NumericBoundary | None,
705
+ ) -> bool:
706
+ if target_boundary is None:
707
+ return True
708
+ if source_boundary is None or source_boundary[0] > target_boundary[0]:
709
+ return False
710
+ if source_boundary[0] < target_boundary[0]:
711
+ return True
712
+ return target_boundary[1] or not source_boundary[1]
713
+
714
+
715
+ def _multiple_of_is_subset(source_multiple: Any, target_multiple: Any) -> bool:
716
+ if not isinstance(target_multiple, (int, float)) or isinstance(target_multiple, bool):
717
+ return True
718
+ if not isinstance(source_multiple, (int, float)) or isinstance(source_multiple, bool):
719
+ return False
720
+ source_decimal = Decimal(str(source_multiple))
721
+ target_decimal = Decimal(str(target_multiple))
722
+ return target_decimal > 0 and source_decimal > 0 and source_decimal % target_decimal == 0
723
+
724
+
725
+ def schema_is_proven_subset(
726
+ source_schema: Any,
727
+ source_root_schema: Any,
728
+ target_schema: Any,
729
+ target_root_schema: Any,
730
+ ) -> bool:
731
+ """Conservatively prove JSON-Schema language inclusion for flow value contracts."""
732
+
733
+ if target_schema is True or source_schema is False:
734
+ return True
735
+ if target_schema is False:
736
+ return finite_schema_instances(source_schema, source_root_schema) == {}
737
+ if _schema_assertion_json(source_schema) == _schema_assertion_json(target_schema):
738
+ return True
739
+
740
+ source_instances = finite_schema_instances(source_schema, source_root_schema)
741
+ if source_instances is not None:
742
+ return all(
743
+ schema_accepts_known_instance(target_schema, target_root_schema, source_instance)
744
+ for source_instance in source_instances.values()
745
+ )
746
+ if not isinstance(source_schema, Mapping) or not isinstance(target_schema, Mapping):
747
+ return False
748
+
749
+ if (source_reference := source_schema.get("$ref")) is not None:
750
+ source_without_reference = schema_without_keyword(source_schema, "$ref")
751
+ if source_without_reference is True:
752
+ return schema_is_proven_subset(
753
+ schema_reference_target(source_root_schema, cast(str, source_reference)),
754
+ source_root_schema,
755
+ target_schema,
756
+ target_root_schema,
757
+ )
758
+
759
+ for union_keyword in ("anyOf", "oneOf"):
760
+ source_branches = source_schema.get(union_keyword)
761
+ if not isinstance(source_branches, (list, tuple)):
762
+ continue
763
+ source_without_union = schema_without_keyword(source_schema, union_keyword)
764
+ return all(
765
+ schema_is_proven_subset(
766
+ combined_contract([source_without_union, source_branch]),
767
+ source_root_schema,
768
+ target_schema,
769
+ target_root_schema,
770
+ )
771
+ for source_branch in source_branches
772
+ )
773
+
774
+ if (target_reference := target_schema.get("$ref")) is not None:
775
+ return schema_is_proven_subset(
776
+ source_schema,
777
+ source_root_schema,
778
+ schema_without_keyword(target_schema, "$ref"),
779
+ target_root_schema,
780
+ ) and schema_is_proven_subset(
781
+ source_schema,
782
+ source_root_schema,
783
+ schema_reference_target(target_root_schema, cast(str, target_reference)),
784
+ target_root_schema,
785
+ )
786
+
787
+ if isinstance((target_all_of := target_schema.get("allOf")), (list, tuple)):
788
+ return schema_is_proven_subset(
789
+ source_schema,
790
+ source_root_schema,
791
+ schema_without_keyword(target_schema, "allOf"),
792
+ target_root_schema,
793
+ ) and all(
794
+ schema_is_proven_subset(
795
+ source_schema,
796
+ source_root_schema,
797
+ target_branch,
798
+ target_root_schema,
799
+ )
800
+ for target_branch in target_all_of
801
+ )
802
+
803
+ for union_keyword in ("anyOf", "oneOf"):
804
+ target_branches = target_schema.get(union_keyword)
805
+ if not isinstance(target_branches, (list, tuple)):
806
+ continue
807
+ if not schema_is_proven_subset(
808
+ source_schema,
809
+ source_root_schema,
810
+ schema_without_keyword(target_schema, union_keyword),
811
+ target_root_schema,
812
+ ):
813
+ return False
814
+ for branch_index, target_branch in enumerate(target_branches):
815
+ if not schema_is_proven_subset(
816
+ source_schema,
817
+ source_root_schema,
818
+ target_branch,
819
+ target_root_schema,
820
+ ):
821
+ continue
822
+ if union_keyword == "anyOf" or all(
823
+ _schemas_are_clearly_disjoint(
824
+ source_schema,
825
+ source_root_schema,
826
+ other_branch,
827
+ target_root_schema,
828
+ )
829
+ for other_index, other_branch in enumerate(target_branches)
830
+ if other_index != branch_index
831
+ ):
832
+ return True
833
+ return False
834
+
835
+ if any(keyword in target_schema for keyword in ("if", "not")):
836
+ return False
837
+ if "const" in target_schema or "enum" in target_schema:
838
+ return False
839
+
840
+ source_atoms = schema_type_atoms(source_schema, source_root_schema)
841
+ target_atoms = schema_type_atoms(target_schema, target_root_schema)
842
+ if not source_atoms <= target_atoms:
843
+ return False
844
+
845
+ numeric_atoms = frozenset({"integer", "non_integer_number"})
846
+ if source_atoms & numeric_atoms:
847
+ source_interval = _schema_numeric_interval(source_schema, source_root_schema)
848
+ target_interval = _schema_numeric_interval(target_schema, target_root_schema)
849
+ if not (
850
+ _lower_boundary_contains(source_interval[0], target_interval[0])
851
+ and _upper_boundary_contains(source_interval[1], target_interval[1])
852
+ and _multiple_of_is_subset(
853
+ source_schema.get("multipleOf"), target_schema.get("multipleOf")
854
+ )
855
+ ):
856
+ return False
857
+
858
+ if "string" in source_atoms:
859
+ source_minimum_length = _schema_length_bound(source_schema, source_root_schema, "minLength")
860
+ target_minimum_length = _schema_length_bound(target_schema, target_root_schema, "minLength")
861
+ if target_minimum_length is not None and (
862
+ source_minimum_length is None or source_minimum_length < target_minimum_length
863
+ ):
864
+ return False
865
+ source_maximum_length = _schema_length_bound(source_schema, source_root_schema, "maxLength")
866
+ target_maximum_length = _schema_length_bound(target_schema, target_root_schema, "maxLength")
867
+ if target_maximum_length is not None and (
868
+ source_maximum_length is None or source_maximum_length > target_maximum_length
869
+ ):
870
+ return False
871
+ for string_keyword in ("format", "pattern"):
872
+ if not _schema_string_assertions(
873
+ target_schema, target_root_schema, string_keyword
874
+ ) <= _schema_string_assertions(source_schema, source_root_schema, string_keyword):
875
+ return False
876
+
877
+ structured_atoms = frozenset({"array", "object"})
878
+ if source_atoms & structured_atoms:
879
+ if "object" in source_atoms:
880
+ source_minimum_properties = _schema_object_count_bound(
881
+ source_schema, source_root_schema, "minProperties"
882
+ )
883
+ target_minimum_properties = _schema_object_count_bound(
884
+ target_schema, target_root_schema, "minProperties"
885
+ )
886
+ if target_minimum_properties is not None and (
887
+ source_minimum_properties is None
888
+ or source_minimum_properties < target_minimum_properties
889
+ ):
890
+ return False
891
+ source_maximum_properties = _schema_object_count_bound(
892
+ source_schema, source_root_schema, "maxProperties"
893
+ )
894
+ target_maximum_properties = _schema_object_count_bound(
895
+ target_schema, target_root_schema, "maxProperties"
896
+ )
897
+ if target_maximum_properties is not None and (
898
+ source_maximum_properties is None
899
+ or source_maximum_properties > target_maximum_properties
900
+ ):
901
+ return False
902
+ structured_assertions = set(target_schema) - {
903
+ *_SCHEMA_ANNOTATION_KEYWORDS,
904
+ "$anchor",
905
+ "$defs",
906
+ "$id",
907
+ "$schema",
908
+ "maxProperties",
909
+ "minProperties",
910
+ "type",
911
+ }
912
+ if structured_assertions:
913
+ return False
914
+ return True
915
+
916
+
917
+ def _projection_all(projections: list[tuple[bool, bool]]) -> tuple[bool, bool]:
918
+ return (
919
+ all(projection[0] for projection in projections),
920
+ all(projection[1] for projection in projections),
921
+ )
922
+
923
+
924
+ def _projection_any(projections: list[tuple[bool, bool]]) -> tuple[bool, bool]:
925
+ return (
926
+ any(projection[0] for projection in projections),
927
+ any(projection[1] for projection in projections),
928
+ )
929
+
930
+
931
+ def _projection_not(projection: tuple[bool, bool]) -> tuple[bool, bool]:
932
+ can_match, must_match = projection
933
+ return not must_match, not can_match
934
+
935
+
936
+ def _projection_one_of(projections: list[tuple[bool, bool]]) -> tuple[bool, bool]:
937
+ possible_projections = [projection for projection in projections if projection[0]]
938
+ guaranteed_matches = sum(projection[1] for projection in possible_projections)
939
+ return (
940
+ bool(possible_projections) and guaranteed_matches < 2,
941
+ guaranteed_matches == 1 and len(possible_projections) == 1,
942
+ )
943
+
944
+
945
+ def _unknown_value_projection(
946
+ schema: Any,
947
+ root_schema: Any,
948
+ reference_stack: tuple[str, ...] = (),
949
+ ) -> tuple[bool, bool]:
950
+ if schema is False:
951
+ return False, False
952
+ if schema is True or not isinstance(schema, Mapping):
953
+ return True, True
954
+
955
+ projections: list[tuple[bool, bool]] = []
956
+ value_keywords = set(schema) - {
957
+ "$anchor",
958
+ "$comment",
959
+ "$defs",
960
+ "$id",
961
+ "$ref",
962
+ "$schema",
963
+ "allOf",
964
+ "anyOf",
965
+ "definitions",
966
+ "description",
967
+ "else",
968
+ "examples",
969
+ "if",
970
+ "oneOf",
971
+ "then",
972
+ "title",
973
+ }
974
+ if value_keywords:
975
+ projections.append((True, False))
976
+
977
+ if (reference := schema.get("$ref")) is not None:
978
+ reference_text = cast(str, reference)
979
+ if reference_text in reference_stack:
980
+ projections.append((True, False))
981
+ else:
982
+ projections.append(
983
+ _unknown_value_projection(
984
+ schema_reference_target(root_schema, reference_text),
985
+ root_schema,
986
+ (*reference_stack, reference_text),
987
+ )
988
+ )
989
+ if isinstance((all_of := schema.get("allOf")), (list, tuple)):
990
+ projections.append(
991
+ _projection_all(
992
+ [
993
+ _unknown_value_projection(branch, root_schema, reference_stack)
994
+ for branch in all_of
995
+ ]
996
+ )
997
+ )
998
+ if isinstance((any_of := schema.get("anyOf")), (list, tuple)):
999
+ projections.append(
1000
+ _projection_any(
1001
+ [
1002
+ _unknown_value_projection(branch, root_schema, reference_stack)
1003
+ for branch in any_of
1004
+ ]
1005
+ )
1006
+ )
1007
+ if isinstance((one_of := schema.get("oneOf")), (list, tuple)):
1008
+ projections.append(
1009
+ _projection_one_of(
1010
+ [
1011
+ _unknown_value_projection(branch, root_schema, reference_stack)
1012
+ for branch in one_of
1013
+ ]
1014
+ )
1015
+ )
1016
+ if "not" in schema:
1017
+ projections.append(
1018
+ _projection_not(_unknown_value_projection(schema["not"], root_schema, reference_stack))
1019
+ )
1020
+ if "if" in schema:
1021
+ condition_projection = _unknown_value_projection(schema["if"], root_schema, reference_stack)
1022
+ conditional_projection = _projection_any(
1023
+ [
1024
+ _projection_all(
1025
+ [
1026
+ condition_projection,
1027
+ _unknown_value_projection(
1028
+ schema.get("then", True), root_schema, reference_stack
1029
+ ),
1030
+ ]
1031
+ ),
1032
+ _projection_all(
1033
+ [
1034
+ _projection_not(condition_projection),
1035
+ _unknown_value_projection(
1036
+ schema.get("else", True), root_schema, reference_stack
1037
+ ),
1038
+ ]
1039
+ ),
1040
+ ]
1041
+ )
1042
+ projections.append(conditional_projection)
1043
+ return _projection_all(projections) if projections else (True, True)
1044
+
1045
+
1046
+ def key_projection_match(
1047
+ schema: Any,
1048
+ root_schema: Any,
1049
+ bound_keys: frozenset[str],
1050
+ reference_stack: tuple[str, ...] = (),
1051
+ ) -> tuple[bool, bool]:
1052
+ if schema is False:
1053
+ return False, False
1054
+ if schema is True or not isinstance(schema, Mapping):
1055
+ return True, True
1056
+
1057
+ projections: list[tuple[bool, bool]] = []
1058
+ schema_types = schema.get("type")
1059
+ if schema_types is not None:
1060
+ supports_object = schema_types == "object" or (
1061
+ isinstance(schema_types, (list, tuple)) and "object" in schema_types
1062
+ )
1063
+ projections.append((supports_object, supports_object))
1064
+
1065
+ if "const" in schema:
1066
+ constant_object = schema["const"]
1067
+ constant_keys_match = (
1068
+ isinstance(constant_object, Mapping)
1069
+ and frozenset(str(property_name) for property_name in constant_object) == bound_keys
1070
+ )
1071
+ projections.append((constant_keys_match, False))
1072
+ if isinstance((enum_members := schema.get("enum")), (list, tuple)):
1073
+ matching_enum_member = any(
1074
+ isinstance(enum_member, Mapping)
1075
+ and frozenset(str(property_name) for property_name in enum_member) == bound_keys
1076
+ for enum_member in enum_members
1077
+ )
1078
+ projections.append((matching_enum_member, False))
1079
+
1080
+ required_properties = {
1081
+ property_name
1082
+ for property_name in schema.get("required", ())
1083
+ if isinstance(property_name, str)
1084
+ }
1085
+ if required_properties:
1086
+ has_required_properties = required_properties <= bound_keys
1087
+ projections.append((has_required_properties, has_required_properties))
1088
+ if isinstance((minimum_properties := schema.get("minProperties")), int):
1089
+ meets_minimum = len(bound_keys) >= minimum_properties
1090
+ projections.append((meets_minimum, meets_minimum))
1091
+ if isinstance((maximum_properties := schema.get("maxProperties")), int):
1092
+ meets_maximum = len(bound_keys) <= maximum_properties
1093
+ projections.append((meets_maximum, meets_maximum))
1094
+ if (property_names := schema.get("propertyNames")) is not None:
1095
+ valid_property_names = all(
1096
+ schema_accepts_known_instance(property_names, root_schema, bound_key)
1097
+ for bound_key in bound_keys
1098
+ )
1099
+ projections.append((valid_property_names, valid_property_names))
1100
+
1101
+ properties = schema.get("properties")
1102
+ pattern_properties = schema.get("patternProperties")
1103
+ additional_properties = schema.get("additionalProperties", True)
1104
+ if isinstance(properties, Mapping) or isinstance(pattern_properties, Mapping):
1105
+ property_projections: list[tuple[bool, bool]] = []
1106
+ for bound_key in bound_keys:
1107
+ matching_property_schemas: list[Any] = []
1108
+ if isinstance(properties, Mapping) and bound_key in properties:
1109
+ matching_property_schemas.append(properties[bound_key])
1110
+ if isinstance(pattern_properties, Mapping):
1111
+ matching_property_schemas.extend(
1112
+ property_schema
1113
+ for property_pattern, property_schema in pattern_properties.items()
1114
+ if re.search(property_pattern, bound_key) is not None
1115
+ )
1116
+ if not matching_property_schemas:
1117
+ if additional_properties is False:
1118
+ property_projections.append((False, False))
1119
+ continue
1120
+ matching_property_schemas.append(additional_properties)
1121
+ property_projections.append(
1122
+ _projection_all(
1123
+ [
1124
+ _unknown_value_projection(property_schema, root_schema, reference_stack)
1125
+ for property_schema in matching_property_schemas
1126
+ ]
1127
+ )
1128
+ )
1129
+ if property_projections:
1130
+ projections.append(_projection_all(property_projections))
1131
+ elif additional_properties is False and bound_keys:
1132
+ projections.append((False, False))
1133
+
1134
+ dependent_required = schema.get("dependentRequired")
1135
+ if isinstance(dependent_required, Mapping):
1136
+ for trigger_property, dependent_properties in dependent_required.items():
1137
+ if trigger_property not in bound_keys:
1138
+ continue
1139
+ dependencies_present = all(
1140
+ dependent_property in bound_keys for dependent_property in dependent_properties
1141
+ )
1142
+ projections.append((dependencies_present, dependencies_present))
1143
+ dependent_schemas = schema.get("dependentSchemas")
1144
+ if isinstance(dependent_schemas, Mapping):
1145
+ projections.extend(
1146
+ key_projection_match(
1147
+ dependent_schema,
1148
+ root_schema,
1149
+ bound_keys,
1150
+ reference_stack,
1151
+ )
1152
+ for trigger_property, dependent_schema in dependent_schemas.items()
1153
+ if trigger_property in bound_keys
1154
+ )
1155
+
1156
+ if (reference := schema.get("$ref")) is not None:
1157
+ reference_text = cast(str, reference)
1158
+ if reference_text in reference_stack:
1159
+ projections.append((True, False))
1160
+ else:
1161
+ projections.append(
1162
+ key_projection_match(
1163
+ schema_reference_target(root_schema, reference_text),
1164
+ root_schema,
1165
+ bound_keys,
1166
+ (*reference_stack, reference_text),
1167
+ )
1168
+ )
1169
+ if isinstance((all_of := schema.get("allOf")), (list, tuple)):
1170
+ projections.append(
1171
+ _projection_all(
1172
+ [
1173
+ key_projection_match(branch, root_schema, bound_keys, reference_stack)
1174
+ for branch in all_of
1175
+ ]
1176
+ )
1177
+ )
1178
+ if isinstance((any_of := schema.get("anyOf")), (list, tuple)):
1179
+ projections.append(
1180
+ _projection_any(
1181
+ [
1182
+ key_projection_match(branch, root_schema, bound_keys, reference_stack)
1183
+ for branch in any_of
1184
+ ]
1185
+ )
1186
+ )
1187
+ if isinstance((one_of := schema.get("oneOf")), (list, tuple)):
1188
+ projections.append(
1189
+ _projection_one_of(
1190
+ [
1191
+ key_projection_match(branch, root_schema, bound_keys, reference_stack)
1192
+ for branch in one_of
1193
+ ]
1194
+ )
1195
+ )
1196
+ if "not" in schema:
1197
+ projections.append(
1198
+ _projection_not(
1199
+ key_projection_match(schema["not"], root_schema, bound_keys, reference_stack)
1200
+ )
1201
+ )
1202
+ if "if" in schema:
1203
+ condition_projection = key_projection_match(
1204
+ schema["if"], root_schema, bound_keys, reference_stack
1205
+ )
1206
+ projections.append(
1207
+ _projection_any(
1208
+ [
1209
+ _projection_all(
1210
+ [
1211
+ condition_projection,
1212
+ key_projection_match(
1213
+ schema.get("then", True),
1214
+ root_schema,
1215
+ bound_keys,
1216
+ reference_stack,
1217
+ ),
1218
+ ]
1219
+ ),
1220
+ _projection_all(
1221
+ [
1222
+ _projection_not(condition_projection),
1223
+ key_projection_match(
1224
+ schema.get("else", True),
1225
+ root_schema,
1226
+ bound_keys,
1227
+ reference_stack,
1228
+ ),
1229
+ ]
1230
+ ),
1231
+ ]
1232
+ )
1233
+ )
1234
+
1235
+ return _projection_all(projections) if projections else (True, True)
1236
+
1237
+
1238
+ def schema_property_status(
1239
+ schema: Any,
1240
+ property_name: str,
1241
+ root_schema: Any | None = None,
1242
+ reference_stack: tuple[str, ...] = (),
1243
+ ) -> int:
1244
+ resolved_root_schema = schema if root_schema is None else root_schema
1245
+ if schema is False:
1246
+ return CONTRACT_INVALID
1247
+ if schema is True or not isinstance(schema, Mapping):
1248
+ return CONTRACT_UNKNOWN
1249
+ if (reference := schema.get("$ref")) is not None:
1250
+ reference_text = cast(str, reference)
1251
+ if reference_text in reference_stack:
1252
+ return CONTRACT_UNKNOWN
1253
+ return schema_property_status(
1254
+ schema_reference_target(resolved_root_schema, reference_text),
1255
+ property_name,
1256
+ resolved_root_schema,
1257
+ (*reference_stack, reference_text),
1258
+ )
1259
+
1260
+ all_of = schema.get("allOf")
1261
+ if isinstance(all_of, (list, tuple)):
1262
+ statuses = [
1263
+ schema_property_status(branch, property_name, resolved_root_schema, reference_stack)
1264
+ for branch in all_of
1265
+ ]
1266
+ if CONTRACT_INVALID in statuses:
1267
+ return CONTRACT_INVALID
1268
+ if CONTRACT_VALID in statuses:
1269
+ return CONTRACT_VALID
1270
+
1271
+ for union_keyword in ("oneOf", "anyOf"):
1272
+ branches = schema.get(union_keyword)
1273
+ if not isinstance(branches, (list, tuple)):
1274
+ continue
1275
+ statuses = [
1276
+ schema_property_status(branch, property_name, resolved_root_schema, reference_stack)
1277
+ for branch in branches
1278
+ ]
1279
+ if CONTRACT_VALID in statuses:
1280
+ return CONTRACT_VALID
1281
+ if statuses and all(status == CONTRACT_INVALID for status in statuses):
1282
+ return CONTRACT_INVALID
1283
+ return CONTRACT_UNKNOWN
1284
+
1285
+ schema_types = schema.get("type")
1286
+ if isinstance(schema_types, str) and schema_types != "object":
1287
+ return CONTRACT_INVALID
1288
+ if isinstance(schema_types, (list, tuple)) and "object" not in schema_types:
1289
+ return CONTRACT_INVALID
1290
+
1291
+ properties = schema.get("properties")
1292
+ if isinstance(properties, Mapping) and property_name in properties:
1293
+ return CONTRACT_VALID
1294
+ pattern_properties = schema.get("patternProperties")
1295
+ if isinstance(pattern_properties, Mapping) and any(
1296
+ re.search(pattern, property_name) is not None for pattern in pattern_properties
1297
+ ):
1298
+ return CONTRACT_VALID
1299
+ additional_properties = schema.get("additionalProperties", True)
1300
+ if additional_properties is False:
1301
+ return CONTRACT_INVALID
1302
+ return CONTRACT_VALID if isinstance(additional_properties, Mapping) else CONTRACT_UNKNOWN
1303
+
1304
+
1305
+ def required_schema_properties(
1306
+ schema: Any,
1307
+ root_schema: Any | None = None,
1308
+ reference_stack: tuple[str, ...] = (),
1309
+ ) -> set[str]:
1310
+ resolved_root_schema = schema if root_schema is None else root_schema
1311
+ if not isinstance(schema, Mapping):
1312
+ return set()
1313
+ if (reference := schema.get("$ref")) is not None:
1314
+ reference_text = cast(str, reference)
1315
+ if reference_text in reference_stack:
1316
+ return set()
1317
+ return required_schema_properties(
1318
+ schema_reference_target(resolved_root_schema, reference_text),
1319
+ resolved_root_schema,
1320
+ (*reference_stack, reference_text),
1321
+ )
1322
+ required_properties = {
1323
+ property_name
1324
+ for property_name in schema.get("required", ())
1325
+ if isinstance(property_name, str)
1326
+ }
1327
+ all_of = schema.get("allOf")
1328
+ if isinstance(all_of, (list, tuple)):
1329
+ for branch in all_of:
1330
+ required_properties.update(
1331
+ required_schema_properties(branch, resolved_root_schema, reference_stack)
1332
+ )
1333
+ for union_keyword in ("oneOf", "anyOf"):
1334
+ branches = schema.get(union_keyword)
1335
+ if not isinstance(branches, (list, tuple)) or not branches:
1336
+ continue
1337
+ branch_requirements = [
1338
+ required_schema_properties(branch, resolved_root_schema, reference_stack)
1339
+ for branch in branches
1340
+ ]
1341
+ required_properties.update(set.intersection(*branch_requirements))
1342
+ return required_properties
1343
+
1344
+
1345
+ def schema_path_status(
1346
+ schema: Any,
1347
+ path_segments: tuple[str, ...],
1348
+ root_schema: Any | None = None,
1349
+ reference_stack: tuple[str, ...] = (),
1350
+ ) -> int:
1351
+ resolved_root_schema = schema if root_schema is None else root_schema
1352
+ if schema is False:
1353
+ return CONTRACT_INVALID
1354
+ if not path_segments:
1355
+ return CONTRACT_VALID
1356
+ if schema is True or not isinstance(schema, Mapping):
1357
+ return CONTRACT_UNKNOWN
1358
+ if (reference := schema.get("$ref")) is not None:
1359
+ reference_text = cast(str, reference)
1360
+ if reference_text in reference_stack:
1361
+ return CONTRACT_UNKNOWN
1362
+ return schema_path_status(
1363
+ schema_reference_target(resolved_root_schema, reference_text),
1364
+ path_segments,
1365
+ resolved_root_schema,
1366
+ (*reference_stack, reference_text),
1367
+ )
1368
+
1369
+ all_of = schema.get("allOf")
1370
+ if isinstance(all_of, (list, tuple)):
1371
+ statuses = [
1372
+ schema_path_status(branch, path_segments, resolved_root_schema, reference_stack)
1373
+ for branch in all_of
1374
+ ]
1375
+ if CONTRACT_INVALID in statuses:
1376
+ return CONTRACT_INVALID
1377
+ if CONTRACT_VALID in statuses:
1378
+ return CONTRACT_VALID
1379
+
1380
+ for union_keyword in ("oneOf", "anyOf"):
1381
+ branches = schema.get(union_keyword)
1382
+ if not isinstance(branches, (list, tuple)):
1383
+ continue
1384
+ statuses = [
1385
+ schema_path_status(branch, path_segments, resolved_root_schema, reference_stack)
1386
+ for branch in branches
1387
+ ]
1388
+ if CONTRACT_VALID in statuses:
1389
+ return CONTRACT_VALID
1390
+ if statuses and all(status == CONTRACT_INVALID for status in statuses):
1391
+ return CONTRACT_INVALID
1392
+ return CONTRACT_UNKNOWN
1393
+
1394
+ path_segment, remaining_segments = path_segments[0], path_segments[1:]
1395
+ schema_types = schema.get("type")
1396
+ supports_array = schema_types == "array" or (
1397
+ isinstance(schema_types, (list, tuple)) and "array" in schema_types
1398
+ )
1399
+ supports_object = (
1400
+ schema_types in (None, "object")
1401
+ or isinstance(schema.get("properties"), Mapping)
1402
+ or (isinstance(schema_types, (list, tuple)) and "object" in schema_types)
1403
+ )
1404
+
1405
+ if supports_array and path_segment.isdigit():
1406
+ items = schema.get("items", True)
1407
+ return schema_path_status(items, remaining_segments, resolved_root_schema, reference_stack)
1408
+ if supports_object:
1409
+ properties = schema.get("properties")
1410
+ if isinstance(properties, Mapping) and path_segment in properties:
1411
+ return schema_path_status(
1412
+ properties[path_segment],
1413
+ remaining_segments,
1414
+ resolved_root_schema,
1415
+ reference_stack,
1416
+ )
1417
+ pattern_properties = schema.get("patternProperties")
1418
+ if isinstance(pattern_properties, Mapping):
1419
+ matching_schemas = [
1420
+ nested_schema
1421
+ for pattern, nested_schema in pattern_properties.items()
1422
+ if re.search(pattern, path_segment) is not None
1423
+ ]
1424
+ if matching_schemas:
1425
+ statuses = [
1426
+ schema_path_status(
1427
+ nested_schema,
1428
+ remaining_segments,
1429
+ resolved_root_schema,
1430
+ reference_stack,
1431
+ )
1432
+ for nested_schema in matching_schemas
1433
+ ]
1434
+ if CONTRACT_INVALID in statuses:
1435
+ return CONTRACT_INVALID
1436
+ if all(status == CONTRACT_VALID for status in statuses):
1437
+ return CONTRACT_VALID
1438
+ return CONTRACT_UNKNOWN
1439
+ additional_properties = schema.get("additionalProperties", True)
1440
+ if additional_properties is False:
1441
+ return CONTRACT_INVALID
1442
+ if isinstance(additional_properties, Mapping):
1443
+ return schema_path_status(
1444
+ additional_properties,
1445
+ remaining_segments,
1446
+ resolved_root_schema,
1447
+ reference_stack,
1448
+ )
1449
+ return CONTRACT_UNKNOWN
1450
+ return CONTRACT_INVALID
1451
+
1452
+
1453
+ def _combine_schema_alternatives(
1454
+ left_alternatives: list[list[Any]],
1455
+ right_alternatives: list[list[Any]],
1456
+ ) -> list[list[Any]]:
1457
+ return [
1458
+ [*left_alternative, *right_alternative]
1459
+ for left_alternative in left_alternatives
1460
+ for right_alternative in right_alternatives
1461
+ ]
1462
+
1463
+
1464
+ def schema_conjunctive_alternatives(
1465
+ schema: Any,
1466
+ root_schema: Any,
1467
+ reference_stack: tuple[str, ...] = (),
1468
+ ) -> list[list[Any]]:
1469
+ if schema is False:
1470
+ return []
1471
+ if schema is True or not isinstance(schema, Mapping):
1472
+ return [[]]
1473
+
1474
+ applicator_keywords = {
1475
+ "$ref",
1476
+ "allOf",
1477
+ "anyOf",
1478
+ "else",
1479
+ "if",
1480
+ "oneOf",
1481
+ "then",
1482
+ }
1483
+ direct_schema = {
1484
+ property_name: property_contract
1485
+ for property_name, property_contract in schema.items()
1486
+ if property_name not in applicator_keywords
1487
+ }
1488
+ alternatives: list[list[Any]] = [[direct_schema]] if direct_schema else [[]]
1489
+
1490
+ if (reference := schema.get("$ref")) is not None:
1491
+ reference_text = cast(str, reference)
1492
+ if reference_text not in reference_stack:
1493
+ alternatives = _combine_schema_alternatives(
1494
+ alternatives,
1495
+ schema_conjunctive_alternatives(
1496
+ schema_reference_target(root_schema, reference_text),
1497
+ root_schema,
1498
+ (*reference_stack, reference_text),
1499
+ ),
1500
+ )
1501
+ if isinstance((all_of := schema.get("allOf")), (list, tuple)):
1502
+ for branch in all_of:
1503
+ alternatives = _combine_schema_alternatives(
1504
+ alternatives,
1505
+ schema_conjunctive_alternatives(branch, root_schema, reference_stack),
1506
+ )
1507
+ for union_keyword in ("anyOf", "oneOf"):
1508
+ branches = schema.get(union_keyword)
1509
+ if not isinstance(branches, (list, tuple)):
1510
+ continue
1511
+ union_alternatives = [
1512
+ branch_alternative
1513
+ for branch in branches
1514
+ for branch_alternative in schema_conjunctive_alternatives(
1515
+ branch, root_schema, reference_stack
1516
+ )
1517
+ ]
1518
+ alternatives = _combine_schema_alternatives(alternatives, union_alternatives)
1519
+ if "if" in schema:
1520
+ condition_schema = schema["if"]
1521
+ conditional_alternatives = [
1522
+ *(
1523
+ _combine_schema_alternatives(
1524
+ schema_conjunctive_alternatives(condition_schema, root_schema, reference_stack),
1525
+ schema_conjunctive_alternatives(
1526
+ schema.get("then", True), root_schema, reference_stack
1527
+ ),
1528
+ )
1529
+ ),
1530
+ *(
1531
+ _combine_schema_alternatives(
1532
+ [[{"not": condition_schema}]],
1533
+ schema_conjunctive_alternatives(
1534
+ schema.get("else", True), root_schema, reference_stack
1535
+ ),
1536
+ )
1537
+ ),
1538
+ ]
1539
+ alternatives = _combine_schema_alternatives(alternatives, conditional_alternatives)
1540
+ return alternatives
1541
+
1542
+
1543
+ def _alternative_required_properties(
1544
+ schema_fragments: list[Any],
1545
+ root_schema: Any,
1546
+ ) -> frozenset[str]:
1547
+ required_properties = {
1548
+ property_name
1549
+ for schema_fragment in schema_fragments
1550
+ if isinstance(schema_fragment, Mapping)
1551
+ for property_name in schema_fragment.get("required", ())
1552
+ if isinstance(property_name, str)
1553
+ }
1554
+ requirements_changed = True
1555
+ while requirements_changed:
1556
+ requirements_changed = False
1557
+ for schema_fragment in schema_fragments:
1558
+ if not isinstance(schema_fragment, Mapping):
1559
+ continue
1560
+ dependent_required = schema_fragment.get("dependentRequired")
1561
+ if isinstance(dependent_required, Mapping):
1562
+ for trigger_property, dependent_properties in dependent_required.items():
1563
+ if trigger_property not in required_properties:
1564
+ continue
1565
+ previous_size = len(required_properties)
1566
+ required_properties.update(
1567
+ property_name
1568
+ for property_name in dependent_properties
1569
+ if isinstance(property_name, str)
1570
+ )
1571
+ requirements_changed = (
1572
+ requirements_changed or len(required_properties) > previous_size
1573
+ )
1574
+ dependent_schemas = schema_fragment.get("dependentSchemas")
1575
+ if not isinstance(dependent_schemas, Mapping):
1576
+ continue
1577
+ for trigger_property, dependent_schema in dependent_schemas.items():
1578
+ if trigger_property not in required_properties:
1579
+ continue
1580
+ previous_size = len(required_properties)
1581
+ required_properties.update(
1582
+ required_schema_properties(dependent_schema, root_schema)
1583
+ )
1584
+ requirements_changed = (
1585
+ requirements_changed or len(required_properties) > previous_size
1586
+ )
1587
+ return frozenset(required_properties)
1588
+
1589
+
1590
+ def _alternative_property_contract(
1591
+ schema_fragments: list[Any],
1592
+ property_name: str,
1593
+ root_schema: Any,
1594
+ required_properties: frozenset[str],
1595
+ ) -> Any:
1596
+ return combined_contract(
1597
+ [
1598
+ schema_property_contract(
1599
+ schema_fragment,
1600
+ property_name,
1601
+ root_schema,
1602
+ required_properties,
1603
+ )
1604
+ for schema_fragment in schema_fragments
1605
+ ]
1606
+ )
1607
+
1608
+
1609
+ def _schema_is_unconditional(
1610
+ schema: Any,
1611
+ root_schema: Any,
1612
+ reference_stack: tuple[str, ...] = (),
1613
+ ) -> bool:
1614
+ if schema is True:
1615
+ return True
1616
+ if schema is False or not isinstance(schema, Mapping):
1617
+ return False
1618
+ assertion_keywords = set(schema) - {
1619
+ "$anchor",
1620
+ "$comment",
1621
+ "$defs",
1622
+ "$id",
1623
+ "$schema",
1624
+ "description",
1625
+ "examples",
1626
+ "title",
1627
+ }
1628
+ if not assertion_keywords:
1629
+ return True
1630
+ if assertion_keywords == {"$ref"}:
1631
+ reference_text = cast(str, schema["$ref"])
1632
+ return reference_text in reference_stack or _schema_is_unconditional(
1633
+ schema_reference_target(root_schema, reference_text),
1634
+ root_schema,
1635
+ (*reference_stack, reference_text),
1636
+ )
1637
+ if assertion_keywords == {"allOf"} and isinstance(
1638
+ (all_of := schema.get("allOf")), (list, tuple)
1639
+ ):
1640
+ return all(
1641
+ _schema_is_unconditional(branch, root_schema, reference_stack) for branch in all_of
1642
+ )
1643
+ if assertion_keywords == {"anyOf"} and isinstance(
1644
+ (any_of := schema.get("anyOf")), (list, tuple)
1645
+ ):
1646
+ return any(
1647
+ _schema_is_unconditional(branch, root_schema, reference_stack) for branch in any_of
1648
+ )
1649
+ return False
1650
+
1651
+
1652
+ def _schema_is_guaranteed_for_known_object(
1653
+ schema: Any,
1654
+ root_schema: Any,
1655
+ known_properties: Mapping[str, Any],
1656
+ required_properties: frozenset[str],
1657
+ reference_stack: tuple[str, ...] = (),
1658
+ ) -> bool:
1659
+ if schema is True:
1660
+ return True
1661
+ if schema is False or not isinstance(schema, Mapping):
1662
+ return False
1663
+
1664
+ schema_types = schema.get("type")
1665
+ if schema_types is not None and not (
1666
+ schema_types == "object"
1667
+ or isinstance(schema_types, (list, tuple))
1668
+ and "object" in schema_types
1669
+ ):
1670
+ return False
1671
+ if "const" in schema or "enum" in schema:
1672
+ return False
1673
+ declared_required = {
1674
+ property_name
1675
+ for property_name in schema.get("required", ())
1676
+ if isinstance(property_name, str)
1677
+ }
1678
+ if not declared_required <= required_properties:
1679
+ return False
1680
+ if isinstance((minimum_properties := schema.get("minProperties")), int) and (
1681
+ len(required_properties) < minimum_properties
1682
+ ):
1683
+ return False
1684
+ if "maxProperties" in schema:
1685
+ return False
1686
+ if schema.get("additionalProperties") is False:
1687
+ return False
1688
+ if "propertyNames" in schema:
1689
+ return False
1690
+ if any(
1691
+ schema.get(dependency_keyword)
1692
+ for dependency_keyword in ("dependentRequired", "dependentSchemas")
1693
+ ):
1694
+ return False
1695
+
1696
+ properties = schema.get("properties")
1697
+ if isinstance(properties, Mapping):
1698
+ for property_name, property_contract in properties.items():
1699
+ if property_name in known_properties:
1700
+ if not schema_accepts_known_instance(
1701
+ property_contract,
1702
+ root_schema,
1703
+ known_properties[property_name],
1704
+ ):
1705
+ return False
1706
+ elif not _schema_is_unconditional(property_contract, root_schema):
1707
+ return False
1708
+ pattern_properties = schema.get("patternProperties")
1709
+ if isinstance(pattern_properties, Mapping) and any(
1710
+ not _schema_is_unconditional(property_contract, root_schema)
1711
+ for property_contract in pattern_properties.values()
1712
+ ):
1713
+ return False
1714
+
1715
+ if (reference := schema.get("$ref")) is not None:
1716
+ reference_text = cast(str, reference)
1717
+ if reference_text not in reference_stack and not _schema_is_guaranteed_for_known_object(
1718
+ schema_reference_target(root_schema, reference_text),
1719
+ root_schema,
1720
+ known_properties,
1721
+ required_properties,
1722
+ (*reference_stack, reference_text),
1723
+ ):
1724
+ return False
1725
+ if isinstance((all_of := schema.get("allOf")), (list, tuple)) and not all(
1726
+ _schema_is_guaranteed_for_known_object(
1727
+ branch,
1728
+ root_schema,
1729
+ known_properties,
1730
+ required_properties,
1731
+ reference_stack,
1732
+ )
1733
+ for branch in all_of
1734
+ ):
1735
+ return False
1736
+ if isinstance((any_of := schema.get("anyOf")), (list, tuple)) and not any(
1737
+ _schema_is_guaranteed_for_known_object(
1738
+ branch,
1739
+ root_schema,
1740
+ known_properties,
1741
+ required_properties,
1742
+ reference_stack,
1743
+ )
1744
+ for branch in any_of
1745
+ ):
1746
+ return False
1747
+ if any(composition_keyword in schema for composition_keyword in ("oneOf", "not", "if")):
1748
+ return False
1749
+ return True
1750
+
1751
+
1752
+ def alternative_is_success_compatible(
1753
+ schema_fragments: list[Any],
1754
+ root_schema: Any,
1755
+ ) -> bool:
1756
+ required_properties = _alternative_required_properties(schema_fragments, root_schema)
1757
+ if "status" not in required_properties:
1758
+ return True
1759
+ status_contract = _alternative_property_contract(
1760
+ schema_fragments,
1761
+ "status",
1762
+ root_schema,
1763
+ required_properties,
1764
+ )
1765
+ if not schema_accepts_known_instance(status_contract, root_schema, "success"):
1766
+ return False
1767
+ known_properties = {"status": "success"}
1768
+ return not any(
1769
+ isinstance(schema_fragment, Mapping)
1770
+ and (negated_schema := schema_fragment.get("not")) is not None
1771
+ and _schema_is_guaranteed_for_known_object(
1772
+ negated_schema,
1773
+ root_schema,
1774
+ known_properties,
1775
+ required_properties,
1776
+ )
1777
+ for schema_fragment in schema_fragments
1778
+ )
1779
+
1780
+
1781
+ def _alternative_requires_path(
1782
+ schema_fragments: list[Any],
1783
+ path_segments: tuple[str, ...],
1784
+ root_schema: Any,
1785
+ ) -> bool:
1786
+ required_properties = _alternative_required_properties(schema_fragments, root_schema)
1787
+ path_segment, remaining_segments = path_segments[0], path_segments[1:]
1788
+ if path_segment not in required_properties:
1789
+ return False
1790
+ if not remaining_segments:
1791
+ return True
1792
+ nested_contract = _alternative_property_contract(
1793
+ schema_fragments,
1794
+ path_segment,
1795
+ root_schema,
1796
+ required_properties,
1797
+ )
1798
+ return schema_requires_path_in_every_alternative(
1799
+ nested_contract,
1800
+ remaining_segments,
1801
+ root_schema,
1802
+ )
1803
+
1804
+
1805
+ def schema_requires_path_in_every_alternative(
1806
+ schema: Any,
1807
+ path_segments: tuple[str, ...],
1808
+ root_schema: Any,
1809
+ ) -> bool:
1810
+ alternatives = schema_conjunctive_alternatives(schema, root_schema)
1811
+ return bool(alternatives) and all(
1812
+ _alternative_requires_path(
1813
+ schema_fragments,
1814
+ path_segments,
1815
+ root_schema,
1816
+ )
1817
+ for schema_fragments in alternatives
1818
+ )
1819
+
1820
+
1821
+ def schema_requires_path_for_every_success(
1822
+ schema: Any,
1823
+ path_segments: tuple[str, ...],
1824
+ ) -> bool:
1825
+ alternatives = schema_conjunctive_alternatives(schema, schema)
1826
+ return all(
1827
+ not alternative_is_success_compatible(schema_fragments, schema)
1828
+ or _alternative_requires_path(schema_fragments, path_segments, schema)
1829
+ for schema_fragments in alternatives
1830
+ )