katalyst-plugin-context 0.2.1a10__tar.gz → 0.2.1a12__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/PKG-INFO +1 -1
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/pyproject.toml +1 -1
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/src/katalyst_plugin_context/adapters/runtime_graph.py +106 -2
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/src/katalyst_plugin_context/composition.py +6 -2
- katalyst_plugin_context-0.2.1a12/tests/test_cross_backend_edges.py +287 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/.gitignore +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/src/Justfile +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/src/katalyst_plugin_context/__init__.py +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/src/katalyst_plugin_context/adapters/__init__.py +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/src/katalyst_plugin_context/cli.py +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/src/katalyst_plugin_context/domain/__init__.py +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/src/katalyst_plugin_context/domain/context.py +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/src/katalyst_plugin_context/domain/graph.py +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/src/katalyst_plugin_context/domain/markdown.py +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/src/katalyst_plugin_context/domain/paths.py +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/src/katalyst_plugin_context/domain/policy.py +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/src/katalyst_plugin_context/domain/settings.py +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/src/katalyst_plugin_context/lint.py +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/src/katalyst_plugin_context/mcp_tools.py +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/src/katalyst_plugin_context/plugin.py +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/src/katalyst_plugin_context/py.typed +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/src/katalyst_plugin_context/rest.py +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/src/katalyst_plugin_context/schema.py +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/src/katalyst_plugin_context/service.py +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/src/layer.yaml +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/tests/conftest.py +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/tests/context_support.py +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/tests/test_context.py +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/tests/test_lint.py +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/tests/test_shared_names.py +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/tests/test_surfaces.py +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/xspec/.gitignore +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/xspec/Justfile +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/xspec/features/cli/52_kata_context.feature +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/xspec/features/cli/53_kata_context_budget_policy.feature +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/xspec/features/mcp/09_mcp_context.feature +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/xspec/fixtures/context-catalogue/catalogue/areas/payments.yaml +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/xspec/fixtures/context-catalogue/catalogue/checks/encrypt-card-data.scan-for-card-numbers.yaml +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/xspec/fixtures/context-catalogue/catalogue/components/payments.checkout.web-api.yaml +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/xspec/fixtures/context-catalogue/catalogue/decisions/async-events.yaml +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/xspec/fixtures/context-catalogue/catalogue/decisions/pin-httpx.yaml +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/xspec/fixtures/context-catalogue/catalogue/decisions/use-postgres.yaml +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/xspec/fixtures/context-catalogue/catalogue/packages/httpx.yaml +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/xspec/fixtures/context-catalogue/catalogue/rules/encrypt-card-data.yaml +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/xspec/fixtures/context-catalogue/catalogue/rules/log-correlation-id.yaml +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/xspec/fixtures/context-catalogue/catalogue/services/payments.checkout.yaml +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/xspec/layer.yaml +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/xspec/package.json +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/xspec/playwright.config.ts +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/xspec/steps/context.ts +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/xspec/steps/steps.ts +0 -0
- {katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/xspec/tsconfig.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "katalyst-plugin-context"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.1a12"
|
|
4
4
|
description = "Katalyst context plugin — the right context from the taxonomy for an agent working on a node"
|
|
5
5
|
license = { text = "Proprietary" }
|
|
6
6
|
requires-python = ">=3.13,<3.14"
|
|
@@ -6,12 +6,23 @@ the profile's filters (:func:`katalyst_taxonomy.services.filters.visible_nodes`)
|
|
|
6
6
|
kept only when both its ends are visible. A reference into another backend is stored as
|
|
7
7
|
``backend:fqtn`` and keyed that way, so an edge held by one backend can reach a node of
|
|
8
8
|
another.
|
|
9
|
+
|
|
10
|
+
Cross-backend edges (#418)
|
|
11
|
+
--------------------------
|
|
12
|
+
A backend's ``list_relationships`` resolves references against *its own* documents only, so a
|
|
13
|
+
reference with a ``backend:`` prefix that names a node in another backend silently produces no
|
|
14
|
+
edge. After collecting per-backend edges, :func:`build_graph` runs a second pass that
|
|
15
|
+
resolves declared references (``FieldRef``) over the aggregated document set — the same set
|
|
16
|
+
lint uses — and emits every edge a backend could not emit on its own. The resolver is
|
|
17
|
+
``engine.references.resolve_cross_references``, so the graph and the lint gate read
|
|
18
|
+
identical outcomes.
|
|
9
19
|
"""
|
|
10
20
|
|
|
11
21
|
from __future__ import annotations
|
|
12
22
|
|
|
23
|
+
import logging
|
|
13
24
|
from collections.abc import Callable, Iterable, Sequence
|
|
14
|
-
from typing import Any, cast
|
|
25
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
15
26
|
|
|
16
27
|
from katalyst_engine.model.node import Node
|
|
17
28
|
from katalyst_taxonomy.engine.accessors import (
|
|
@@ -19,14 +30,23 @@ from katalyst_taxonomy.engine.accessors import (
|
|
|
19
30
|
get_markdown_body,
|
|
20
31
|
get_node_backend,
|
|
21
32
|
)
|
|
22
|
-
from katalyst_taxonomy.engine.identity import
|
|
33
|
+
from katalyst_taxonomy.engine.identity import (
|
|
34
|
+
split_backend_prefix,
|
|
35
|
+
with_backend_prefix,
|
|
36
|
+
)
|
|
23
37
|
from katalyst_taxonomy.engine.qualification import split_kind_selector
|
|
38
|
+
from katalyst_taxonomy.engine.references import resolve_cross_references
|
|
24
39
|
from katalyst_taxonomy.ports.backend import TaxonomyBackend
|
|
25
40
|
|
|
26
41
|
from katalyst_plugin_context.domain.graph import Edge, Graph, GraphNode
|
|
27
42
|
|
|
43
|
+
if TYPE_CHECKING: # pragma: no cover - typing only
|
|
44
|
+
from katalyst_engine.schema.registry import SchemaRegistry
|
|
45
|
+
|
|
28
46
|
__all__ = ["Carriers", "build_graph", "graph_from"]
|
|
29
47
|
|
|
48
|
+
logger = logging.getLogger(__name__)
|
|
49
|
+
|
|
30
50
|
#: What a kind can carry: the relationship types it holds, each with the kinds it may point at
|
|
31
51
|
#: (empty: any kind).
|
|
32
52
|
Carriers = Callable[[str], Sequence[tuple[str, frozenset[str]]]]
|
|
@@ -136,14 +156,92 @@ def _of_kind(kinds: list[str], address: str) -> list[str]:
|
|
|
136
156
|
return kinds if selected is None else [kind for kind in kinds if kind == selected]
|
|
137
157
|
|
|
138
158
|
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
# Cross-backend edge stitching (#418)
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
#: An edge by the addresses of its ends: ``((backend, fqtn), rel_type, (backend, fqtn))``.
|
|
165
|
+
EdgeKey = tuple[tuple[str, str], str, tuple[str, str]]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _edge_key(holder: str, source: str, rel_type: str, target: str) -> EdgeKey:
|
|
169
|
+
"""*source* and *target* as their backend and qualified name, *holder* filling in a
|
|
170
|
+
backend an address does not name."""
|
|
171
|
+
return (_address(holder, source), rel_type, _address(holder, target))
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _stitch_cross_backend_edges(
|
|
175
|
+
registry: SchemaRegistry,
|
|
176
|
+
loaded: Sequence[tuple[str, Node]],
|
|
177
|
+
existing: set[EdgeKey],
|
|
178
|
+
) -> list[tuple[str, str, str, str]]:
|
|
179
|
+
"""Emit edges that per-backend resolution missed because the target is elsewhere.
|
|
180
|
+
|
|
181
|
+
Each backend builds its reference index from its own documents only, so a value
|
|
182
|
+
like ``ric:ric-testing`` (target in the ``ric`` backend) produces no edge when
|
|
183
|
+
resolved inside the ``golden`` backend. This function resolves every declared
|
|
184
|
+
``FieldRef`` over the **aggregated** document set — the same corpus lint uses —
|
|
185
|
+
and returns the edges no backend emitted.
|
|
186
|
+
|
|
187
|
+
*existing* holds the edges the backends already produced, keyed by the addresses of
|
|
188
|
+
their ends (:func:`_edge_key`), so an edge is never duplicated and two backends'
|
|
189
|
+
edges between nodes of the same names are never mistaken for each other.
|
|
190
|
+
|
|
191
|
+
Each returned tuple is ``(holder_backend, source, rel_type, target)``, both ends
|
|
192
|
+
prefixed with their backend.
|
|
193
|
+
"""
|
|
194
|
+
all_docs = tuple(doc for _, doc in loaded)
|
|
195
|
+
holder_of = {id(doc): holder for holder, doc in loaded}
|
|
196
|
+
|
|
197
|
+
def fqtn_of(doc: Node) -> str:
|
|
198
|
+
return doc.fqn or doc.name
|
|
199
|
+
|
|
200
|
+
def address_of(doc: Node) -> tuple[str, str]:
|
|
201
|
+
# A document without a backend stamp belongs to the backend that held it.
|
|
202
|
+
return (get_node_backend(doc) or holder_of.get(id(doc), ""), fqtn_of(doc))
|
|
203
|
+
|
|
204
|
+
extra: list[tuple[str, str, str, str]] = []
|
|
205
|
+
for resolution in resolve_cross_references(registry, all_docs, fqtn_of=fqtn_of):
|
|
206
|
+
target = resolution.target
|
|
207
|
+
rel_type = resolution.rel_type
|
|
208
|
+
if target is None or not rel_type:
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
source_address = address_of(resolution.source)
|
|
212
|
+
target_address = address_of(target)
|
|
213
|
+
key: EdgeKey = (source_address, rel_type, target_address)
|
|
214
|
+
if key in existing:
|
|
215
|
+
continue
|
|
216
|
+
|
|
217
|
+
source_backend = source_address[0]
|
|
218
|
+
extra.append(
|
|
219
|
+
(
|
|
220
|
+
source_backend,
|
|
221
|
+
with_backend_prefix(source_backend, source_address[1]),
|
|
222
|
+
rel_type,
|
|
223
|
+
with_backend_prefix(target_address[0], target_address[1]),
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
existing.add(key)
|
|
227
|
+
|
|
228
|
+
if extra:
|
|
229
|
+
logger.debug("Cross-backend stitching added %d edge(s)", len(extra))
|
|
230
|
+
return extra
|
|
231
|
+
|
|
232
|
+
|
|
139
233
|
def build_graph(
|
|
140
234
|
sources: Iterable[tuple[str, TaxonomyBackend]],
|
|
141
235
|
*,
|
|
142
236
|
profile: str | None = None,
|
|
143
237
|
carriers: Carriers | None = None,
|
|
238
|
+
registry: SchemaRegistry | None = None,
|
|
144
239
|
) -> Graph:
|
|
145
240
|
"""Read every backend in *sources* once and build the graph the profile may see.
|
|
146
241
|
|
|
242
|
+
When *registry* is supplied, a second pass resolves cross-backend references that
|
|
243
|
+
no single backend could resolve on its own (#418).
|
|
244
|
+
|
|
147
245
|
Raises:
|
|
148
246
|
PermissionDeniedError: If the profile may not read a backend (as every read does).
|
|
149
247
|
"""
|
|
@@ -157,6 +255,12 @@ def build_graph(
|
|
|
157
255
|
(name, rel.source_fqtn, rel.rel_type, rel.target_fqtn)
|
|
158
256
|
for rel in backend.list_relationships()
|
|
159
257
|
)
|
|
258
|
+
|
|
259
|
+
# --- Cross-backend edge stitching (#418) ---
|
|
260
|
+
if registry is not None and len({name for name, _ in loaded}) > 1:
|
|
261
|
+
existing = {_edge_key(*rel) for rel in relationships}
|
|
262
|
+
relationships.extend(_stitch_cross_backend_edges(registry, loaded, existing))
|
|
263
|
+
|
|
160
264
|
visible = {id(doc) for doc in visible_nodes(None, [doc for _, doc in loaded], profile=profile)}
|
|
161
265
|
hidden = (
|
|
162
266
|
(get_node_backend(doc) or holder, doc.fqn or doc.name)
|
|
@@ -43,8 +43,12 @@ def service_for(
|
|
|
43
43
|
*,
|
|
44
44
|
profile: str | None,
|
|
45
45
|
) -> ContextService:
|
|
46
|
-
"""The service over what *sources* hold, under the profile named *profile*.
|
|
46
|
+
"""The service over what *sources* hold, under the profile named *profile*.
|
|
47
|
+
|
|
48
|
+
The *registry* is forwarded to :func:`build_graph` so cross-backend references
|
|
49
|
+
can be resolved over the aggregated document set (#418).
|
|
50
|
+
"""
|
|
47
51
|
config, active = plugin_config(profile)
|
|
48
52
|
schema = RegistrySchema(registry)
|
|
49
|
-
graph = build_graph(sources, profile=active, carriers=schema.carried)
|
|
53
|
+
graph = build_graph(sources, profile=active, carriers=schema.carried, registry=registry)
|
|
50
54
|
return ContextService(registry, graph, config=config, profile=active)
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# pyright: reportPrivateUsage=false
|
|
2
|
+
"""Tests for cross-backend edge stitching (#418).
|
|
3
|
+
|
|
4
|
+
When multiple backends contribute documents, a reference like ``beta:d-cross``
|
|
5
|
+
(target in the ``beta`` backend) silently produces no edge when resolved inside the
|
|
6
|
+
``alpha`` backend, because each backend resolves references against its own documents
|
|
7
|
+
only. The stitching pass in :func:`build_graph` resolves every declared
|
|
8
|
+
``FieldRef`` over the aggregated document set and emits the missing edges.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any
|
|
14
|
+
from unittest.mock import MagicMock
|
|
15
|
+
|
|
16
|
+
from context_support import node, registry
|
|
17
|
+
from katalyst_engine.model.node import Node
|
|
18
|
+
from katalyst_plugin_context.adapters.runtime_graph import (
|
|
19
|
+
_edge_key,
|
|
20
|
+
_stitch_cross_backend_edges,
|
|
21
|
+
build_graph,
|
|
22
|
+
)
|
|
23
|
+
from katalyst_plugin_context.domain.graph import Graph
|
|
24
|
+
from katalyst_taxonomy.engine.accessors import set_node_backend
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
# Helpers
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _stamped(n: Node, backend: str) -> Node:
|
|
32
|
+
"""Return *n* stamped as owned by *backend*."""
|
|
33
|
+
return set_node_backend(n, backend)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _all_edges(g: Graph) -> list[tuple[str, str, str]]:
|
|
37
|
+
"""Collect every edge in a graph as (source, rel, target) triples."""
|
|
38
|
+
seen: set[tuple[str, str, str]] = set()
|
|
39
|
+
for key in g.nodes:
|
|
40
|
+
for neighbour in g.neighbours(key):
|
|
41
|
+
if neighbour.direction == "out":
|
|
42
|
+
triple = (key, neighbour.rel, neighbour.other)
|
|
43
|
+
else:
|
|
44
|
+
triple = (neighbour.other, neighbour.rel, key)
|
|
45
|
+
seen.add(triple)
|
|
46
|
+
return sorted(seen)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _two_backend_corpus() -> tuple[list[tuple[str, Node]], list[tuple[str, Node]]]:
|
|
50
|
+
"""A corpus split across backends ``alpha`` and ``beta``.
|
|
51
|
+
|
|
52
|
+
alpha has:
|
|
53
|
+
region "north" with decisions=["beta:d-cross"] (target in beta)
|
|
54
|
+
zone "east" under north
|
|
55
|
+
|
|
56
|
+
beta has:
|
|
57
|
+
decision "d-cross" (the target alpha references)
|
|
58
|
+
decision "d-local" (unreferenced)
|
|
59
|
+
"""
|
|
60
|
+
north = _stamped(
|
|
61
|
+
node("region", "north", decisions=["beta:d-cross"], rules=[]),
|
|
62
|
+
"alpha",
|
|
63
|
+
)
|
|
64
|
+
east = _stamped(
|
|
65
|
+
node("zone", "east", "north", parents={"node": "north"}, decisions=[], rules=[]),
|
|
66
|
+
"alpha",
|
|
67
|
+
)
|
|
68
|
+
d_cross = _stamped(
|
|
69
|
+
node("decision", "d-cross", title="Cross-backend decision", status="accepted"),
|
|
70
|
+
"beta",
|
|
71
|
+
)
|
|
72
|
+
d_local = _stamped(
|
|
73
|
+
node("decision", "d-local", title="Local decision", status="accepted"),
|
|
74
|
+
"beta",
|
|
75
|
+
)
|
|
76
|
+
alpha_docs: list[tuple[str, Node]] = [("alpha", north), ("alpha", east)]
|
|
77
|
+
beta_docs: list[tuple[str, Node]] = [("beta", d_cross), ("beta", d_local)]
|
|
78
|
+
return alpha_docs, beta_docs
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _make_backend(
|
|
82
|
+
docs: list[tuple[str, Node]], rels: list[tuple[str, str, str]] | None = None
|
|
83
|
+
) -> Any:
|
|
84
|
+
"""A mock TaxonomyBackend that returns the given documents and relationships."""
|
|
85
|
+
backend: Any = MagicMock()
|
|
86
|
+
backend.load_documents.return_value = [doc for _, doc in docs]
|
|
87
|
+
if rels:
|
|
88
|
+
rel_objects: list[Any] = []
|
|
89
|
+
for src, rel, tgt in rels:
|
|
90
|
+
r: Any = MagicMock()
|
|
91
|
+
r.source_fqtn = src
|
|
92
|
+
r.rel_type = rel
|
|
93
|
+
r.target_fqtn = tgt
|
|
94
|
+
rel_objects.append(r)
|
|
95
|
+
backend.list_relationships.return_value = rel_objects
|
|
96
|
+
else:
|
|
97
|
+
backend.list_relationships.return_value = []
|
|
98
|
+
return backend
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
# _stitch_cross_backend_edges
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class TestStitchCrossBackendEdges:
|
|
107
|
+
"""Unit tests for the stitching function itself."""
|
|
108
|
+
|
|
109
|
+
def test_emits_edge_for_cross_backend_reference(self) -> None:
|
|
110
|
+
"""A reference from alpha to beta:d-cross produces an edge."""
|
|
111
|
+
alpha_docs, beta_docs = _two_backend_corpus()
|
|
112
|
+
loaded = [*alpha_docs, *beta_docs]
|
|
113
|
+
reg = registry()
|
|
114
|
+
|
|
115
|
+
extra = _stitch_cross_backend_edges(reg, loaded, existing=set())
|
|
116
|
+
|
|
117
|
+
cross_edges = [(src, rel, tgt) for _, src, rel, tgt in extra if "d-cross" in tgt]
|
|
118
|
+
assert len(cross_edges) >= 1, f"Expected cross-backend edge to d-cross, got {extra}"
|
|
119
|
+
assert any(rel == "GOVERNED_BY" for _, rel, _ in cross_edges)
|
|
120
|
+
|
|
121
|
+
def test_does_not_duplicate_existing_edges(self) -> None:
|
|
122
|
+
"""Edges the backends already produced are not emitted again."""
|
|
123
|
+
alpha_docs, beta_docs = _two_backend_corpus()
|
|
124
|
+
loaded = [*alpha_docs, *beta_docs]
|
|
125
|
+
reg = registry()
|
|
126
|
+
|
|
127
|
+
extra_first = _stitch_cross_backend_edges(reg, loaded, existing=set())
|
|
128
|
+
assert len(extra_first) > 0, "Precondition: should have found cross-backend edges"
|
|
129
|
+
|
|
130
|
+
existing = {_edge_key(*edge) for edge in extra_first}
|
|
131
|
+
extra_second = _stitch_cross_backend_edges(reg, loaded, existing=existing)
|
|
132
|
+
assert extra_second == [], f"Should not duplicate, got {extra_second}"
|
|
133
|
+
|
|
134
|
+
def test_no_edges_when_all_references_are_same_backend(self) -> None:
|
|
135
|
+
"""When all references resolve within the same backend, nothing new is emitted."""
|
|
136
|
+
from context_support import DOCUMENTS, EDGES
|
|
137
|
+
|
|
138
|
+
loaded: list[tuple[str, Node]] = [("repo", doc) for doc in DOCUMENTS]
|
|
139
|
+
reg = registry()
|
|
140
|
+
|
|
141
|
+
existing = {_edge_key("repo", src, rel, tgt) for src, rel, tgt in EDGES}
|
|
142
|
+
extra = _stitch_cross_backend_edges(reg, loaded, existing=existing)
|
|
143
|
+
assert extra == []
|
|
144
|
+
|
|
145
|
+
def test_skips_unresolvable_references(self) -> None:
|
|
146
|
+
"""A reference to a node that doesn't exist in any backend produces no edge."""
|
|
147
|
+
north = _stamped(
|
|
148
|
+
node("region", "north", decisions=["beta:nonexistent"], rules=[]),
|
|
149
|
+
"alpha",
|
|
150
|
+
)
|
|
151
|
+
d_other = _stamped(
|
|
152
|
+
node("decision", "d-other", title="Other"),
|
|
153
|
+
"beta",
|
|
154
|
+
)
|
|
155
|
+
loaded: list[tuple[str, Node]] = [("alpha", north), ("beta", d_other)]
|
|
156
|
+
reg = registry()
|
|
157
|
+
|
|
158
|
+
extra = _stitch_cross_backend_edges(reg, loaded, existing=set())
|
|
159
|
+
assert extra == []
|
|
160
|
+
|
|
161
|
+
def test_edge_carries_correct_backend_prefixes(self) -> None:
|
|
162
|
+
"""Emitted edges prefix both source and target with their backend."""
|
|
163
|
+
alpha_docs, beta_docs = _two_backend_corpus()
|
|
164
|
+
loaded = [*alpha_docs, *beta_docs]
|
|
165
|
+
reg = registry()
|
|
166
|
+
|
|
167
|
+
extra = _stitch_cross_backend_edges(reg, loaded, existing=set())
|
|
168
|
+
|
|
169
|
+
for _holder, source, _rel, target in extra:
|
|
170
|
+
if "d-cross" in target:
|
|
171
|
+
assert "beta:" in target, f"Target should be beta-prefixed: {target}"
|
|
172
|
+
assert "alpha:" in source, f"Source should be alpha-prefixed: {source}"
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# ---------------------------------------------------------------------------
|
|
176
|
+
# build_graph integration
|
|
177
|
+
# ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class TestBuildGraphCrossBackend:
|
|
181
|
+
"""Integration tests: build_graph with a registry resolves cross-backend edges."""
|
|
182
|
+
|
|
183
|
+
def test_cross_backend_edges_appear_in_graph(self) -> None:
|
|
184
|
+
"""build_graph with registry stitches cross-backend edges into the result."""
|
|
185
|
+
alpha_docs, beta_docs = _two_backend_corpus()
|
|
186
|
+
reg = registry()
|
|
187
|
+
|
|
188
|
+
g = build_graph(
|
|
189
|
+
[("alpha", _make_backend(alpha_docs)), ("beta", _make_backend(beta_docs))],
|
|
190
|
+
registry=reg,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
edges = _all_edges(g)
|
|
194
|
+
cross_edges = [(s, r, t) for s, r, t in edges if "d-cross" in t]
|
|
195
|
+
assert len(cross_edges) >= 1, f"Expected cross-backend edge to d-cross. All edges: {edges}"
|
|
196
|
+
|
|
197
|
+
def test_no_stitching_without_registry(self) -> None:
|
|
198
|
+
"""Without a registry, build_graph does not attempt stitching."""
|
|
199
|
+
alpha_docs, beta_docs = _two_backend_corpus()
|
|
200
|
+
|
|
201
|
+
g = build_graph(
|
|
202
|
+
[("alpha", _make_backend(alpha_docs)), ("beta", _make_backend(beta_docs))],
|
|
203
|
+
registry=None,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
edges = _all_edges(g)
|
|
207
|
+
cross_edges = [(s, r, t) for s, r, t in edges if "d-cross" in t]
|
|
208
|
+
assert cross_edges == [], (
|
|
209
|
+
f"Without registry, should not have cross-backend edges. Edges: {edges}"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def test_no_stitching_with_single_backend(self) -> None:
|
|
213
|
+
"""With only one backend, stitching is skipped even with a registry."""
|
|
214
|
+
alpha_docs, _ = _two_backend_corpus()
|
|
215
|
+
reg = registry()
|
|
216
|
+
|
|
217
|
+
g = build_graph(
|
|
218
|
+
[("alpha", _make_backend(alpha_docs))],
|
|
219
|
+
registry=reg,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
edges = _all_edges(g)
|
|
223
|
+
cross_edges = [(s, r, t) for s, r, t in edges if "d-cross" in t]
|
|
224
|
+
assert cross_edges == []
|
|
225
|
+
|
|
226
|
+
def test_existing_same_backend_edges_preserved(self) -> None:
|
|
227
|
+
"""Edges within a single backend still work after stitching is added."""
|
|
228
|
+
alpha_docs, beta_docs = _two_backend_corpus()
|
|
229
|
+
reg = registry()
|
|
230
|
+
|
|
231
|
+
alpha_rels = [("north.east", "CHILD_OF", "north")]
|
|
232
|
+
g = build_graph(
|
|
233
|
+
[
|
|
234
|
+
("alpha", _make_backend(alpha_docs, alpha_rels)),
|
|
235
|
+
("beta", _make_backend(beta_docs)),
|
|
236
|
+
],
|
|
237
|
+
registry=reg,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
edges = _all_edges(g)
|
|
241
|
+
child_edges = [(s, r, t) for s, r, t in edges if r == "CHILD_OF"]
|
|
242
|
+
assert len(child_edges) >= 1, f"Expected CHILD_OF edge to survive. All edges: {edges}"
|
|
243
|
+
|
|
244
|
+
def test_a_same_named_edge_in_another_backend_does_not_hide_a_cross_backend_one(
|
|
245
|
+
self,
|
|
246
|
+
) -> None:
|
|
247
|
+
"""foe's own x -> y edge and ric's x -> foe:y are two edges, both kept."""
|
|
248
|
+
foe_x = _stamped(node("region", "x", decisions=["y"], rules=[]), "foe")
|
|
249
|
+
foe_y = _stamped(node("decision", "y", title="Y", status="accepted"), "foe")
|
|
250
|
+
ric_x = _stamped(node("region", "x", decisions=["foe:y"], rules=[]), "ric")
|
|
251
|
+
g = build_graph(
|
|
252
|
+
[
|
|
253
|
+
(
|
|
254
|
+
"foe",
|
|
255
|
+
_make_backend([("foe", foe_x), ("foe", foe_y)], [("x", "GOVERNED_BY", "y")]),
|
|
256
|
+
),
|
|
257
|
+
("ric", _make_backend([("ric", ric_x)])),
|
|
258
|
+
],
|
|
259
|
+
registry=registry(),
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
edges = _all_edges(g)
|
|
263
|
+
assert ("foe:x", "GOVERNED_BY", "foe:y") in edges, edges
|
|
264
|
+
assert ("ric:x", "GOVERNED_BY", "foe:y") in edges, edges
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class TestUnstampedDocuments:
|
|
268
|
+
"""A document with no backend stamp belongs to the backend that held it."""
|
|
269
|
+
|
|
270
|
+
def test_an_unstamped_source_is_stitched_under_its_holder(self) -> None:
|
|
271
|
+
ric_x = node("region", "x", decisions=["foe:y"], rules=[])
|
|
272
|
+
foe_y = _stamped(node("decision", "y", title="Y", status="accepted"), "foe")
|
|
273
|
+
loaded: list[tuple[str, Node]] = [("ric", ric_x), ("foe", foe_y)]
|
|
274
|
+
|
|
275
|
+
extra = _stitch_cross_backend_edges(registry(), loaded, existing=set())
|
|
276
|
+
|
|
277
|
+
assert ("ric", "ric:x", "GOVERNED_BY", "foe:y") in extra, extra
|
|
278
|
+
|
|
279
|
+
def test_an_unstamped_source_gets_its_edge_in_the_graph(self) -> None:
|
|
280
|
+
ric_x = node("region", "x", decisions=["foe:y"], rules=[])
|
|
281
|
+
foe_y = _stamped(node("decision", "y", title="Y", status="accepted"), "foe")
|
|
282
|
+
g = build_graph(
|
|
283
|
+
[("ric", _make_backend([("ric", ric_x)])), ("foe", _make_backend([("foe", foe_y)]))],
|
|
284
|
+
registry=registry(),
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
assert ("ric:x", "GOVERNED_BY", "foe:y") in _all_edges(g)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/tests/context_support.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/tests/test_shared_names.py
RENAMED
|
File without changes
|
{katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/tests/test_surfaces.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/xspec/playwright.config.ts
RENAMED
|
File without changes
|
{katalyst_plugin_context-0.2.1a10 → katalyst_plugin_context-0.2.1a12}/xspec/steps/context.ts
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|