katalyst-engine 2.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 (61) hide show
  1. katalyst_engine/__init__.py +6 -0
  2. katalyst_engine/bundle/__init__.py +30 -0
  3. katalyst_engine/bundle/discovery.py +158 -0
  4. katalyst_engine/bundle/loader.py +134 -0
  5. katalyst_engine/bundle/protocol.py +209 -0
  6. katalyst_engine/core/__init__.py +62 -0
  7. katalyst_engine/core/compatibility.py +58 -0
  8. katalyst_engine/core/compositional.py +103 -0
  9. katalyst_engine/core/definitive.py +195 -0
  10. katalyst_engine/core/evolvable.py +89 -0
  11. katalyst_engine/core/identity.py +95 -0
  12. katalyst_engine/core/lifecycle.py +62 -0
  13. katalyst_engine/core/relation.py +151 -0
  14. katalyst_engine/core/version.py +203 -0
  15. katalyst_engine/discovery/__init__.py +20 -0
  16. katalyst_engine/discovery/declaration.py +74 -0
  17. katalyst_engine/discovery/dispatcher.py +83 -0
  18. katalyst_engine/discovery/protocol.py +69 -0
  19. katalyst_engine/events/__init__.py +10 -0
  20. katalyst_engine/events/bus.py +102 -0
  21. katalyst_engine/events/event.py +82 -0
  22. katalyst_engine/extensions/__init__.py +32 -0
  23. katalyst_engine/extensions/capability.py +45 -0
  24. katalyst_engine/extensions/discovery.py +85 -0
  25. katalyst_engine/extensions/effector.py +54 -0
  26. katalyst_engine/extensions/provider.py +33 -0
  27. katalyst_engine/extensions/registry.py +77 -0
  28. katalyst_engine/extensions/trigger.py +64 -0
  29. katalyst_engine/model/__init__.py +25 -0
  30. katalyst_engine/model/manager.py +85 -0
  31. katalyst_engine/model/materializer.py +78 -0
  32. katalyst_engine/model/node.py +49 -0
  33. katalyst_engine/model/query.py +186 -0
  34. katalyst_engine/model/store.py +119 -0
  35. katalyst_engine/py.typed +0 -0
  36. katalyst_engine/replication/__init__.py +30 -0
  37. katalyst_engine/replication/engine.py +104 -0
  38. katalyst_engine/replication/job.py +88 -0
  39. katalyst_engine/replication/transform.py +111 -0
  40. katalyst_engine/resolution/__init__.py +32 -0
  41. katalyst_engine/resolution/conflict.py +91 -0
  42. katalyst_engine/resolution/engine.py +131 -0
  43. katalyst_engine/resolution/strategies.py +122 -0
  44. katalyst_engine/schema/__init__.py +35 -0
  45. katalyst_engine/schema/definition.py +281 -0
  46. katalyst_engine/schema/manager.py +95 -0
  47. katalyst_engine/schema/registry.py +367 -0
  48. katalyst_engine/schema/versioning.py +115 -0
  49. katalyst_engine/snapshot/__init__.py +18 -0
  50. katalyst_engine/snapshot/diff.py +94 -0
  51. katalyst_engine/snapshot/snapshot.py +111 -0
  52. katalyst_engine/source/__init__.py +26 -0
  53. katalyst_engine/source/manifest.py +45 -0
  54. katalyst_engine/source/registry.py +122 -0
  55. katalyst_engine/source/source.py +92 -0
  56. katalyst_engine/toolkit/__init__.py +22 -0
  57. katalyst_engine/toolkit/file_ops.py +194 -0
  58. katalyst_engine/toolkit/rendering.py +58 -0
  59. katalyst_engine-2.0.0.dist-info/METADATA +50 -0
  60. katalyst_engine-2.0.0.dist-info/RECORD +61 -0
  61. katalyst_engine-2.0.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,6 @@
