katalyst-engine 5.0.1a2__tar.gz → 5.0.1a4__tar.gz

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 (155) hide show
  1. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/PKG-INFO +2 -2
  2. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/pyproject.toml +2 -2
  3. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/bundle/__init__.py +2 -2
  4. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/bundle/loader.py +57 -2
  5. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/bundle/policy.py +29 -33
  6. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/bundle/protocol.py +8 -1
  7. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/evolution/__init__.py +64 -0
  8. katalyst_engine-5.0.1a4/src/katalyst_engine/evolution/chain.py +213 -0
  9. katalyst_engine-5.0.1a4/src/katalyst_engine/evolution/changelog.py +201 -0
  10. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/evolution/gate.py +21 -0
  11. katalyst_engine-5.0.1a4/src/katalyst_engine/evolution/lock.py +228 -0
  12. katalyst_engine-5.0.1a4/src/katalyst_engine/evolution/ranges.py +241 -0
  13. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/evolution/release.py +49 -0
  14. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/schema/definition.py +1 -1
  15. katalyst_engine-5.0.1a4/src/katalyst_engine/schema_docs/__init__.py +35 -0
  16. katalyst_engine-5.0.1a4/src/katalyst_engine/schema_docs/reference.py +293 -0
  17. katalyst_engine-5.0.1a4/src/katalyst_engine/schema_docs/regions.py +107 -0
  18. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/toolkit/linkml.py +72 -2
  19. katalyst_engine-5.0.1a4/tests/bundle/test_bundle_requires.py +116 -0
  20. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/evolution/conftest.py +0 -7
  21. katalyst_engine-5.0.1a4/tests/evolution/test_chain.py +99 -0
  22. katalyst_engine-5.0.1a4/tests/evolution/test_changelog.py +137 -0
  23. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/evolution/test_diff.py +2 -2
  24. katalyst_engine-5.0.1a4/tests/evolution/test_lock.py +94 -0
  25. katalyst_engine-5.0.1a4/tests/evolution/test_ranges.py +106 -0
  26. katalyst_engine-5.0.1a4/tests/evolution/test_regions.py +78 -0
  27. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/evolution/test_release.py +50 -0
  28. katalyst_engine-5.0.1a4/tests/evolution/test_schema_docs.py +161 -0
  29. katalyst_engine-5.0.1a4/tests/toolkit/test_core_parent.py +171 -0
  30. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/.gitignore +0 -0
  31. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/CHANGELOG.md +0 -0
  32. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/README.md +0 -0
  33. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/__init__.py +0 -0
  34. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/bundle/discovery.py +0 -0
  35. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/bundle/path_loader.py +0 -0
  36. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/core/__init__.py +0 -0
  37. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/core/compatibility.py +0 -0
  38. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/core/compositional.py +0 -0
  39. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/core/definitive.py +0 -0
  40. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/core/errors.py +0 -0
  41. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/core/evolvable.py +0 -0
  42. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/core/identity.py +0 -0
  43. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/core/lifecycle.py +0 -0
  44. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/core/matching.py +0 -0
  45. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/core/relation.py +0 -0
  46. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/core/version.py +0 -0
  47. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/discovery/__init__.py +0 -0
  48. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/discovery/declaration.py +0 -0
  49. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/discovery/dispatcher.py +0 -0
  50. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/discovery/protocol.py +0 -0
  51. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/events/__init__.py +0 -0
  52. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/events/bus.py +0 -0
  53. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/events/event.py +0 -0
  54. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/evolution/diff.py +0 -0
  55. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/evolution/migration.py +0 -0
  56. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/extensions/__init__.py +0 -0
  57. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/extensions/capability.py +0 -0
  58. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/extensions/discovery.py +0 -0
  59. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/extensions/effector.py +0 -0
  60. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/extensions/provider.py +0 -0
  61. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/extensions/registry.py +0 -0
  62. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/extensions/trigger.py +0 -0
  63. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/model/__init__.py +0 -0
  64. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/model/manager.py +0 -0
  65. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/model/materializer.py +0 -0
  66. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/model/node.py +0 -0
  67. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/model/query.py +0 -0
  68. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/model/store.py +0 -0
  69. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/py.typed +0 -0
  70. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/replication/__init__.py +0 -0
  71. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/replication/engine.py +0 -0
  72. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/replication/job.py +0 -0
  73. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/replication/transform.py +0 -0
  74. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/resolution/__init__.py +0 -0
  75. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/resolution/conflict.py +0 -0
  76. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/resolution/engine.py +0 -0
  77. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/resolution/strategies.py +0 -0
  78. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/schema/__init__.py +0 -0
  79. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/schema/conflict.py +0 -0
  80. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/schema/manager.py +0 -0
  81. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/schema/registry.py +0 -0
  82. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/schema/versioning.py +0 -0
  83. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/snapshot/__init__.py +0 -0
  84. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/snapshot/diff.py +0 -0
  85. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/snapshot/snapshot.py +0 -0
  86. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/source/__init__.py +0 -0
  87. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/source/manifest.py +0 -0
  88. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/source/registry.py +0 -0
  89. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/source/source.py +0 -0
  90. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/toolkit/__init__.py +0 -0
  91. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/toolkit/file_ops.py +0 -0
  92. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/src/katalyst_engine/toolkit/rendering.py +0 -0
  93. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/bundle/__init__.py +0 -0
  94. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/bundle/test_discovery.py +0 -0
  95. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/bundle/test_loader.py +0 -0
  96. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/bundle/test_loader_policy.py +0 -0
  97. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/bundle/test_path_loader.py +0 -0
  98. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/bundle/test_protocol.py +0 -0
  99. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/conftest.py +0 -0
  100. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/core/__init__.py +0 -0
  101. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/core/test_compatibility.py +0 -0
  102. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/core/test_compositional.py +0 -0
  103. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/core/test_definitive.py +0 -0
  104. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/core/test_evolvable.py +0 -0
  105. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/core/test_identity.py +0 -0
  106. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/core/test_init.py +0 -0
  107. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/core/test_lifecycle.py +0 -0
  108. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/core/test_matching.py +0 -0
  109. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/core/test_matching_consolidation.py +0 -0
  110. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/core/test_relation.py +0 -0
  111. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/core/test_relation_selectors.py +0 -0
  112. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/core/test_version.py +0 -0
  113. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/discovery/__init__.py +0 -0
  114. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/discovery/test_declaration.py +0 -0
  115. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/discovery/test_dispatcher.py +0 -0
  116. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/events/__init__.py +0 -0
  117. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/events/test_bus.py +0 -0
  118. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/evolution/__init__.py +0 -0
  119. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/evolution/test_gate.py +0 -0
  120. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/evolution/test_migration.py +0 -0
  121. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/extensions/__init__.py +0 -0
  122. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/extensions/test_effector.py +0 -0
  123. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/extensions/test_provider.py +0 -0
  124. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/extensions/test_registry.py +0 -0
  125. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/model/__init__.py +0 -0
  126. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/model/test_materializer.py +0 -0
  127. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/model/test_node.py +0 -0
  128. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/model/test_query.py +0 -0
  129. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/model/test_store.py +0 -0
  130. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/replication/__init__.py +0 -0
  131. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/replication/test_engine.py +0 -0
  132. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/resolution/__init__.py +0 -0
  133. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/resolution/test_engine.py +0 -0
  134. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/resolution/test_strategies.py +0 -0
  135. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/schema/__init__.py +0 -0
  136. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/schema/test_definition.py +0 -0
  137. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/schema/test_engine_boundary_guard.py +0 -0
  138. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/schema/test_field_role.py +0 -0
  139. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/schema/test_fieldspec_allowed_values.py +0 -0
  140. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/schema/test_manager.py +0 -0
  141. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/schema/test_plugin_settings.py +0 -0
  142. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/schema/test_registry.py +0 -0
  143. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/schema/test_registry_provenance.py +0 -0
  144. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/schema/test_storage_policy.py +0 -0
  145. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/schema/test_versioning.py +0 -0
  146. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/snapshot/__init__.py +0 -0
  147. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/snapshot/test_diff.py +0 -0
  148. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/snapshot/test_snapshot.py +0 -0
  149. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/source/__init__.py +0 -0
  150. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/source/test_source.py +0 -0
  151. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/test_version.py +0 -0
  152. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/toolkit/__init__.py +0 -0
  153. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/toolkit/test_file_ops.py +0 -0
  154. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/toolkit/test_linkml_bundle.py +0 -0
  155. {katalyst_engine-5.0.1a2 → katalyst_engine-5.0.1a4}/tests/toolkit/test_rendering.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: katalyst-engine
