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,203 @@
1
+ """Semantic versioning with ranges and constraints.
2
+
3
+ Immutable version objects supporting comparison, parsing, and
4
+ semver-compatible range checking. Pre-release versions sort
5
+ before their release counterpart: 1.0.0-alpha < 1.0.0-beta < 1.0.0.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+
12
+ from pydantic import BaseModel
13
+
14
+ _VERSION_RE = re.compile(
15
+ r"^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)"
16
+ r"(?:-(?P<pre>[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*))?$"
17
+ )
18
+
19
+
20
+ class Version(BaseModel, frozen=True):
21
+ """Semantic version with optional pre-release tag.
22
+
23
+ Immutable. Two versions of the same artifact are two distinct
24
+ Evolvable instances connected by a MigrationPath.
25
+
26
+ Supports comparison operators for sorting and range checks.
27
+ Pre-release versions sort before their release counterpart:
28
+ 1.0.0-alpha < 1.0.0-beta < 1.0.0
29
+ """
30
+
31
+ major: int = 0
32
+ minor: int = 0
33
+ patch: int = 0
34
+ pre: str | None = None
35
+ """Pre-release tag, e.g. "alpha", "beta", "rc.1"."""
36
+
37
+ def __str__(self) -> str:
38
+ """Format as '1.2.3' or '1.2.3-alpha'."""
39
+ base = f"{self.major}.{self.minor}.{self.patch}"
40
+ if self.pre:
41
+ return f"{base}-{self.pre}"
42
+ return base
43
+
44
+ def _cmp_tuple(self) -> tuple[int, int, int, int, str]:
45
+ """Comparison tuple. Pre-release sorts before release (release = ~).
46
+
47
+ The tilde character (~) sorts after all alphanumeric strings,
48
+ so a release version always compares greater than any pre-release.
49
+ """
50
+ if self.pre is None:
51
+ return (self.major, self.minor, self.patch, 1, "")
52
+ return (self.major, self.minor, self.patch, 0, self.pre)
53
+
54
+ def __lt__(self, other: object) -> bool:
55
+ if not isinstance(other, Version):
56
+ return NotImplemented
57
+ return self._cmp_tuple() < other._cmp_tuple()
58
+
59
+ def __le__(self, other: object) -> bool:
60
+ if not isinstance(other, Version):
61
+ return NotImplemented
62
+ return self._cmp_tuple() <= other._cmp_tuple()
63
+
64
+ def __gt__(self, other: object) -> bool:
65
+ if not isinstance(other, Version):
66
+ return NotImplemented
67
+ return self._cmp_tuple() > other._cmp_tuple()
68
+
69
+ def __ge__(self, other: object) -> bool:
70
+ if not isinstance(other, Version):
71
+ return NotImplemented
72
+ return self._cmp_tuple() >= other._cmp_tuple()
73
+
74
+ def is_compatible_with(self, other: Version) -> bool:
75
+ """True if same major version (semver compatibility assumption).
76
+
77
+ Version 0.x is only compatible with itself (0.x is unstable).
78
+ """
79
+ if self.major == 0 or other.major == 0:
80
+ return self.major == other.major and self.minor == other.minor
81
+ return self.major == other.major
82
+
83
+ def next_major(self) -> Version:
84
+ """Return the next major version (e.g. 1.2.3 → 2.0.0)."""
85
+ return Version(major=self.major + 1)
86
+
87
+ def next_minor(self) -> Version:
88
+ """Return the next minor version (e.g. 1.2.3 → 1.3.0)."""
89
+ return Version(major=self.major, minor=self.minor + 1)
90
+
91
+ def next_patch(self) -> Version:
92
+ """Return the next patch version (e.g. 1.2.3 → 1.2.4)."""
93
+ return Version(major=self.major, minor=self.minor, patch=self.patch + 1)
94
+
95
+ @classmethod
96
+ def parse(cls, s: str) -> Version:
97
+ """Parse version strings like '1.2.3', '1.2.3-alpha', '0.1.0'.
98
+
99
+ Raises:
100
+ ValueError: If the string doesn't match the expected format.
101
+ """
102
+ m = _VERSION_RE.match(s.strip())
103
+ if not m:
104
+ raise ValueError(f"Invalid version string: {s!r}")
105
+ return cls(
106
+ major=int(m.group("major")),
107
+ minor=int(m.group("minor")),
108
+ patch=int(m.group("patch")),
109
+ pre=m.group("pre"),
110
+ )
111
+
112
+
113
+ class VersionRange(BaseModel, frozen=True):
114
+ """A range constraint on versions.
115
+
116
+ Used by CanonicityClaims and dependency declarations
117
+ to express "I support versions >=1.0, <2.0".
118
+ """
119
+
120
+ min_version: Version | None = None
121
+ max_version: Version | None = None
122
+ include_min: bool = True
123
+ include_max: bool = False
124
+
125
+ def contains(self, v: Version) -> bool:
126
+ """Check if a version falls within this range."""
127
+ if self.min_version is not None:
128
+ if self.include_min:
129
+ if v < self.min_version:
130
+ return False
131
+ else:
132
+ if v <= self.min_version:
133
+ return False
134
+ if self.max_version is not None:
135
+ if self.include_max:
136
+ if v > self.max_version:
137
+ return False
138
+ else:
139
+ if v >= self.max_version:
140
+ return False
141
+ return True
142
+
143
+ @classmethod
144
+ def parse(cls, s: str) -> VersionRange:
145
+ """Parse version range strings.
146
+
147
+ Supported formats:
148
+ '*' → unbounded (matches everything)
149
+ '==1.0.0' → exact match (min=max, both inclusive)
150
+ '>=1.0.0' → lower-bounded
151
+ '>=1.0.0,<2.0.0' → bounded range
152
+
153
+ Raises:
154
+ ValueError: If the string doesn't match any expected format.
155
+ """
156
+ s = s.strip()
157
+ if s == "*":
158
+ return cls()
159
+
160
+ if s.startswith("=="):
161
+ v = Version.parse(s[2:])
162
+ return cls(min_version=v, max_version=v, include_min=True, include_max=True)
163
+
164
+ parts = [p.strip() for p in s.split(",")]
165
+ min_v: Version | None = None
166
+ max_v: Version | None = None
167
+ inc_min = True
168
+ inc_max = False
169
+
170
+ for part in parts:
171
+ if part.startswith(">="):
172
+ min_v = Version.parse(part[2:])
173
+ inc_min = True
174
+ elif part.startswith(">"):
175
+ min_v = Version.parse(part[1:])
176
+ inc_min = False
177
+ elif part.startswith("<="):
178
+ max_v = Version.parse(part[2:])
179
+ inc_max = True
180
+ elif part.startswith("<"):
181
+ max_v = Version.parse(part[1:])
182
+ inc_max = False
183
+ else:
184
+ raise ValueError(f"Invalid version range component: {part!r}")
185
+
186
+ return cls(
187
+ min_version=min_v,
188
+ max_version=max_v,
189
+ include_min=inc_min,
190
+ include_max=inc_max,
191
+ )
192
+
193
+
194
+ class VersionConstraint(BaseModel, frozen=True):
195
+ """Named version constraint for dependency declarations.
196
+
197
+ Binds a name (identifying what is constrained) to a version range.
198
+ """
199
+
200
+ name: str
201
+ """What this constrains (e.g. "api_version", "schema_version")."""
202
+
203
+ range: VersionRange
@@ -0,0 +1,20 @@
1
+ """Discovery — finding declarations from sources.
2
+
3
+ .. note:: FUTURE: Wire into taxonomy when multi-source node discovery is implemented.
4
+ The taxonomy's filesystem discovery strategies would be refactored into a
5
+ FilesystemDiscoveryProtocol that returns Declaration objects. Pairs with
6
+ source/ and resolution/ as a coherent pipeline.
7
+ """
8
+
9
+ from katalyst_engine.discovery.declaration import Cursor, Declaration, Format
10
+ from katalyst_engine.discovery.dispatcher import DiscoveryDispatcher
11
+ from katalyst_engine.discovery.protocol import DiscoveryProtocol, DiscoveryResult
12
+
13
+ __all__ = [
14
+ "Cursor",
15
+ "Declaration",
16
+ "DiscoveryDispatcher",
17
+ "DiscoveryProtocol",
18
+ "DiscoveryResult",
19
+ "Format",
20
+ ]
@@ -0,0 +1,74 @@
1
+ """Declarations — raw data discovered from sources.
2
+
3
+ A Declaration is the raw, unresolved data unit discovered from a source.
4
+ It carries format metadata and a cursor for incremental discovery.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from enum import Enum
10
+ from typing import Any
11
+
12
+ from pydantic import BaseModel
13
+
14
+ from katalyst_engine.core.identity import Identity
15
+
16
+
17
+ class Format(str, Enum):
18
+ """Serialization format of a discovered declaration."""
19
+
20
+ YAML = "yaml"
21
+ JSON = "json"
22
+ TOML = "toml"
23
+ MARKDOWN = "markdown"
24
+ BINARY = "binary"
25
+ UNKNOWN = "unknown"
26
+
27
+
28
+ class Cursor(BaseModel, frozen=True):
29
+ """Opaque pagination/position marker for incremental discovery.
30
+
31
+ Sources use cursors to resume discovery from a previous position.
32
+ The engine treats cursor values as opaque — only the source
33
+ implementation interprets them.
34
+ """
35
+
36
+ value: str = ""
37
+ """Opaque position marker."""
38
+
39
+ source_fqn: str = ""
40
+ """FQN of the source this cursor belongs to."""
41
+
42
+ def is_empty(self) -> bool:
43
+ """True if this is a fresh/empty cursor."""
44
+ return self.value == ""
45
+
46
+
47
+ class Declaration(BaseModel, frozen=True):
48
+ """A raw, unresolved data unit discovered from a source.
49
+
50
+ Declarations are the input to the resolution engine. They carry
51
+ raw spec data in whatever format the source provides, plus
52
+ metadata about where they came from.
53
+ """
54
+
55
+ identity: Identity
56
+ """Provisional identity assigned during discovery."""
57
+
58
+ source_fqn: str
59
+ """FQN of the source that produced this declaration."""
60
+
61
+ format: Format = Format.YAML
62
+ """Serialization format of the raw_data."""
63
+
64
+ raw_data: dict[str, Any] = {}
65
+ """The raw spec/content as parsed key-value data."""
66
+
67
+ raw_text: str = ""
68
+ """The original text content, if available."""
69
+
70
+ checksum: str = ""
71
+ """Content hash for change detection."""
72
+
73
+ cursor: Cursor = Cursor()
74
+ """Position marker for incremental discovery."""
@@ -0,0 +1,83 @@
1
+ """Discovery dispatcher — routes discovery to correct protocol.
2
+
3
+ The dispatcher maintains a registry of DiscoveryProtocol implementations
4
+ and routes discovery requests to the appropriate protocol based on
5
+ source kind.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from katalyst_engine.discovery.declaration import Cursor, Declaration
11
+ from katalyst_engine.discovery.protocol import DiscoveryProtocol, DiscoveryResult
12
+ from katalyst_engine.source.source import Source
13
+
14
+
15
+ class DiscoveryDispatcher:
16
+ """Routes discovery requests to the correct protocol implementation.
17
+
18
+ Usage:
19
+ dispatcher = DiscoveryDispatcher()
20
+ dispatcher.register(MyFilesystemProtocol())
21
+ result = dispatcher.discover(filesystem_source)
22
+ """
23
+
24
+ def __init__(self) -> None:
25
+ self._protocols: list[DiscoveryProtocol] = []
26
+
27
+ def register(self, protocol: DiscoveryProtocol) -> None:
28
+ """Register a discovery protocol implementation."""
29
+ self._protocols.append(protocol)
30
+
31
+ def discover(
32
+ self,
33
+ source: Source,
34
+ cursor: Cursor | None = None,
35
+ ) -> DiscoveryResult:
36
+ """Dispatch discovery to the first matching protocol.
37
+
38
+ Tries each registered protocol in order. The first one that
39
+ returns True from supports() is used.
40
+
41
+ Returns an empty result with an error if no protocol matches.
42
+ """
43
+ for protocol in self._protocols:
44
+ if protocol.supports(source):
45
+ return protocol.discover(source, cursor)
46
+
47
+ return DiscoveryResult(
48
+ complete=True,
49
+ errors=(f"No discovery protocol registered for source kind: {source.kind}",),
50
+ )
51
+
52
+ def discover_all(
53
+ self,
54
+ source: Source,
55
+ ) -> DiscoveryResult:
56
+ """Discover all declarations from a source (follows cursors).
57
+
58
+ Repeatedly calls discover() until complete, aggregating all
59
+ declarations into a single result.
60
+ """
61
+ all_declarations: list[Declaration] = []
62
+ all_errors: list[str] = []
63
+ cursor: Cursor | None = None
64
+
65
+ while True:
66
+ result = self.discover(source, cursor)
67
+ all_declarations.extend(result.declarations)
68
+ all_errors.extend(result.errors)
69
+
70
+ if result.complete or result.next_cursor is None:
71
+ break
72
+ cursor = result.next_cursor
73
+
74
+ return DiscoveryResult(
75
+ declarations=tuple(all_declarations),
76
+ complete=True,
77
+ errors=tuple(all_errors),
78
+ )
79
+
80
+ @property
81
+ def protocol_count(self) -> int:
82
+ """Number of registered protocols."""
83
+ return len(self._protocols)
@@ -0,0 +1,69 @@
1
+ """Discovery protocol — abstract interface for source adapters.
2
+
3
+ Consumers implement DiscoveryProtocol to teach the engine how to
4
+ find declarations in a specific source kind. The engine never
5
+ does I/O directly — all I/O happens inside protocol implementations.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from abc import ABC, abstractmethod
11
+ from dataclasses import dataclass
12
+
13
+ from katalyst_engine.discovery.declaration import Cursor, Declaration
14
+ from katalyst_engine.source.source import Source
15
+
16
+
17
+ class DiscoveryProtocol(ABC):
18
+ """Abstract base for source discovery implementations.
19
+
20
+ Each source kind (filesystem, database, API) has its own
21
+ DiscoveryProtocol that knows how to enumerate declarations.
22
+
23
+ Implementations must be stateless between calls — all state
24
+ is carried in Cursor objects.
25
+ """
26
+
27
+ @abstractmethod
28
+ def discover(
29
+ self,
30
+ source: Source,
31
+ cursor: Cursor | None = None,
32
+ ) -> DiscoveryResult:
33
+ """Discover declarations from a source.
34
+
35
+ Args:
36
+ source: The source to discover from.
37
+ cursor: Resume from this position. None means start from beginning.
38
+
39
+ Returns:
40
+ A DiscoveryResult with declarations and an optional next cursor.
41
+ """
42
+
43
+ @abstractmethod
44
+ def supports(self, source: Source) -> bool:
45
+ """Check if this protocol can handle the given source."""
46
+
47
+
48
+ @dataclass(frozen=True, slots=True)
49
+ class DiscoveryResult:
50
+ """Result of a discovery pass.
51
+
52
+ Contains the declarations found and an optional cursor for
53
+ resuming discovery (for paginated/incremental sources).
54
+ """
55
+
56
+ declarations: tuple[Declaration, ...] = ()
57
+ next_cursor: Cursor | None = None
58
+ complete: bool = True
59
+ errors: tuple[str, ...] = ()
60
+
61
+ @property
62
+ def count(self) -> int:
63
+ """Number of declarations found."""
64
+ return len(self.declarations)
65
+
66
+ @property
67
+ def has_more(self) -> bool:
68
+ """True if there are more declarations to discover."""
69
+ return not self.complete and self.next_cursor is not None
@@ -0,0 +1,10 @@
1
+ """Event bus — in-process pub/sub for engine lifecycle events."""
2
+
3
+ from katalyst_engine.events.bus import EventBus
4
+ from katalyst_engine.events.event import Event, EventType
5
+
6
+ __all__ = [
7
+ "Event",
8
+ "EventBus",
9
+ "EventType",
10
+ ]
@@ -0,0 +1,102 @@
1
+ """In-process publish/subscribe event bus.
2
+
3
+ Simple, synchronous event dispatch. Every handler sees every event
4
+ (no type-based filtering at the bus level — handlers do their own
5
+ filtering). One failing handler cannot break others.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Callable
11
+ from typing import Any
12
+
13
+ from katalyst_engine.events.event import Event, EventType
14
+
15
+
16
+ class EventBus:
17
+ """Synchronous pub/sub event bus.
18
+
19
+ Handlers are called in subscription order. Exceptions in handlers
20
+ are caught and collected — they never propagate to the publisher.
21
+
22
+ Usage:
23
+ bus = EventBus()
24
+ unsub = bus.subscribe(my_handler)
25
+ bus.publish(Event(type=EventType.NODE_CREATED, properties={"name": "app"}))
26
+ unsub() # stop receiving events
27
+ """
28
+
29
+ def __init__(self) -> None:
30
+ self._handlers: list[Callable[[Event], Any]] = []
31
+ self._type_handlers: dict[EventType, list[Callable[[Event], Any]]] = {}
32
+ self._errors: list[tuple[Event, Exception]] = []
33
+
34
+ def subscribe(
35
+ self,
36
+ handler: Callable[[Event], Any],
37
+ event_type: EventType | None = None,
38
+ ) -> Callable[[], None]:
39
+ """Register a handler. Returns an unsubscribe callable.
40
+
41
+ Args:
42
+ handler: Callable that receives an Event. Return value is ignored.
43
+ event_type: If provided, the handler only receives events of this type.
44
+ If None, the handler receives ALL events.
45
+
46
+ Returns:
47
+ A zero-argument callable that removes this handler.
48
+ """
49
+ if event_type is not None:
50
+ if event_type not in self._type_handlers:
51
+ self._type_handlers[event_type] = []
52
+ self._type_handlers[event_type].append(handler)
53
+
54
+ def _unsub_typed() -> None:
55
+ handlers = self._type_handlers.get(event_type) # type: ignore[arg-type]
56
+ if handlers and handler in handlers:
57
+ handlers.remove(handler)
58
+
59
+ return _unsub_typed
60
+
61
+ self._handlers.append(handler)
62
+
63
+ def _unsub() -> None:
64
+ if handler in self._handlers:
65
+ self._handlers.remove(handler)
66
+
67
+ return _unsub
68
+
69
+ def publish(self, event: Event) -> None:
70
+ """Dispatch an event to all matching handlers.
71
+
72
+ Global handlers (subscribed without event_type) are called first,
73
+ then type-specific handlers. Exceptions are caught and stored in
74
+ the errors list.
75
+ """
76
+ for handler in list(self._handlers):
77
+ try:
78
+ handler(event)
79
+ except Exception as exc:
80
+ self._errors.append((event, exc))
81
+
82
+ type_handlers = self._type_handlers.get(event.type, [])
83
+ for handler in list(type_handlers):
84
+ try:
85
+ handler(event)
86
+ except Exception as exc:
87
+ self._errors.append((event, exc))
88
+
89
+ @property
90
+ def errors(self) -> list[tuple[Event, Exception]]:
91
+ """Events that caused handler exceptions, in order."""
92
+ return list(self._errors)
93
+
94
+ def clear_errors(self) -> None:
95
+ """Clear the error log."""
96
+ self._errors.clear()
97
+
98
+ @property
99
+ def handler_count(self) -> int:
100
+ """Total number of registered handlers (global + typed)."""
101
+ typed_count = sum(len(h) for h in self._type_handlers.values())
102
+ return len(self._handlers) + typed_count
@@ -0,0 +1,82 @@
1
+ """Event types and event data model.
2
+
3
+ Defines the vocabulary of events the engine can emit. Events are
4
+ simple immutable data carriers — the bus handles dispatch.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from enum import Enum
10
+ from typing import Any
11
+
12
+ from pydantic import BaseModel
13
+
14
+
15
+ class EventType(str, Enum):
16
+ """Categories of events the engine can emit.
17
+
18
+ Organized by subsystem. Consumers can subscribe to specific
19
+ types or use pattern matching on the string value.
20
+ """
21
+
22
+ # Lifecycle
23
+ ENGINE_STARTED = "engine.started"
24
+ ENGINE_STOPPED = "engine.stopped"
25
+
26
+ # Source management
27
+ SOURCE_REGISTERED = "source.registered"
28
+ SOURCE_REMOVED = "source.removed"
29
+ SOURCE_SYNC_STARTED = "source.sync.started"
30
+ SOURCE_SYNC_COMPLETED = "source.sync.completed"
31
+ SOURCE_SYNC_FAILED = "source.sync.failed"
32
+
33
+ # Discovery
34
+ DISCOVERY_STARTED = "discovery.started"
35
+ DISCOVERY_COMPLETED = "discovery.completed"
36
+ DECLARATION_FOUND = "discovery.declaration.found"
37
+
38
+ # Schema
39
+ SCHEMA_REGISTERED = "schema.registered"
40
+ SCHEMA_UPDATED = "schema.updated"
41
+ SCHEMA_DEPRECATED = "schema.deprecated"
42
+
43
+ # Model
44
+ NODE_CREATED = "model.node.created"
45
+ NODE_UPDATED = "model.node.updated"
46
+ NODE_DELETED = "model.node.deleted"
47
+ RELATION_CREATED = "model.relation.created"
48
+ RELATION_DELETED = "model.relation.deleted"
49
+
50
+ # Resolution
51
+ CONFLICT_DETECTED = "resolution.conflict.detected"
52
+ CONFLICT_RESOLVED = "resolution.conflict.resolved"
53
+
54
+ # Snapshot
55
+ SNAPSHOT_CREATED = "snapshot.created"
56
+ SNAPSHOT_RESTORED = "snapshot.restored"
57
+
58
+ # Extension
59
+ EXTENSION_REGISTERED = "extension.registered"
60
+ EXTENSION_TRIGGERED = "extension.triggered"
61
+
62
+ # Validation
63
+ VALIDATION_PASSED = "validation.passed"
64
+ VALIDATION_FAILED = "validation.failed"
65
+
66
+
67
+ class Event(BaseModel, frozen=True):
68
+ """An immutable event emitted by the engine.
69
+
70
+ Events carry a type discriminator and a free-form properties
71
+ dict. The engine never inspects properties — consumers use
72
+ them to carry context.
73
+ """
74
+
75
+ type: EventType
76
+ """What happened."""
77
+
78
+ properties: dict[str, Any] = {}
79
+ """Context-specific data about the event."""
80
+
81
+ source: str = ""
82
+ """Optional identifier of the subsystem that emitted this event."""
@@ -0,0 +1,32 @@
1
+ """Extensions — capabilities, providers, effectors, triggers, and discovery.
2
+
3
+ .. note:: WIRING IN PROGRESS: Taxonomy's plugins/ package is a near-1:1 duplicate.
4
+ PluginRegistry should compose ExtensionRegistry. PluginManifest maps to
5
+ Extension. Trigger/Effector would formalize ad-hoc event handling.
6
+ See ARCHITECTURE_CLEANUP.md Part 2.
7
+ """
8
+
9
+ from katalyst_engine.extensions.capability import Capability, Extension
10
+ from katalyst_engine.extensions.discovery import (
11
+ DiscoveredExtension,
12
+ ExtensionDiscovery,
13
+ ExtensionSourceKind,
14
+ )
15
+ from katalyst_engine.extensions.effector import Effector, EffectResult
16
+ from katalyst_engine.extensions.provider import Provider
17
+ from katalyst_engine.extensions.registry import ExtensionRegistry
18
+ from katalyst_engine.extensions.trigger import Trigger, TriggerContext
19
+
20
+ __all__ = [
21
+ "Capability",
22
+ "DiscoveredExtension",
23
+ "EffectResult",
24
+ "Effector",
25
+ "Extension",
26
+ "ExtensionDiscovery",
27
+ "ExtensionRegistry",
28
+ "ExtensionSourceKind",
29
+ "Provider",
30
+ "Trigger",
31
+ "TriggerContext",
32
+ ]