1
+ """katalyst-engine — A distributed declaration framework.
2
+
3
+ Primitives for schema-aware, multi-source, evolvable artifact graphs.
4
+ """
5
+
6
+ __version__ = "0.1.0"
@@ -0,0 +1,30 @@
1
+ """Bundle — standard packaging for node type registration.
2
+
3
+ A Bundle is a self-describing unit that contributes node types, wing
4
+ definitions, and schema metadata to the engine. Bundles are discovered
5
+ via Python entry points and registered with a SchemaManager.
6
+ """
7
+
8
+ from katalyst_engine.bundle.discovery import (
9
+ BUNDLE_ENTRY_POINT_GROUP,
10
+ BundleDiscovery,
11
+ DiscoveredBundle,
12
+ )
13
+ from katalyst_engine.bundle.loader import BundleLoader
14
+ from katalyst_engine.bundle.protocol import (
15
+ Bundle,
16
+ BundleManifest,
17
+ get_creation_templates,
18
+ get_scaffold_templates,
19
+ )
20
+
21
+ __all__ = [
22
+ "BUNDLE_ENTRY_POINT_GROUP",
23
+ "Bundle",
24
+ "BundleDiscovery",
25
+ "BundleLoader",
26
+ "BundleManifest",
27
+ "DiscoveredBundle",
28
+ "get_creation_templates",
29
+ "get_scaffold_templates",
30
+ ]
@@ -0,0 +1,158 @@
1
+ """Bundle discovery — find bundles via Python entry points.
2
+
3
+ Scans ``importlib.metadata`` entry points in the
4
+ ``katalyst_engine.bundles`` group to discover installed bundles.
5
+ Each entry point should point to a callable that returns a Bundle
6
+ instance (typically the Bundle class itself).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import importlib.metadata
12
+ import logging
13
+ from dataclasses import dataclass, field
14
+
15
+ from katalyst_engine.bundle.protocol import Bundle
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ #: Default entry point group for bundle discovery.
20
+ BUNDLE_ENTRY_POINT_GROUP = "katalyst_engine.bundles"
21
+
22
+
23
+ @dataclass(frozen=True, slots=True)
24
+ class DiscoveredBundle:
25
+ """Metadata about a discovered (but not yet loaded) bundle.
26
+
27
+ Represents the location of a bundle found during entry point
28
+ scanning. The ``load()`` method instantiates the actual Bundle.
29
+ """
30
+
31
+ name: str
32
+ """Entry point name (e.g. "forge")."""
33
+
34
+ entry_point: str
35
+ """Dotted module path (e.g. "katalyst_taxonomy.bundles.forge:ForgeBundle")."""
36
+
37
+ group: str = BUNDLE_ENTRY_POINT_GROUP
38
+ """Entry point group this was discovered from."""
39
+
40
+ def load(self) -> Bundle:
41
+ """Load and instantiate the bundle from its entry point.
42
+
43
+ The entry point must be a callable that returns a Bundle
44
+ instance. If it's a class, it will be instantiated with
45
+ no arguments.
46
+
47
+ Raises:
48
+ ImportError: If the module cannot be imported.
49
+ TypeError: If the loaded object is not a valid Bundle.
50
+ """
51
+ module_path, _, attr_name = self.entry_point.rpartition(":")
52
+ if not attr_name:
53
+ # Entry point format is "module.path" (no colon)
54
+ module_path = self.entry_point
55
+ attr_name = ""
56
+
57
+ module = importlib.import_module(module_path)
58
+ if attr_name:
59
+ factory = getattr(module, attr_name)
60
+ else:
61
+ factory = module
62
+
63
+ # If it's a class, instantiate it; if callable, call it
64
+ if isinstance(factory, type):
65
+ return factory() # type: ignore[return-value]
66
+ if callable(factory):
67
+ return factory() # type: ignore[return-value]
68
+ # If it's already a Bundle instance, return it
69
+ return factory # type: ignore[return-value]
70
+
71
+
72
+ @dataclass(slots=True)
73
+ class BundleDiscovery:
74
+ """Discovers bundles from Python entry points.
75
+
76
+ Scans the ``katalyst_engine.bundles`` entry point group to find
77
+ installed bundle packages. Results are cached after first scan.
78
+
79
+ Usage::
80
+
81
+ discovery = BundleDiscovery()
82
+ discovery.scan() # or scan happens lazily on first access
83
+ for db in discovery.all():
84
+ bundle = db.load()
85
+ bundle.register(schema_manager)
86
+ """
87
+
88
+ _discovered: dict[str, DiscoveredBundle] = field(
89
+ default_factory=lambda: dict[str, DiscoveredBundle]()
90
+ )
91
+ _scanned: bool = False
92
+ group: str = BUNDLE_ENTRY_POINT_GROUP
93
+
94
+ def scan(self) -> None:
95
+ """Scan entry points for bundles.
96
+
97
+ Clears any previous results and re-scans. Uses
98
+ ``importlib.metadata.entry_points()`` to discover bundles.
99
+ """
100
+ self._discovered.clear()
101
+
102
+ try:
103
+ eps = importlib.metadata.entry_points(group=self.group)
104
+ except Exception:
105
+ logger.warning("Failed to scan entry points for group %s", self.group)
106
+ self._scanned = True
107
+ return
108
+
109
+ for ep in eps:
110
+ db = DiscoveredBundle(
111
+ name=ep.name,
112
+ entry_point=ep.value,
113
+ group=self.group,
114
+ )
115
+ self._discovered[ep.name] = db
116
+ logger.debug("Discovered bundle: %s -> %s", ep.name, ep.value)
117
+
118
+ self._scanned = True
119
+
120
+ def all(self) -> list[DiscoveredBundle]:
121
+ """Return all discovered bundles, sorted by name.
122
+
123
+ Triggers a scan if one hasn't happened yet.
124
+ """
125
+ if not self._scanned:
126
+ self.scan()
127
+ return sorted(self._discovered.values(), key=lambda db: db.name)
128
+
129
+ def get(self, name: str) -> DiscoveredBundle | None:
130
+ """Look up a discovered bundle by name.
131
+
132
+ Triggers a scan if one hasn't happened yet.
133
+ """
134
+ if not self._scanned:
135
+ self.scan()
136
+ return self._discovered.get(name)
137
+
138
+ @property
139
+ def count(self) -> int:
140
+ """Number of discovered bundles."""
141
+ if not self._scanned:
142
+ self.scan()
143
+ return len(self._discovered)
144
+
145
+ def add(self, bundle: DiscoveredBundle) -> None:
146
+ """Manually add a discovered bundle (for testing or explicit registration).
147
+
148
+ If no scan has happened yet, marks as scanned to prevent a
149
+ lazy scan from clearing manually-added entries.
150
+ """
151
+ if not self._scanned:
152
+ self._scanned = True
153
+ self._discovered[bundle.name] = bundle
154
+
155
+ def clear(self) -> None:
156
+ """Remove all discovered bundles and reset scan state."""
157
+ self._discovered.clear()
158
+ self._scanned = False
@@ -0,0 +1,134 @@
1
+ """Bundle loader — discover and register all installed bundles.
2
+
3
+ Provides a high-level ``load_bundles()`` function that discovers
4
+ bundles via entry points, loads them, and registers them with
5
+ a SchemaManager. This is the standard startup path for any
6
+ application using the engine.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from dataclasses import dataclass, field
13
+
14
+ from katalyst_engine.bundle.discovery import BundleDiscovery, DiscoveredBundle
15
+ from katalyst_engine.bundle.protocol import Bundle, BundleManifest
16
+ from katalyst_engine.schema.manager import SchemaManager
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ @dataclass(slots=True)
22
+ class BundleLoader:
23
+ """Loads and registers bundles with a SchemaManager.
24
+
25
+ Manages the lifecycle of bundle discovery → loading → registration.
26
+ Tracks which bundles have been loaded and their manifests.
27
+
28
+ Usage::
29
+
30
+ loader = BundleLoader(schema_manager)
31
+ loader.discover_and_register()
32
+ # Or manually:
33
+ loader.register(my_bundle)
34
+ """
35
+
36
+ manager: SchemaManager
37
+ """The SchemaManager to register bundles with."""
38
+
39
+ _loaded: dict[str, Bundle] = field(default_factory=lambda: dict[str, Bundle]())
40
+ _manifests: dict[str, BundleManifest] = field(
41
+ default_factory=lambda: dict[str, BundleManifest]()
42
+ )
43
+
44
+ def register(self, bundle: Bundle) -> BundleManifest:
45
+ """Register a single bundle with the SchemaManager.
46
+
47
+ Args:
48
+ bundle: A Bundle instance to register.
49
+
50
+ Returns:
51
+ The bundle's manifest.
52
+
53
+ Raises:
54
+ ValueError: If a bundle with the same identity is already loaded.
55
+ """
56
+ manifest = bundle.manifest
57
+ fqn = manifest.identity.fqn
58
+
59
+ if fqn in self._loaded:
60
+ raise ValueError(
61
+ f"Bundle already registered: {fqn!r}. "
62
+ f"Unregister it first or use a different identity."
63
+ )
64
+
65
+ bundle.register(self.manager)
66
+ self._loaded[fqn] = bundle
67
+ self._manifests[fqn] = manifest
68
+
69
+ logger.info(
70
+ "Registered bundle %r: %d kinds, %d wings",
71
+ fqn,
72
+ len(manifest.provided_kinds),
73
+ len(manifest.wing_names),
74
+ )
75
+
76
+ return manifest
77
+
78
+ def discover_and_register(
79
+ self,
80
+ discovery: BundleDiscovery | None = None,
81
+ ) -> list[BundleManifest]:
82
+ """Discover bundles via entry points and register them all.
83
+
84
+ Args:
85
+ discovery: Optional pre-configured BundleDiscovery.
86
+ If None, creates a new one and scans entry points.
87
+
88
+ Returns:
89
+ List of manifests for all successfully registered bundles.
90
+ """
91
+ if discovery is None:
92
+ discovery = BundleDiscovery()
93
+ discovery.scan()
94
+
95
+ manifests: list[BundleManifest] = []
96
+
97
+ for discovered in discovery.all():
98
+ try:
99
+ bundle = discovered.load()
100
+ manifest = self.register(bundle)
101
+ manifests.append(manifest)
102
+ except Exception:
103
+ logger.exception("Failed to load bundle %r", discovered.name)
104
+
105
+ return manifests
106
+
107
+ def register_discovered(self, discovered: DiscoveredBundle) -> BundleManifest:
108
+ """Load a single DiscoveredBundle and register it.
109
+
110
+ Args:
111
+ discovered: A discovered bundle to load and register.
112
+
113
+ Returns:
114
+ The bundle's manifest.
115
+ """
116
+ bundle = discovered.load()
117
+ return self.register(bundle)
118
+
119
+ def get_manifest(self, fqn: str) -> BundleManifest | None:
120
+ """Look up a loaded bundle's manifest by FQN."""
121
+ return self._manifests.get(fqn)
122
+
123
+ def all_manifests(self) -> list[BundleManifest]:
124
+ """Return all loaded bundle manifests, sorted by FQN."""
125
+ return sorted(self._manifests.values(), key=lambda m: m.identity.fqn)
126
+
127
+ def is_loaded(self, fqn: str) -> bool:
128
+ """Check if a bundle with the given FQN is loaded."""
129
+ return fqn in self._loaded
130
+
131
+ @property
132
+ def loaded_count(self) -> int:
133
+ """Number of loaded bundles."""
134
+ return len(self._loaded)
@@ -0,0 +1,209 @@
1
+ """Bundle protocol — the standard unit of schema packaging.
2
+
3
+ A Bundle is a self-describing package of node types, wing definitions,
4
+ and schema metadata that can be discovered and registered with the
5
+ engine. Bundles are the mechanism by which different domains contribute
6
+ their types to a shared engine.
7
+
8
+ The Forge bundle for software delivery is one bundle. A security
9
+ compliance package could be another. Each defines a ``Bundle`` class
10
+ that the engine discovers via Python entry points.
11
+
12
+ Bundles may optionally declare templates:
13
+
14
+ - **Creation templates** — Jinja2-rendered YAML templates that produce the
15
+ taxonomy YAML document when a node of a given kind is created.
16
+ - **Scaffold templates** — directory paths containing file trees that a
17
+ scaffolding plugin copies when a node with a scaffold variant is created.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from pathlib import Path
23
+ from typing import Any, Protocol, cast, runtime_checkable
24
+
25
+ from pydantic import BaseModel
26
+
27
+ from katalyst_engine.core.identity import Identity
28
+ from katalyst_engine.core.version import Version
29
+ from katalyst_engine.schema.definition import (
30
+ NodeDefinition,
31
+ SchemaDefinition,
32
+ WingDefinition,
33
+ )
34
+ from katalyst_engine.schema.manager import SchemaManager
35
+
36
+
37
+ class BundleManifest(BaseModel, frozen=True):
38
+ """Immutable manifest describing what a bundle provides.
39
+
40
+ This is the metadata about a bundle — its identity, version,
41
+ what kinds it supplies, and what wings it defines. Used for
42
+ querying what's installed without loading the full definitions.
43
+ """
44
+
45
+ identity: Identity
46
+ """Unique identity of this bundle (kind="bundle", name="forge")."""
47
+
48
+ version: Version = Version()
49
+ """Version of this bundle."""
50
+
51
+ provided_kinds: frozenset[str] = frozenset()
52
+ """All node kinds this bundle registers."""
53
+
54
+ wing_names: frozenset[str] = frozenset()
55
+ """All wing names this bundle defines."""
56
+
57
+ description: str = ""
58
+ """Human-readable description of what this bundle provides."""
59
+
60
+ entry_point: str = ""
61
+ """The entry point string that loaded this bundle (if discovered)."""
62
+
63
+
64
+ @runtime_checkable
65
+ class Bundle(Protocol):
66
+ """Protocol for a registerable bundle of node types.
67
+
68
+ Any class that implements this protocol can be discovered and
69
+ registered with the engine. The engine discovers bundles via
70
+ Python entry points (``katalyst_engine.bundles`` group) or
71
+ explicit registration.
72
+
73
+ Minimal implementation::
74
+
75
+ class MyBundle:
76
+ @property
77
+ def manifest(self) -> BundleManifest:
78
+ return BundleManifest(
79
+ identity=Identity(kind="bundle", name="my-bundle"),
80
+ provided_kinds=frozenset({"widget", "gadget"}),
81
+ )
82
+
83
+ def node_definitions(self) -> list[NodeDefinition]:
84
+ return [WIDGET_DEF, GADGET_DEF]
85
+
86
+ def wing_definitions(self) -> list[WingDefinition]:
87
+ return [MY_WING_DEF]
88
+
89
+ def schema_definition(self) -> SchemaDefinition:
90
+ return SchemaDefinition(...)
91
+
92
+ def register(self, manager: SchemaManager) -> None:
93
+ manager.register_schema(
94
+ self.schema_definition(),
95
+ node_definitions=self.node_definitions(),
96
+ wing_definitions=self.wing_definitions(),
97
+ )
98
+
99
+ Bundles may also provide ``relationship_definitions()`` for
100
+ relationship type classification::
101
+
102
+ def relationship_definitions(self) -> list[RelationshipDefinition]:
103
+ return [PARENT_REL, DEPENDS_ON_REL, ...]
104
+
105
+ def register(self, manager: SchemaManager) -> None:
106
+ manager.register_schema(
107
+ self.schema_definition(),
108
+ node_definitions=self.node_definitions(),
109
+ wing_definitions=self.wing_definitions(),
110
+ relationship_definitions=self.relationship_definitions(),
111
+ )
112
+
113
+ Bundles may optionally declare templates for creation and scaffolding::
114
+
115
+ def creation_templates(self) -> dict[str, str]:
116
+ return {"system": "<yaml template content>", ...}
117
+
118
+ def scaffold_templates(self) -> dict[str, Path]:
119
+ return {"layer:app-docker": Path("/path/to/scaffold/dir"), ...}
120
+ """
121
+
122
+ @property
123
+ def manifest(self) -> BundleManifest:
124
+ """Return the bundle's manifest describing what it provides."""
125
+ ...
126
+
127
+ def node_definitions(self) -> list[NodeDefinition]:
128
+ """Return all NodeDefinitions this bundle provides."""
129
+ ...
130
+
131
+ def wing_definitions(self) -> list[WingDefinition]:
132
+ """Return all WingDefinitions this bundle provides."""
133
+ ...
134
+
135
+ def schema_definition(self) -> SchemaDefinition:
136
+ """Return the SchemaDefinition for this bundle."""
137
+ ...
138
+
139
+ def register(self, manager: SchemaManager) -> None:
140
+ """Register this bundle's types with the given SchemaManager.
141
+
142
+ The default pattern is::
143
+
144
+ manager.register_schema(
145
+ self.schema_definition(),
146
+ node_definitions=self.node_definitions(),
147
+ wing_definitions=self.wing_definitions(),
148
+ )
149
+
150
+ Implementations may customize registration (e.g. conditional
151
+ registration, logging, event emission).
152
+ """
153
+ ...
154
+
155
+
156
+ # ---------------------------------------------------------------------------
157
+ # Optional template accessors
158
+ #
159
+ # These are NOT part of the Bundle Protocol (adding them would make
160
+ # isinstance(b, Bundle) fail for any bundle that does not implement them).
161
+ # Instead, bundles that wish to declare templates do so by adding the
162
+ # methods as duck-typed extras, and consumers use the helpers below.
163
+ # ---------------------------------------------------------------------------
164
+
165
+
166
+ def get_creation_templates(bundle: Any) -> dict[str, str]:
167
+ """Safely extract creation templates from a bundle.
168
+
169
+ Bundles that wish to provide creation templates implement a
170
+ ``creation_templates() -> dict[str, str]`` method. This helper
171
+ safely calls that method if present, returning an empty dict
172
+ otherwise.
173
+
174
+ Creation templates are Jinja2-rendered YAML templates that produce
175
+ the taxonomy YAML document when a node of a given kind is created.
176
+ The ``_base`` key is reserved for a fallback template.
177
+
178
+ Returns:
179
+ Mapping of node kind (or ``"_base"``) to Jinja2 template
180
+ content as a string. Empty dict if the bundle has no templates.
181
+ """
182
+ fn = getattr(bundle, "creation_templates", None)
183
+ if fn is not None and callable(fn):
184
+ return cast(dict[str, str], fn())
185
+ return {}
186
+
187
+
188
+ def get_scaffold_templates(bundle: Any) -> dict[str, Path]:
189
+ """Safely extract scaffold templates from a bundle.
190
+
191
+ Bundles that wish to provide scaffold templates implement a
192
+ ``scaffold_templates() -> dict[str, Path]`` method. This helper
193
+ safely calls that method if present, returning an empty dict
194
+ otherwise.
195
+
196
+ Scaffold templates are directory paths containing file trees that
197
+ a scaffolding plugin copies when a node with a scaffold variant is
198
+ created. Keys use the format ``{kind}:{variant}`` — e.g.,
199
+ ``"layer:app-docker"``. For kind-only templates, use just the kind.
200
+
201
+ Returns:
202
+ Mapping of ``kind:variant`` (or just ``kind``) to the directory
203
+ ``Path`` containing the scaffold file tree. Empty dict if the
204
+ bundle has no scaffold templates.
205
+ """
206
+ fn = getattr(bundle, "scaffold_templates", None)
207
+ if fn is not None and callable(fn):
208
+ return cast(dict[str, Path], fn())
209
+ return {}
@@ -0,0 +1,62 @@
1
+ """katalyst-engine core primitives.
2
+
3
+ This is the foundation of the engine. Every design decision here
4
+ cascades through the entire library.
5
+
6
+ The four root primitives:
7
+ - Evolvable: identity + version + lifecycle + compatibility
8
+ - Definitive: describes structure/constraints of OTHER things
9
+ - Compositional: groups/contains other Evolvables
10
+ - Relation: typed, directed, versioned edge between two Evolvables
11
+ """
12
+
13
+ from katalyst_engine.core.compatibility import CompatibilityContract, MigrationPath
14
+ from katalyst_engine.core.compositional import Compositional, MemberConstraint
15
+ from katalyst_engine.core.definitive import (
16
+ Constraint,
17
+ ConstraintSeverity,
18
+ ConstraintViolation,
19
+ Definitive,
20
+ ValidationResult,
21
+ )
22
+ from katalyst_engine.core.evolvable import Evolvable
23
+ from katalyst_engine.core.identity import Identity
24
+ from katalyst_engine.core.lifecycle import Lifecycle, LifecyclePolicy
25
+ from katalyst_engine.core.relation import (
26
+ Cardinality,
27
+ NodeSelector,
28
+ Relation,
29
+ RelationshipClaim,
30
+ )
31
+ from katalyst_engine.core.version import Version, VersionConstraint, VersionRange
32
+
33
+ __all__ = [
34
+ # identity
35
+ "Identity",
36
+ # version
37
+ "Version",
38
+ "VersionRange",
39
+ "VersionConstraint",
40
+ # lifecycle
41
+ "Lifecycle",
42
+ "LifecyclePolicy",
43
+ # compatibility
44
+ "CompatibilityContract",
45
+ "MigrationPath",
46
+ # evolvable
47
+ "Evolvable",
48
+ # definitive
49
+ "Constraint",
50
+ "ConstraintSeverity",
51
+ "ConstraintViolation",
52
+ "Definitive",
53
+ "ValidationResult",
54
+ # compositional
55
+ "Compositional",
56
+ "MemberConstraint",
57
+ # relation
58
+ "Cardinality",
59
+ "NodeSelector",
60
+ "Relation",
61
+ "RelationshipClaim",
62
+ ]
@@ -0,0 +1,58 @@
1
+ """Compatibility contracts and migration paths.
2
+
3
+ Describes what guarantees a version makes about interoperability
4
+ with other versions, and how to transform data between versions.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pydantic import BaseModel
10
+
11
+ from katalyst_engine.core.version import Version
12
+
13
+
14
+ class CompatibilityContract(BaseModel, frozen=True):
15
+ """Compatibility guarantees of a version.
16
+
17
+ Attached to every Evolvable. Describes whether old consumers
18
+ can work with this version and whether this version can
19
+ handle older data.
20
+
21
+ - backward_compatible: old consumers can read new data
22
+ - forward_compatible: new consumers can read old data
23
+ - breaking_changes: list of human-readable descriptions
24
+ """
25
+
26
+ backward_compatible: bool = True
27
+ forward_compatible: bool = False
28
+ breaking_changes: tuple[str, ...] = ()
29
+ notes: str = ""
30
+
31
+
32
+ class MigrationPath(BaseModel, frozen=True):
33
+ """Links version N to version N+1 of an Evolvable.
34
+
35
+ The transform field is a string reference to a callable,
36
+ script, or module. The engine does not execute transforms —
37
+ consumers provide the runtime.
38
+
39
+ MigrationPaths form a directed graph that the schema versioning
40
+ layer uses to plan multi-step migrations (e.g. v1 → v2 → v3).
41
+ """
42
+
43
+ from_version: Version
44
+ to_version: Version
45
+ subject_kind: str
46
+ """The kind of Evolvable this migration applies to."""
47
+
48
+ transform: str
49
+ """Opaque reference to a callable/script that performs the migration."""
50
+
51
+ reversible: bool = False
52
+ """If True, the transform can be applied in reverse."""
53
+
54
+ compatibility: CompatibilityContract = CompatibilityContract()
55
+ """Compatibility guarantees of the target version after migration."""
56
+
57
+ validation_steps: tuple[str, ...] = ()
58
+ """Opaque references to validation callables run after migration."""