3
- Version: 5.0.1a2
3
+ Version: 5.0.1a4
4
4
  Summary: A distributed declaration framework — primitives for schema-aware, multi-source, evolvable artifact graphs
5
5
  Project-URL: Repository, https://github.com/esimplicityinc/katalyst-taxonomy
6
6
  Author: eSimplicity
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
15
  Classifier: Typing :: Typed
16
16
  Requires-Python: >=3.12
17
- Requires-Dist: katalyst-core<1,>=0.1
17
+ Requires-Dist: katalyst-core<1,>=0.2
18
18
  Requires-Dist: pydantic>=2.0
19
19
  Requires-Dist: pyyaml>=6
20
20
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "katalyst-engine"
3
- version = "5.0.1a2"
3
+ version = "5.0.1a4"
4
4
  description = "A distributed declaration framework — primitives for schema-aware, multi-source, evolvable artifact graphs"
5
5
  readme = "README.md"
6
6
  license = { text = "Proprietary" }
@@ -25,7 +25,7 @@ dependencies = [
25
25
  "pydantic>=2.0",
26
26
  # LinkML is the only schema source: bundles build their definitions from the
27
27
  # LinkML modules katalyst-core opens (toolkit/linkml.py).
28
- "katalyst-core>=0.1,<1",
28
+ "katalyst-core>=0.2,<1",
29
29
  "pyyaml>=6",
30
30
  ]
