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.
- katalyst_engine/__init__.py +6 -0
- katalyst_engine/bundle/__init__.py +30 -0
- katalyst_engine/bundle/discovery.py +158 -0
- katalyst_engine/bundle/loader.py +134 -0
- katalyst_engine/bundle/protocol.py +209 -0
- katalyst_engine/core/__init__.py +62 -0
- katalyst_engine/core/compatibility.py +58 -0
- katalyst_engine/core/compositional.py +103 -0
- katalyst_engine/core/definitive.py +195 -0
- katalyst_engine/core/evolvable.py +89 -0
- katalyst_engine/core/identity.py +95 -0
- katalyst_engine/core/lifecycle.py +62 -0
- katalyst_engine/core/relation.py +151 -0
- katalyst_engine/core/version.py +203 -0
- katalyst_engine/discovery/__init__.py +20 -0
- katalyst_engine/discovery/declaration.py +74 -0
- katalyst_engine/discovery/dispatcher.py +83 -0
- katalyst_engine/discovery/protocol.py +69 -0
- katalyst_engine/events/__init__.py +10 -0
- katalyst_engine/events/bus.py +102 -0
- katalyst_engine/events/event.py +82 -0
- katalyst_engine/extensions/__init__.py +32 -0
- katalyst_engine/extensions/capability.py +45 -0
- katalyst_engine/extensions/discovery.py +85 -0
- katalyst_engine/extensions/effector.py +54 -0
- katalyst_engine/extensions/provider.py +33 -0
- katalyst_engine/extensions/registry.py +77 -0
- katalyst_engine/extensions/trigger.py +64 -0
- katalyst_engine/model/__init__.py +25 -0
- katalyst_engine/model/manager.py +85 -0
- katalyst_engine/model/materializer.py +78 -0
- katalyst_engine/model/node.py +49 -0
- katalyst_engine/model/query.py +186 -0
- katalyst_engine/model/store.py +119 -0
- katalyst_engine/py.typed +0 -0
- katalyst_engine/replication/__init__.py +30 -0
- katalyst_engine/replication/engine.py +104 -0
- katalyst_engine/replication/job.py +88 -0
- katalyst_engine/replication/transform.py +111 -0
- katalyst_engine/resolution/__init__.py +32 -0
- katalyst_engine/resolution/conflict.py +91 -0
- katalyst_engine/resolution/engine.py +131 -0
- katalyst_engine/resolution/strategies.py +122 -0
- katalyst_engine/schema/__init__.py +35 -0
- katalyst_engine/schema/definition.py +281 -0
- katalyst_engine/schema/manager.py +95 -0
- katalyst_engine/schema/registry.py +367 -0
- katalyst_engine/schema/versioning.py +115 -0
- katalyst_engine/snapshot/__init__.py +18 -0
- katalyst_engine/snapshot/diff.py +94 -0
- katalyst_engine/snapshot/snapshot.py +111 -0
- katalyst_engine/source/__init__.py +26 -0
- katalyst_engine/source/manifest.py +45 -0
- katalyst_engine/source/registry.py +122 -0
- katalyst_engine/source/source.py +92 -0
- katalyst_engine/toolkit/__init__.py +22 -0
- katalyst_engine/toolkit/file_ops.py +194 -0
- katalyst_engine/toolkit/rendering.py +58 -0
- katalyst_engine-2.0.0.dist-info/METADATA +50 -0
- katalyst_engine-2.0.0.dist-info/RECORD +61 -0
- katalyst_engine-2.0.0.dist-info/WHEEL +4 -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."""
|