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,91 @@
|
|
|
1
|
+
"""Conflict detection models for multi-source declaration resolution.
|
|
2
|
+
|
|
3
|
+
When multiple sources declare the same identity, a Conflict captures
|
|
4
|
+
the competing declarations and their severity. A ConflictReport
|
|
5
|
+
aggregates all conflicts from a resolution pass.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from enum import Enum
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, computed_field
|
|
13
|
+
|
|
14
|
+
from katalyst_engine.core.identity import Identity
|
|
15
|
+
from katalyst_engine.discovery.declaration import Declaration
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ConflictSeverity(str, Enum):
|
|
19
|
+
"""How serious a conflict between declarations is.
|
|
20
|
+
|
|
21
|
+
INFO: cosmetic differences only (e.g. whitespace, ordering).
|
|
22
|
+
WARNING: non-critical field differences that may be auto-merged.
|
|
23
|
+
ERROR: contradictory values that require explicit resolution.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
INFO = "info"
|
|
27
|
+
WARNING = "warning"
|
|
28
|
+
ERROR = "error"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Conflict(BaseModel, frozen=True):
|
|
32
|
+
"""A detected conflict between declarations from different sources.
|
|
33
|
+
|
|
34
|
+
Two or more sources have provided declarations for the same identity.
|
|
35
|
+
The conflict captures which declarations disagree and which fields
|
|
36
|
+
are in contention.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
identity: Identity
|
|
40
|
+
"""The identity that multiple sources declare."""
|
|
41
|
+
|
|
42
|
+
declarations: tuple[Declaration, ...]
|
|
43
|
+
"""The competing declarations, ordered by source priority (highest first)."""
|
|
44
|
+
|
|
45
|
+
severity: ConflictSeverity = ConflictSeverity.ERROR
|
|
46
|
+
"""How serious this conflict is."""
|
|
47
|
+
|
|
48
|
+
conflicting_fields: frozenset[str] = frozenset()
|
|
49
|
+
"""Field names where the declarations disagree."""
|
|
50
|
+
|
|
51
|
+
message: str = ""
|
|
52
|
+
"""Human-readable description of the conflict."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ConflictReport(BaseModel, frozen=True):
|
|
56
|
+
"""Aggregation of all conflicts from a resolution pass.
|
|
57
|
+
|
|
58
|
+
Provides summary counts by severity and lookup by identity.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
conflicts: tuple[Conflict, ...] = ()
|
|
62
|
+
"""All detected conflicts."""
|
|
63
|
+
|
|
64
|
+
@computed_field # type: ignore[prop-decorator]
|
|
65
|
+
@property
|
|
66
|
+
def error_count(self) -> int:
|
|
67
|
+
"""Number of ERROR-severity conflicts."""
|
|
68
|
+
return sum(1 for c in self.conflicts if c.severity == ConflictSeverity.ERROR)
|
|
69
|
+
|
|
70
|
+
@computed_field # type: ignore[prop-decorator]
|
|
71
|
+
@property
|
|
72
|
+
def warning_count(self) -> int:
|
|
73
|
+
"""Number of WARNING-severity conflicts."""
|
|
74
|
+
return sum(1 for c in self.conflicts if c.severity == ConflictSeverity.WARNING)
|
|
75
|
+
|
|
76
|
+
@computed_field # type: ignore[prop-decorator]
|
|
77
|
+
@property
|
|
78
|
+
def total(self) -> int:
|
|
79
|
+
"""Total number of conflicts."""
|
|
80
|
+
return len(self.conflicts)
|
|
81
|
+
|
|
82
|
+
def has_errors(self) -> bool:
|
|
83
|
+
"""True if any ERROR-severity conflicts exist."""
|
|
84
|
+
return self.error_count > 0
|
|
85
|
+
|
|
86
|
+
def for_identity(self, identity: Identity) -> Conflict | None:
|
|
87
|
+
"""Look up the conflict for a specific identity, if any."""
|
|
88
|
+
for conflict in self.conflicts:
|
|
89
|
+
if conflict.identity == identity:
|
|
90
|
+
return conflict
|
|
91
|
+
return None
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Resolution engine — groups declarations, detects conflicts, applies strategies.
|
|
2
|
+
|
|
3
|
+
The ResolutionEngine takes declarations from multiple sources, groups
|
|
4
|
+
them by identity, detects where sources disagree, and applies resolution
|
|
5
|
+
strategies in priority order to produce a single resolved declaration
|
|
6
|
+
per identity.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
from katalyst_engine.discovery.declaration import Declaration
|
|
14
|
+
from katalyst_engine.resolution.conflict import (
|
|
15
|
+
Conflict,
|
|
16
|
+
ConflictReport,
|
|
17
|
+
ConflictSeverity,
|
|
18
|
+
)
|
|
19
|
+
from katalyst_engine.resolution.strategies import ResolutionStrategy
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ResolutionResult(BaseModel, frozen=True):
|
|
23
|
+
"""Output of a resolution pass.
|
|
24
|
+
|
|
25
|
+
Contains the resolved declarations (one per identity) and the
|
|
26
|
+
conflict report describing any disagreements found.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
resolved: tuple[Declaration, ...] = ()
|
|
30
|
+
"""One resolved declaration per identity."""
|
|
31
|
+
|
|
32
|
+
conflict_report: ConflictReport = ConflictReport()
|
|
33
|
+
"""All conflicts detected, including those that were resolved."""
|
|
34
|
+
|
|
35
|
+
unresolved_identities: frozenset[str] = frozenset()
|
|
36
|
+
"""FQNs of identities that could not be resolved by any strategy."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ResolutionEngine:
|
|
40
|
+
"""Groups declarations by identity, detects conflicts, applies strategies.
|
|
41
|
+
|
|
42
|
+
Usage:
|
|
43
|
+
engine = ResolutionEngine()
|
|
44
|
+
engine.add_strategy(CanonicalSourceStrategy(registry))
|
|
45
|
+
engine.add_strategy(MergeStrategy())
|
|
46
|
+
result = engine.resolve(declarations)
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self) -> None:
|
|
50
|
+
self._strategies: list[ResolutionStrategy] = []
|
|
51
|
+
|
|
52
|
+
def add_strategy(self, strategy: ResolutionStrategy) -> None:
|
|
53
|
+
"""Add a resolution strategy. Strategies are tried in insertion order."""
|
|
54
|
+
self._strategies.append(strategy)
|
|
55
|
+
|
|
56
|
+
def resolve(self, declarations: list[Declaration]) -> ResolutionResult:
|
|
57
|
+
"""Resolve a list of declarations from potentially multiple sources.
|
|
58
|
+
|
|
59
|
+
Groups declarations by identity FQN, detects conflicts where
|
|
60
|
+
multiple sources declare the same identity, and applies strategies
|
|
61
|
+
in order until one resolves each conflict.
|
|
62
|
+
|
|
63
|
+
Declarations with no conflicts pass through unchanged.
|
|
64
|
+
"""
|
|
65
|
+
groups = self._group_by_identity(declarations)
|
|
66
|
+
|
|
67
|
+
resolved: list[Declaration] = []
|
|
68
|
+
conflicts: list[Conflict] = []
|
|
69
|
+
unresolved: list[str] = []
|
|
70
|
+
|
|
71
|
+
for fqn, group in groups.items():
|
|
72
|
+
if len(group) == 1:
|
|
73
|
+
resolved.append(group[0])
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
conflict = self._detect_conflict(group)
|
|
77
|
+
conflicts.append(conflict)
|
|
78
|
+
|
|
79
|
+
winner = self._apply_strategies(conflict)
|
|
80
|
+
if winner is not None:
|
|
81
|
+
resolved.append(winner)
|
|
82
|
+
else:
|
|
83
|
+
unresolved.append(fqn)
|
|
84
|
+
|
|
85
|
+
return ResolutionResult(
|
|
86
|
+
resolved=tuple(resolved),
|
|
87
|
+
conflict_report=ConflictReport(conflicts=tuple(conflicts)),
|
|
88
|
+
unresolved_identities=frozenset(unresolved),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def _group_by_identity(self, declarations: list[Declaration]) -> dict[str, list[Declaration]]:
|
|
92
|
+
"""Group declarations by identity FQN."""
|
|
93
|
+
groups: dict[str, list[Declaration]] = {}
|
|
94
|
+
for decl in declarations:
|
|
95
|
+
fqn = decl.identity.fqn
|
|
96
|
+
if fqn not in groups:
|
|
97
|
+
groups[fqn] = []
|
|
98
|
+
groups[fqn].append(decl)
|
|
99
|
+
return groups
|
|
100
|
+
|
|
101
|
+
def _detect_conflict(self, declarations: list[Declaration]) -> Conflict:
|
|
102
|
+
"""Detect which fields conflict between declarations sharing an identity."""
|
|
103
|
+
identity = declarations[0].identity
|
|
104
|
+
all_keys: set[str] = set()
|
|
105
|
+
for decl in declarations:
|
|
106
|
+
all_keys.update(decl.raw_data.keys())
|
|
107
|
+
|
|
108
|
+
conflicting: set[str] = set()
|
|
109
|
+
for key in all_keys:
|
|
110
|
+
values = {decl.raw_data.get(key) for decl in declarations if key in decl.raw_data}
|
|
111
|
+
if len(values) > 1:
|
|
112
|
+
conflicting.add(key)
|
|
113
|
+
|
|
114
|
+
severity = ConflictSeverity.ERROR if conflicting else ConflictSeverity.WARNING
|
|
115
|
+
sources = ", ".join(d.source_fqn for d in declarations)
|
|
116
|
+
|
|
117
|
+
return Conflict(
|
|
118
|
+
identity=identity,
|
|
119
|
+
declarations=tuple(declarations),
|
|
120
|
+
severity=severity,
|
|
121
|
+
conflicting_fields=frozenset(conflicting),
|
|
122
|
+
message=f"Identity {identity.fqn} declared by multiple sources: {sources}",
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def _apply_strategies(self, conflict: Conflict) -> Declaration | None:
|
|
126
|
+
"""Try each strategy in order; return the first successful resolution."""
|
|
127
|
+
for strategy in self._strategies:
|
|
128
|
+
result = strategy.resolve(conflict)
|
|
129
|
+
if result is not None:
|
|
130
|
+
return result
|
|
131
|
+
return None
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Resolution strategies for choosing between conflicting declarations.
|
|
2
|
+
|
|
3
|
+
Each strategy implements a different policy for picking a winner
|
|
4
|
+
when multiple sources declare the same identity. Strategies are
|
|
5
|
+
applied in priority order by the ResolutionEngine.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
|
|
12
|
+
from katalyst_engine.discovery.declaration import Declaration
|
|
13
|
+
from katalyst_engine.resolution.conflict import Conflict
|
|
14
|
+
from katalyst_engine.source.registry import SourceRegistry
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ResolutionStrategy(ABC):
|
|
18
|
+
"""Abstract base for conflict resolution strategies.
|
|
19
|
+
|
|
20
|
+
Implementations examine a Conflict and return the winning
|
|
21
|
+
Declaration, or None if they cannot resolve it.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def resolve(self, conflict: Conflict) -> Declaration | None:
|
|
26
|
+
"""Attempt to resolve a conflict.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
conflict: The conflict to resolve.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
The winning Declaration, or None if this strategy
|
|
33
|
+
cannot resolve the conflict.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CanonicalSourceStrategy(ResolutionStrategy):
|
|
38
|
+
"""Resolve by choosing the declaration from the highest-priority source.
|
|
39
|
+
|
|
40
|
+
Uses the SourceRegistry to look up source priorities. The declaration
|
|
41
|
+
from the source with the highest canonicity priority wins.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, source_registry: SourceRegistry) -> None:
|
|
45
|
+
self._registry = source_registry
|
|
46
|
+
|
|
47
|
+
def resolve(self, conflict: Conflict) -> Declaration | None:
|
|
48
|
+
"""Pick the declaration from the highest-priority canonical source."""
|
|
49
|
+
best: Declaration | None = None
|
|
50
|
+
best_priority = -1
|
|
51
|
+
|
|
52
|
+
for decl in conflict.declarations:
|
|
53
|
+
source = self._registry.get_by_fqn(decl.source_fqn)
|
|
54
|
+
if source is None:
|
|
55
|
+
continue
|
|
56
|
+
priority = source.canonicity.priority
|
|
57
|
+
if priority > best_priority:
|
|
58
|
+
best = decl
|
|
59
|
+
best_priority = priority
|
|
60
|
+
|
|
61
|
+
return best
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class LatestVersionStrategy(ResolutionStrategy):
|
|
65
|
+
"""Resolve by choosing the declaration with the highest version in raw_data.
|
|
66
|
+
|
|
67
|
+
Expects raw_data to contain a "version" key parseable as a semver string.
|
|
68
|
+
Falls back to string comparison if parsing fails.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def resolve(self, conflict: Conflict) -> Declaration | None:
|
|
72
|
+
"""Pick the declaration with the highest version string."""
|
|
73
|
+
from katalyst_engine.core.version import Version
|
|
74
|
+
|
|
75
|
+
best: Declaration | None = None
|
|
76
|
+
best_version: Version | None = None
|
|
77
|
+
|
|
78
|
+
for decl in conflict.declarations:
|
|
79
|
+
version_str = decl.raw_data.get("version", "")
|
|
80
|
+
if not isinstance(version_str, str) or not version_str:
|
|
81
|
+
continue
|
|
82
|
+
try:
|
|
83
|
+
version = Version.parse(version_str)
|
|
84
|
+
except ValueError:
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
if best_version is None or version > best_version:
|
|
88
|
+
best = decl
|
|
89
|
+
best_version = version
|
|
90
|
+
|
|
91
|
+
return best
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class MergeStrategy(ResolutionStrategy):
|
|
95
|
+
"""Resolve by merging non-conflicting fields from all declarations.
|
|
96
|
+
|
|
97
|
+
If all declarations agree on every field (or a field is only set
|
|
98
|
+
by one source), produces a merged declaration. Returns None if
|
|
99
|
+
any field has genuinely conflicting values.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def resolve(self, conflict: Conflict) -> Declaration | None:
|
|
103
|
+
"""Merge declarations where fields don't conflict.
|
|
104
|
+
|
|
105
|
+
Returns a merged Declaration using the first declaration as
|
|
106
|
+
the base, or None if any field values genuinely conflict.
|
|
107
|
+
"""
|
|
108
|
+
if not conflict.declarations:
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
merged_data: dict[str, object] = {}
|
|
112
|
+
|
|
113
|
+
for decl in conflict.declarations:
|
|
114
|
+
for key, value in decl.raw_data.items():
|
|
115
|
+
if key in merged_data:
|
|
116
|
+
if merged_data[key] != value:
|
|
117
|
+
return None # genuine conflict
|
|
118
|
+
else:
|
|
119
|
+
merged_data[key] = value
|
|
120
|
+
|
|
121
|
+
base = conflict.declarations[0]
|
|
122
|
+
return base.model_copy(update={"raw_data": merged_data})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Schema — definitions, registry, versioning, and management."""
|
|
2
|
+
|
|
3
|
+
from katalyst_engine.schema.definition import (
|
|
4
|
+
FieldRef,
|
|
5
|
+
FieldSpec,
|
|
6
|
+
NodeDefinition,
|
|
7
|
+
ParentRule,
|
|
8
|
+
RelationshipDefinition,
|
|
9
|
+
SchemaDefinition,
|
|
10
|
+
WingDefinition,
|
|
11
|
+
)
|
|
12
|
+
from katalyst_engine.schema.manager import SchemaManager
|
|
13
|
+
from katalyst_engine.schema.registry import SchemaRegistry
|
|
14
|
+
from katalyst_engine.schema.versioning import (
|
|
15
|
+
SchemaVersionChange,
|
|
16
|
+
SchemaVersionComparison,
|
|
17
|
+
compare_versions,
|
|
18
|
+
plan_migration_path,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"FieldRef",
|
|
23
|
+
"FieldSpec",
|
|
24
|
+
"NodeDefinition",
|
|
25
|
+
"ParentRule",
|
|
26
|
+
"RelationshipDefinition",
|
|
27
|
+
"SchemaDefinition",
|
|
28
|
+
"SchemaManager",
|
|
29
|
+
"SchemaRegistry",
|
|
30
|
+
"SchemaVersionChange",
|
|
31
|
+
"SchemaVersionComparison",
|
|
32
|
+
"WingDefinition",
|
|
33
|
+
"compare_versions",
|
|
34
|
+
"plan_migration_path",
|
|
35
|
+
]
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""Schema definitions — describe families of node types and their structure.
|
|
2
|
+
|
|
3
|
+
SchemaDefinition is a Definitive that describes an entire schema family.
|
|
4
|
+
NodeDefinition describes a single node kind with field specifications.
|
|
5
|
+
WingDefinition describes a grouping/namespace structure.
|
|
6
|
+
RelationshipDefinition describes a relationship type with classification.
|
|
7
|
+
FieldRef captures a cross-reference from one field to a target kind.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel
|
|
15
|
+
|
|
16
|
+
from katalyst_engine.core.definitive import Definitive
|
|
17
|
+
from katalyst_engine.core.identity import Identity
|
|
18
|
+
from katalyst_engine.core.relation import Cardinality
|
|
19
|
+
from katalyst_engine.core.version import Version
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FieldSpec(BaseModel, frozen=True):
|
|
23
|
+
"""Specification of a single field within a NodeDefinition.
|
|
24
|
+
|
|
25
|
+
Describes the field's type, requirements, and defaults without
|
|
26
|
+
coupling to any particular serialization format.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
name: str
|
|
30
|
+
"""Field name as it appears in the spec dict."""
|
|
31
|
+
|
|
32
|
+
field_type: str = "string"
|
|
33
|
+
"""Type hint: "string", "integer", "boolean", "list", "map", "ref"."""
|
|
34
|
+
|
|
35
|
+
required: bool = False
|
|
36
|
+
"""Whether this field must be present."""
|
|
37
|
+
|
|
38
|
+
default: Any = None
|
|
39
|
+
"""Default value if not provided."""
|
|
40
|
+
|
|
41
|
+
description: str = ""
|
|
42
|
+
"""Human-readable description of this field."""
|
|
43
|
+
|
|
44
|
+
ref_target_kind: str = ""
|
|
45
|
+
"""If field_type is 'ref', the kind of Evolvable it references."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ParentRule(BaseModel, frozen=True):
|
|
49
|
+
"""Declares what kinds are allowed as parents of a node kind.
|
|
50
|
+
|
|
51
|
+
Used by the SchemaRegistry to enforce parent–child relationships.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
child_kind: str
|
|
55
|
+
"""The kind that this rule applies to."""
|
|
56
|
+
|
|
57
|
+
allowed_parent_kinds: frozenset[str] = frozenset()
|
|
58
|
+
"""Kinds allowed as parents. Empty means any kind (unconstrained)."""
|
|
59
|
+
|
|
60
|
+
required: bool = True
|
|
61
|
+
"""Whether a parent is required (True) or optional (False)."""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class FieldRef(BaseModel, frozen=True):
|
|
65
|
+
"""A cross-reference from a field to a target node kind.
|
|
66
|
+
|
|
67
|
+
Captures typed relationships between node kinds that are declared
|
|
68
|
+
via fields (e.g., a ``tool`` field that references node kind ``tool``
|
|
69
|
+
with relationship type ``USES_TOOL``).
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
field_name: str
|
|
73
|
+
"""The field on the source node that holds the reference."""
|
|
74
|
+
|
|
75
|
+
target_kind: str
|
|
76
|
+
"""The node kind being referenced."""
|
|
77
|
+
|
|
78
|
+
rel_type: str = ""
|
|
79
|
+
"""Optional relationship type name (e.g. "USES_TOOL", "BELONGS_TO")."""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class NodeDefinition(Definitive, frozen=True):
|
|
83
|
+
"""A Definitive describing a single node kind.
|
|
84
|
+
|
|
85
|
+
Specifies the fields that instances of this kind carry, which
|
|
86
|
+
wing they belong to, what parent rules apply, and whether this
|
|
87
|
+
kind is a root, leaf, or extension type.
|
|
88
|
+
|
|
89
|
+
This is the canonical location for all node type metadata in the
|
|
90
|
+
engine.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
fields: tuple[FieldSpec, ...] = ()
|
|
94
|
+
"""Ordered field specifications for this kind."""
|
|
95
|
+
|
|
96
|
+
wing: str = ""
|
|
97
|
+
"""Wing this node kind belongs to (e.g. "system", "platform")."""
|
|
98
|
+
|
|
99
|
+
parent_rules: tuple[ParentRule, ...] = ()
|
|
100
|
+
"""Rules governing parent relationships."""
|
|
101
|
+
|
|
102
|
+
directory_name: str = ""
|
|
103
|
+
"""Conventional directory name for filesystem layout (e.g. "layers")."""
|
|
104
|
+
|
|
105
|
+
display_order: int = 0
|
|
106
|
+
"""Ordering hint for UI display."""
|
|
107
|
+
|
|
108
|
+
is_root: bool = False
|
|
109
|
+
"""Whether this is a root node kind (no parent required)."""
|
|
110
|
+
|
|
111
|
+
is_leaf: bool = False
|
|
112
|
+
"""Whether this is a leaf node kind (no children allowed)."""
|
|
113
|
+
|
|
114
|
+
is_extension: bool = False
|
|
115
|
+
"""Whether this kind extends other node kinds (like a plugin/overlay)."""
|
|
116
|
+
|
|
117
|
+
extends_kinds: tuple[str, ...] = ()
|
|
118
|
+
"""If is_extension, the node kinds this extension can target."""
|
|
119
|
+
|
|
120
|
+
cross_references: tuple[FieldRef, ...] = ()
|
|
121
|
+
"""Typed references from fields on this kind to other node kinds."""
|
|
122
|
+
|
|
123
|
+
scaffoldable: bool = False
|
|
124
|
+
"""Whether instances of this kind support scaffolding (project generation)."""
|
|
125
|
+
|
|
126
|
+
clonable: bool = False
|
|
127
|
+
"""Whether instances of this kind can be cloned."""
|
|
128
|
+
|
|
129
|
+
display_label: str = ""
|
|
130
|
+
"""Human-readable singular label (e.g. "System", "Practice Area").
|
|
131
|
+
Falls back to describes_kind.replace('_', ' ').title() if empty."""
|
|
132
|
+
|
|
133
|
+
display_label_plural: str = ""
|
|
134
|
+
"""Human-readable plural label (e.g. "Systems", "Practice Areas").
|
|
135
|
+
Falls back to display_label + 's' if empty."""
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def effective_display_label(self) -> str:
|
|
139
|
+
"""Resolved display label, with fallback to title-cased kind."""
|
|
140
|
+
if self.display_label:
|
|
141
|
+
return self.display_label
|
|
142
|
+
return self.describes_kind.replace("_", " ").title()
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def effective_display_label_plural(self) -> str:
|
|
146
|
+
"""Resolved plural display label, with fallback."""
|
|
147
|
+
if self.display_label_plural:
|
|
148
|
+
return self.display_label_plural
|
|
149
|
+
label = self.effective_display_label
|
|
150
|
+
# Simple English pluralization
|
|
151
|
+
if label.endswith("y") and not label.endswith("ey"):
|
|
152
|
+
return label[:-1] + "ies"
|
|
153
|
+
if label.endswith(("s", "sh", "ch", "x", "z")):
|
|
154
|
+
return label + "es"
|
|
155
|
+
return label + "s"
|
|
156
|
+
|
|
157
|
+
def field_by_name(self, name: str) -> FieldSpec | None:
|
|
158
|
+
"""Look up a field spec by name."""
|
|
159
|
+
for field in self.fields:
|
|
160
|
+
if field.name == name:
|
|
161
|
+
return field
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
def required_fields(self) -> tuple[FieldSpec, ...]:
|
|
165
|
+
"""Return only the required fields."""
|
|
166
|
+
return tuple(f for f in self.fields if f.required)
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def has_parent(self) -> bool:
|
|
170
|
+
"""Whether this node kind can have a parent (has any parent rules)."""
|
|
171
|
+
return len(self.parent_rules) > 0
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def allowed_parent_kinds(self) -> frozenset[str]:
|
|
175
|
+
"""Union of all allowed parent kinds from all parent rules."""
|
|
176
|
+
result: set[str] = set()
|
|
177
|
+
for rule in self.parent_rules:
|
|
178
|
+
result.update(rule.allowed_parent_kinds)
|
|
179
|
+
return frozenset(result)
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def parent_required(self) -> bool:
|
|
183
|
+
"""Whether any parent rule requires a parent."""
|
|
184
|
+
return any(rule.required for rule in self.parent_rules)
|
|
185
|
+
|
|
186
|
+
def cross_ref_for_field(self, field_name: str) -> FieldRef | None:
|
|
187
|
+
"""Look up a cross-reference by field name."""
|
|
188
|
+
for ref in self.cross_references:
|
|
189
|
+
if ref.field_name == field_name:
|
|
190
|
+
return ref
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class WingDefinition(Definitive, frozen=True):
|
|
195
|
+
"""A Definitive describing a grouping/wing.
|
|
196
|
+
|
|
197
|
+
Wings organize node kinds into logical groups (e.g. "system" wing
|
|
198
|
+
contains systems, stacks, layers). A WingDefinition declares which
|
|
199
|
+
node kinds belong to it and their display ordering.
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
member_kinds: tuple[str, ...] = ()
|
|
203
|
+
"""Node kinds belonging to this wing, in display order."""
|
|
204
|
+
|
|
205
|
+
display_name: str = ""
|
|
206
|
+
"""Human-readable name for the wing."""
|
|
207
|
+
|
|
208
|
+
icon: str = ""
|
|
209
|
+
"""Optional icon identifier for UI rendering."""
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class RelationshipDefinition(Definitive, frozen=True):
|
|
213
|
+
"""A Definitive describing a relationship type.
|
|
214
|
+
|
|
215
|
+
Describes a type of relationship (edge) that can exist between
|
|
216
|
+
nodes in the taxonomy graph. Relationship definitions carry
|
|
217
|
+
classification metadata (wing, cardinality, labels) that enables
|
|
218
|
+
grouping, filtering, and display without constraining graph
|
|
219
|
+
operations.
|
|
220
|
+
|
|
221
|
+
Relationship wings are independent of node wings — they describe
|
|
222
|
+
the semantic domain of the relationship itself (e.g. "structural",
|
|
223
|
+
"capability", "verification"), not the wings of the connected nodes.
|
|
224
|
+
|
|
225
|
+
The ``describes_kind`` is the canonical snake_case identifier
|
|
226
|
+
(e.g. "provides_capability"). The graph label (SCREAMING_SNAKE_CASE)
|
|
227
|
+
is derived via the ``graph_label`` property.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
description: str = ""
|
|
231
|
+
"""Human-readable description of this relationship type."""
|
|
232
|
+
|
|
233
|
+
wing: str = ""
|
|
234
|
+
"""Relationship wing for classification (e.g. "structural", "capability")."""
|
|
235
|
+
|
|
236
|
+
display_order: int = 0
|
|
237
|
+
"""Ordering hint for UI display within a wing."""
|
|
238
|
+
|
|
239
|
+
cardinality: Cardinality = Cardinality.MANY_TO_MANY
|
|
240
|
+
"""Expected cardinality of instances of this relationship type."""
|
|
241
|
+
|
|
242
|
+
directed: bool = True
|
|
243
|
+
"""Whether this is a directed relationship. Almost always True."""
|
|
244
|
+
|
|
245
|
+
structural: bool = False
|
|
246
|
+
"""Whether this is a structural relationship (parent, extends, depends_on, deploys_to)."""
|
|
247
|
+
|
|
248
|
+
@property
|
|
249
|
+
def graph_label(self) -> str:
|
|
250
|
+
"""SCREAMING_SNAKE_CASE label for graph export.
|
|
251
|
+
|
|
252
|
+
Derived from ``describes_kind`` by upper-casing.
|
|
253
|
+
|
|
254
|
+
Examples:
|
|
255
|
+
provides_capability -> PROVIDES_CAPABILITY
|
|
256
|
+
child_of -> CHILD_OF
|
|
257
|
+
depends_on -> DEPENDS_ON
|
|
258
|
+
"""
|
|
259
|
+
return self.describes_kind.upper()
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class SchemaDefinition(Definitive, frozen=True):
|
|
263
|
+
"""A Definitive describing an entire schema family.
|
|
264
|
+
|
|
265
|
+
A SchemaDefinition is the top-level container that groups
|
|
266
|
+
NodeDefinitions and WingDefinitions into a coherent schema
|
|
267
|
+
version. It is itself a Definitive so it can be versioned,
|
|
268
|
+
deprecated, and migrated.
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
schema_version: Version = Version()
|
|
272
|
+
"""The version of this schema."""
|
|
273
|
+
|
|
274
|
+
node_definitions: tuple[Identity, ...] = ()
|
|
275
|
+
"""References to NodeDefinitions belonging to this schema."""
|
|
276
|
+
|
|
277
|
+
wing_definitions: tuple[Identity, ...] = ()
|
|
278
|
+
"""References to WingDefinitions belonging to this schema."""
|
|
279
|
+
|
|
280
|
+
api_version: str = ""
|
|
281
|
+
"""API version string (e.g. "v2alpha") for document compatibility."""
|