31
31
 
@@ -13,8 +13,8 @@ from katalyst_engine.bundle.discovery import (
13
13
  from katalyst_engine.bundle.loader import BundleLoader
14
14
  from katalyst_engine.bundle.policy import (
15
15
  BundleLoadPolicy,
16
+ BundleRequirementError,
16
17
  BundleVersionConflictError,
17
- constraint_is_satisfied,
18
18
  )
19
19
  from katalyst_engine.bundle.protocol import (
20
20
  Bundle,
@@ -30,9 +30,9 @@ __all__ = [
30
30
  "BundleLoadPolicy",
31
31
  "BundleLoader",
32
32
  "BundleManifest",
33
+ "BundleRequirementError",
33
34
  "BundleVersionConflictError",
34
35
  "DiscoveredBundle",
35
- "constraint_is_satisfied",
36
36
  "get_creation_templates",
37
37
  "get_scaffold_templates",
38
38
  ]
@@ -10,7 +10,9 @@ Loading is governed by a :class:`~katalyst_engine.bundle.policy.BundleLoadPolicy
10
10
  :attr:`BundleManifest.version` the moment the manifest is read, provenance is
11
11
  recorded so the registry can attribute every kind to a bundle, and
12
12
  ``BundleManifest.entry_point`` is populated for bundles that arrived over an
13
- entry point so provenance is reportable.
13
+ entry point so provenance is reportable. Once every bundle is registered, each one's
14
+ declared ranges (:attr:`BundleManifest.requires`) are checked against katalyst-core and
15
+ the other loaded bundles (:meth:`BundleLoader.check_requirements`).
14
16
  """
15
17
 
16
18
  from __future__ import annotations
@@ -19,9 +21,10 @@ import logging
19
21
  from dataclasses import dataclass, field
20
22
 
21
23
  from katalyst_engine.bundle.discovery import BundleDiscovery, DiscoveredBundle
22
- from katalyst_engine.bundle.policy import BundleLoadPolicy
24
+ from katalyst_engine.bundle.policy import BundleLoadPolicy, BundleRequirementError
23
25
  from katalyst_engine.bundle.protocol import Bundle, BundleManifest
24
26
  from katalyst_engine.core.errors import EngineConfigurationError
27
+ from katalyst_engine.core.version import Version
25
28
  from katalyst_engine.schema.manager import SchemaManager
26
29
 
27
30
  logger = logging.getLogger(__name__)
@@ -107,6 +110,7 @@ class BundleLoader:
107
110
  manifest = manifest.model_copy(update={"entry_point": entry_point})
108
111
 
109
112
  provider = name or discovered_name or fqn
113
+ self._register_core_relationships()
110
114
  with self.manager.registry.providing(provider):
111
115
  bundle.register(self.manager)
112
116
 
@@ -122,6 +126,19 @@ class BundleLoader:
122
126
 
123
127
  return manifest
124
128
 
129
+ def _register_core_relationships(self) -> None:
130
+ """katalyst-core's relationship types (``child_of``), once and unattributed.
131
+
132
+ Every bundle uses them and none defines them, so they belong to no provider and
133
+ never conflict; a view narrowed to some bundles keeps them.
134
+ """
135
+ from katalyst_engine.toolkit.linkml import core_relationships
136
+
137
+ registry = self.manager.registry
138
+ for rel_def in core_relationships():
139
+ if registry.get_relationship(rel_def.describes_kind) is None:
140
+ registry.register_relationship(rel_def)
141
+
125
142
  def discover_and_register(
126
143
  self,
127
144
  discovery: BundleDiscovery | None = None,
@@ -177,8 +194,46 @@ class BundleLoader:
177
194
  ) from exc
178
195
  logger.exception("Failed to load bundle %r", discovered.name)
179
196
 
197
+ self.check_requirements()
180
198
  return manifests
181
199
 
200
+ def loaded_versions(self) -> dict[str, Version]:
201
+ """katalyst-core's version and each loaded bundle's, by schema name."""
202
+ from katalyst_engine.evolution.release import core_release
203
+
204
+ core = core_release()
205
+ versions = {core.name: core.version}
206
+ for manifest in self._manifests.values():
207
+ versions[manifest.identity.name] = manifest.version
208
+ return versions
209
+
210
+ def check_requirements(self) -> None:
211
+ """Refuse a loaded bundle whose declared ranges the loaded schemas don't meet.
212
+
213
+ Raises:
214
+ BundleRequirementError: For the first such bundle (by FQN), naming each range
215
+ and the loaded version.
216
+ """
217
+ from katalyst_engine.evolution.ranges import (
218
+ SchemaRangeError,
219
+ check_requirements,
220
+ parse_requirements,
221
+ )
222
+
223
+ loaded = self.loaded_versions()
224
+ for fqn in sorted(self._manifests):
225
+ manifest = self._manifests[fqn]
226
+ if not manifest.requires:
227
+ continue
228
+ name = manifest.identity.name
229
+ try:
230
+ ranges = parse_requirements(dict(manifest.requires), where="requires")
231
+ except SchemaRangeError as exc:
232
+ raise BundleRequirementError(name, (), detail=str(exc)) from exc
233
+ unsatisfied = check_requirements(ranges, loaded)
234
+ if unsatisfied:
235
+ raise BundleRequirementError(name, unsatisfied)
236
+
182
237
  def register_discovered(self, discovered: DiscoveredBundle) -> BundleManifest:
183
238
  """Load a single DiscoveredBundle and register it.
184
239
 
@@ -10,18 +10,9 @@ Deliberately in the engine and deliberately free of any notion of a profile, a
10
10
  config document or a filesystem: it is a mapping from bundle name to constraint
11
11
  plus a conflict policy, and nothing else.
12
12
 
13
- Constraint grammar, widest to narrowest:
14
-
15
- ``*``
16
- Anything.
17
- ``^1.2.0``
18
- Semver-compatible, delegated to :meth:`Version.is_compatible_with` so the
19
- "0.x is only compatible with itself" rule is stated in exactly one place.
20
- ``>=1.0.0,<2.0.0`` / ``==1.0.0`` / ``>1.0`` …
21
- Delegated to :meth:`VersionRange.parse`.
22
- ``0.1.0``
23
- A bare version is an exact pin — the spelling ``spec.schemas[].version``
24
- uses in the acceptance specs.
13
+ A constraint is a schema range: the one grammar and check of
14
+ :mod:`katalyst_engine.evolution.ranges` (``^1.2``, ``>=1.0,<2``, ``*``; a bare full version
15
+ such as ``0.1.0`` is an exact pin, the spelling ``spec.schemas[].version`` uses).
25
16
  """
26
17
 
27
18
  from __future__ import annotations
@@ -30,13 +21,14 @@ from dataclasses import dataclass, field
30
21
  from typing import Mapping
31
22
 
32
23
  from katalyst_engine.core.errors import EngineConfigurationError
33
- from katalyst_engine.core.version import Version, VersionRange
24
+ from katalyst_engine.core.version import Version
25
+ from katalyst_engine.evolution.ranges import SchemaRange, SchemaRangeError, Unsatisfied, refusal
34
26
  from katalyst_engine.schema.conflict import ConflictPolicy
35
27
 
36
28
  __all__ = [
37
29
  "BundleLoadPolicy",
30
+ "BundleRequirementError",
38
31
  "BundleVersionConflictError",
39
- "constraint_is_satisfied",
40
32
  ]
41
33
 
42
34
 
@@ -60,26 +52,30 @@ class BundleVersionConflictError(EngineConfigurationError):
60
52
  )
61
53
 
62
54
 
63
- def _empty_constraints() -> Mapping[str, str]:
64
- """Typed empty-mapping factory — a bare ``lambda: {}`` is partially unknown."""
65
- return {}
55
+ class BundleRequirementError(EngineConfigurationError):
56
+ """A loaded bundle's declared range (:attr:`BundleManifest.requires`) is not met.
57
+
58
+ The message names the bundle, each range and the loaded version (or that the schema is
59
+ not loaded): ``schema bundle 'x' supports katalyst-core ^0.2, but katalyst-core 1.0.0
60
+ is loaded``.
61
+ """
66
62
 
63
+ def __init__(self, name: str, unsatisfied: tuple[Unsatisfied, ...], detail: str = "") -> None:
64
+ self.name = name
65
+ self.unsatisfied = unsatisfied
66
+ if unsatisfied:
67
+ message = (
68
+ refusal(f"schema bundle {name!r}", unsatisfied)
69
+ + ": install versions it supports, or a release of it that supports these"
70
+ )
71
+ else:
72
+ message = f"schema bundle {name!r}: {detail}"
73
+ super().__init__(message)
67
74
 
68
- def constraint_is_satisfied(constraint: str, version: Version) -> bool:
69
- """Whether *version* satisfies *constraint*.
70
75
 
71
- Raises:
72
- ValueError: If *constraint* cannot be parsed. Callers turn this into a
73
- :class:`BundleVersionConflictError` so the bundle is always named.
74
- """
75
- text = constraint.strip()
76
- if not text or text == "*":
77
- return True
78
- if text.startswith("^"):
79
- return version.is_compatible_with(Version.parse(text[1:]))
80
- if text[0] in "<>=":
81
- return VersionRange.parse(text).contains(version)
82
- return version == Version.parse(text)
76
+ def _empty_constraints() -> Mapping[str, str]:
77
+ """Typed empty-mapping factory — a bare ``lambda: {}`` is partially unknown."""
78
+ return {}
83
79
 
84
80
 
85
81
  @dataclass(frozen=True, slots=True)
@@ -135,8 +131,8 @@ class BundleLoadPolicy:
135
131
  return
136
132
  label = next((name for name in names if name), "<unnamed>")
137
133
  try:
138
- satisfied = constraint_is_satisfied(constraint, version)
139
- except ValueError as exc:
134
+ satisfied = SchemaRange.parse(constraint).contains(version)
135
+ except SchemaRangeError as exc:
140
136
  raise BundleVersionConflictError(
141
137
  label, constraint, version, detail=f"unparseable constraint: {exc}"
142
138
  ) from exc
@@ -19,10 +19,11 @@ Bundles may optionally declare templates:
19
19
 
20
20
  from __future__ import annotations
21
21
 
22
+ from collections.abc import Mapping
22
23
  from pathlib import Path
23
24
  from typing import Any, Protocol, cast, runtime_checkable
24
25
 
25
- from pydantic import BaseModel
26
+ from pydantic import BaseModel, Field
26
27
 
27
28
  from katalyst_engine.core.identity import Identity
28
29
  from katalyst_engine.core.version import Version
@@ -64,6 +65,12 @@ class BundleManifest(BaseModel, frozen=True):
64
65
  description: str = ""
65
66
  """Human-readable description of what this bundle provides."""
66
67
 
68
+ requires: Mapping[str, str] = Field(default_factory=dict[str, str])
69
+ """The other schemas this bundle builds on, and the versions of each it supports: a
70
+ schema name (``katalyst-core``, another bundle's manifest name) to a range
71
+ (:mod:`katalyst_engine.evolution.ranges`), e.g. ``{"katalyst-core": "^0.2"}``. The
72
+ loader refuses the bundle when a loaded schema is outside its range, or not loaded."""
73
+
67
74
  entry_point: str = ""
68
75
  """The entry point string that loaded this bundle (if discovered)."""
69
76
 
@@ -7,8 +7,29 @@ its version bump and its migration.
7
7
  - :mod:`.migration`: the migration file (``migrations/<from>-to-<to>.yaml``) and the
8
8
  pure function that applies its steps to documents in memory.
9
9
  - :mod:`.gate`: the version gate CI runs (``kata core schema check``).
10
+ - :mod:`.lock`: the versions a backend records (its schema lock), against the loaded ones.
11
+ - :mod:`.changelog`: a release's schema changes as markdown, from the diff and the migration.
12
+ - :mod:`.chain`: the chain of migrations from a recorded version to the loaded one, run
13
+ a step at a time.
14
+ - :mod:`.ranges`: the schema ranges a dependent declares (``{name: "^9"}``), and the one
15
+ check that refuses it when the loaded schemas don't satisfy them.
10
16
  """
11
17
 
18
+ from katalyst_engine.evolution.chain import (
19
+ ChainPlan,
20
+ MigrationChainError,
21
+ SchemaChain,
22
+ StepOutcome,
23
+ describe_step,
24
+ migration_chain,
25
+ release_migrations,
26
+ run_chains,
27
+ )
28
+ from katalyst_engine.evolution.changelog import (
29
+ SchemaChangelog,
30
+ build_changelog,
31
+ render_changelog,
32
+ )
12
33
  from katalyst_engine.evolution.diff import (
13
34
  Category,
14
35
  Impact,
@@ -18,6 +39,15 @@ from katalyst_engine.evolution.diff import (
18
39
  diff_modules,
19
40
  )
20
41
  from katalyst_engine.evolution.gate import GateReport, check_release, migration_skeleton
42
+ from katalyst_engine.evolution.lock import (
43
+ LOCK_FILE_NAME,
44
+ BundleStatus,
45
+ SchemaLock,
46
+ SchemaLockError,
47
+ SchemaState,
48
+ SchemaStatus,
49
+ compare_lock,
50
+ )
21
51
  from katalyst_engine.evolution.migration import (
22
52
  CODE_HOOK_GROUP,
23
53
  Document,
@@ -28,7 +58,16 @@ from katalyst_engine.evolution.migration import (
28
58
  apply_migration,
29
59
  load_migration,
30
60
  )
61
+ from katalyst_engine.evolution.ranges import (
62
+ SchemaRange,
63
+ SchemaRangeError,
64
+ Unsatisfied,
65
+ check_requirements,
66
+ parse_requirements,
67
+ refusal,
68
+ )
31
69
  from katalyst_engine.evolution.release import (
70
+ ArchiveState,
32
71
  SchemaRelease,
33
72
  SchemaVersionMismatchError,
34
73
  bundle_release,
@@ -38,25 +77,50 @@ from katalyst_engine.evolution.release import (
38
77
 
39
78
  __all__ = [
40
79
  "CODE_HOOK_GROUP",
80
+ "LOCK_FILE_NAME",
81
+ "ArchiveState",
82
+ "BundleStatus",
41
83
  "Category",
84
+ "ChainPlan",
42
85
  "Document",
43
86
  "DocumentSet",
44
87
  "GateReport",
45
88
  "Impact",
46
89
  "Location",
47
90
  "Migration",
91
+ "MigrationChainError",
48
92
  "MigrationFileError",
49
93
  "MigrationStepError",
94
+ "SchemaChain",
50
95
  "SchemaChange",
96
+ "SchemaChangelog",
51
97
  "SchemaDiff",
98
+ "SchemaLock",
99
+ "SchemaLockError",
100
+ "SchemaRange",
101
+ "SchemaRangeError",
52
102
  "SchemaRelease",
103
+ "SchemaState",
104
+ "SchemaStatus",
53
105
  "SchemaVersionMismatchError",
106
+ "StepOutcome",
107
+ "Unsatisfied",
54
108
  "apply_migration",
109
+ "build_changelog",
55
110
  "bundle_release",
56
111
  "check_release",
112
+ "check_requirements",
113
+ "compare_lock",
57
114
  "core_release",
115
+ "describe_step",
58
116
  "diff_modules",
59
117
  "installed_releases",
60
118
  "load_migration",
119
+ "migration_chain",
61
120
  "migration_skeleton",
121
+ "parse_requirements",
122
+ "refusal",
123
+ "release_migrations",
124
+ "render_changelog",
125
+ "run_chains",
62
126
  ]
@@ -0,0 +1,213 @@
1
+ """Moving documents across releases: the chain of migrations from the version a backend
2
+ is at to the loaded one, and applying it step by step with what each step changed.
3
+
4
+ A schema's migrations live beside its modules (``migrations/<from>-to-<to>.yaml``), one
5
+ per breaking release, each from the release before it. The chain from a version is the
6
+ migration out of its major, then the one out of the next, until the loaded major:
7
+ 8.x to 10.x runs 8 → 9, then 9 → 10. Within a major nothing is migrated.
8
+
9
+ :func:`run_chains` applies the chains of several schemas to one set of documents, in
10
+ order, a step at a time, so a dry run can say what each step did. It is pure: writing
11
+ the result back to a backend is the caller's.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import copy
17
+ import json
18
+ from collections import Counter
19
+ from collections.abc import Callable, Mapping, Sequence
20
+ from dataclasses import dataclass, field
21
+ from typing import Any
22
+
23
+ from katalyst_engine.core.version import Version
24
+ from katalyst_engine.evolution.migration import (
25
+ CodeStep,
26
+ DeleteKind,
27
+ Document,
28
+ DocumentSet,
29
+ DropField,
30
+ MapEnum,
31
+ Migration,
32
+ MoveField,
33
+ MoveFiles,
34
+ RenameField,
35
+ RenameKind,
36
+ RenameRelationship,
37
+ Reparent,
38
+ SetDefault,
39
+ Step,
40
+ apply_migration,
41
+ load_migration,
42
+ )
43
+ from katalyst_engine.evolution.release import SchemaRelease
44
+ from katalyst_engine.schema.definition import NodeDefinition
45
+
46
+ __all__ = [
47
+ "ChainPlan",
48
+ "MigrationChainError",
49
+ "SchemaChain",
50
+ "StepOutcome",
51
+ "describe_step",
52
+ "migration_chain",
53
+ "release_migrations",
54
+ "run_chains",
55
+ ]
56
+
57
+
58
+ class MigrationChainError(ValueError):
59
+ """No chain of migrations leads from a backend's version to the loaded one."""
60
+
61
+
62
+ def release_migrations(release: SchemaRelease) -> tuple[Migration, ...]:
63
+ """Every migration a schema ships, oldest first.
64
+
65
+ Raises:
66
+ MigrationFileError: A migration file does not describe a migration.
67
+ """
68
+ try:
69
+ root = release.migrations_root
70
+ except Exception: # noqa: BLE001 - a schema with no modules has no migrations
71
+ return ()
72
+ if not root.is_dir():
73
+ return ()
74
+ found = [load_migration(path) for path in sorted(root.glob("*.yaml"))]
75
+ return tuple(sorted(found, key=lambda m: m.source))
76
+
77
+
78
+ def migration_chain(
79
+ name: str, migrations: Sequence[Migration], start: Version, target: Version
80
+ ) -> tuple[Migration, ...]:
81
+ """The migrations that take documents written for *start* to *target*'s major.
82
+
83
+ Raises:
84
+ MigrationChainError: *start* is newer than *target*, or a major has no
85
+ migration out of it.
86
+ """
87
+ if start.major > target.major:
88
+ raise MigrationChainError(
89
+ f"{name}: documents at {start} are newer than the loaded {target}"
90
+ )
91
+ chain: list[Migration] = []
92
+ current = start
93
+ while current.major < target.major:
94
+ out = [
95
+ m
96
+ for m in migrations
97
+ if m.source.major == current.major and current.major < m.target.major <= target.major
98
+ ]
99
+ if not out:
100
+ raise MigrationChainError(
101
+ f"{name}: no migration from {current.major}.x towards {target} "
102
+ f"(the schema ships {', '.join(f'{m.from_}-to-{m.to}' for m in migrations) or 'none'})"
103
+ )
104
+ if len(out) > 1:
105
+ listed = ", ".join(f"{m.from_}-to-{m.to}" for m in out)
106
+ raise MigrationChainError(f"{name}: more than one migration from {current}: {listed}")
107
+ chain.append(out[0])
108
+ current = out[0].target
109
+ return tuple(chain)
110
+
111
+
112
+ def _field(kind: str | None, path: str) -> str:
113
+ return f"{kind}.{path}" if kind else path
114
+
115
+
116
+ def describe_step(step: Step) -> str:
117
+ """One line for a step, as a dry run lists it: ``rename_field service.tags → labels``."""
118
+ if isinstance(step, RenameKind):
119
+ return f"rename_kind {step.from_} → {step.to}"
120
+ if isinstance(step, RenameField):
121
+ return f"rename_field {_field(step.kind, step.from_)} → {step.to}"
122
+ if isinstance(step, MoveField):
123
+ return f"move_field {_field(step.kind, step.from_)} → {step.to}"
124
+ if isinstance(step, SetDefault):
125
+ return f"set_default {_field(step.kind, step.field)} = {json.dumps(step.value)}"
126
+ if isinstance(step, DropField):
127
+ return f"drop_field {_field(step.kind, step.field)}"
128
+ if isinstance(step, DeleteKind):
129
+ return f"delete_kind {step.kind}"
130
+ if isinstance(step, MapEnum):
131
+ values = ", ".join(f"{k} → {v}" for k, v in step.values.items())
132
+ return f"map_enum {_field(step.kind, step.field)} {values}"
133
+ if isinstance(step, Reparent):
134
+ rule = (
135
+ f"to {step.to}"
136
+ if step.to is not None
137
+ else (f"from {step.parent_from}" if step.parent_from else f"up {step.up}")
138
+ )
139
+ return f"reparent {step.kind} {rule}"
140
+ if isinstance(step, MoveFiles):
141
+ return f"move_files {step.kind}"
142
+ if isinstance(step, RenameRelationship):
143
+ return f"rename_relationship {step.from_} → {step.to}"
144
+ assert isinstance(step, CodeStep)
145
+ return f"code {step.hook}"
146
+
147
+
148
+ @dataclass(frozen=True)
149
+ class SchemaChain:
150
+ """One schema's migrations, from the version a backend is at to the loaded one."""
151
+
152
+ name: str
153
+ start: Version
154
+ target: Version
155
+ migrations: tuple[Migration, ...] = ()
156
+
157
+
158
+ @dataclass(frozen=True)
159
+ class StepOutcome:
160
+ """What one step did: how many documents it wrote (new or changed) and removed."""
161
+
162
+ schema: str
163
+ migration: Migration
164
+ step: Step
165
+ written: int
166
+ removed: int
167
+
168
+ @property
169
+ def label(self) -> str:
170
+ return describe_step(self.step)
171
+
172
+
173
+ @dataclass
174
+ class ChainPlan:
175
+ """Documents after every chain, and what each step did."""
176
+
177
+ documents: DocumentSet
178
+ outcomes: list[StepOutcome] = field(default_factory=lambda: list[StepOutcome]())
179
+
180
+
181
+ def _key(doc: Document) -> str:
182
+ return json.dumps([doc.path, doc.data], sort_keys=True, default=str)
183
+
184
+
185
+ def run_chains(
186
+ chains: Sequence[SchemaChain],
187
+ documents: DocumentSet,
188
+ *,
189
+ nodes: Mapping[str, NodeDefinition] | None = None,
190
+ hooks: Mapping[str, Callable[..., Any]] | None = None,
191
+ ) -> ChainPlan:
192
+ """Apply each chain's migrations to *documents*, a step at a time. The input is not
193
+ changed.
194
+
195
+ Raises:
196
+ MigrationStepError: A step cannot be applied.
197
+ """
198
+ current = documents
199
+ outcomes: list[StepOutcome] = []
200
+ for chain in chains:
201
+ for migration in chain.migrations:
202
+ for step in migration.steps:
203
+ single = migration.model_copy(update={"steps": (step,)})
204
+ after = apply_migration(single, current, nodes=nodes, hooks=hooks)
205
+ before_keys = Counter(_key(d) for d in current.documents)
206
+ after_keys = Counter(_key(d) for d in after.documents)
207
+ written = sum((after_keys - before_keys).values())
208
+ removed = max(0, len(current.documents) - len(after.documents))
209
+ outcomes.append(StepOutcome(chain.name, migration, step, written, removed))
210
+ current = after
211
+ if current is documents:
212
+ current = copy.deepcopy(documents)
213
+ return ChainPlan(documents=current, outcomes=outcomes)