klew 0.1.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.
klew/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Klew: a self-improving code-context layer for coding agents, on HydraDB."""
File without changes
@@ -0,0 +1,113 @@
1
+ """Read commit history with the git CLI: replay tasks, ground truth, and co-change counts."""
2
+ from __future__ import annotations
3
+
4
+ import math
5
+ import subprocess
6
+ from collections import Counter
7
+ from dataclasses import dataclass, field
8
+ from itertools import combinations
9
+ from pathlib import Path
10
+
11
+
12
+ @dataclass
13
+ class Commit:
14
+ sha: str
15
+ ts: int
16
+ subject: str
17
+ body: str
18
+ changed: list[str] = field(default_factory=list) # modified/renamed-to .py paths
19
+ added: list[str] = field(default_factory=list)
20
+ deleted: list[str] = field(default_factory=list)
21
+
22
+ @property
23
+ def task(self) -> str:
24
+ body = "\n".join(ln for ln in self.body.splitlines()
25
+ if not ln.lower().startswith(("co-authored-by", "signed-off-by")))
26
+ return (self.subject + "\n\n" + body).strip()
27
+
28
+
29
+ def _git(root: Path, *args: str) -> str:
30
+ return subprocess.run(["git", "-C", str(root), *args], check=True, capture_output=True,
31
+ text=True, errors="replace").stdout
32
+
33
+
34
+ def commits(root: Path, rev_range: str = "HEAD", max_count: int | None = None,
35
+ paths: tuple[str, ...] = ("*.py",)) -> list[Commit]:
36
+ """Non-merge commits touching `paths`, oldest first."""
37
+ args = ["log", "--no-merges", "--reverse", "-M", "--name-status",
38
+ "--format=%x1e%H%x1f%at%x1f%s%x1f%b%x1f"]
39
+ if max_count:
40
+ args.append(f"--max-count={max_count}")
41
+ out = _git(root, *args, rev_range, "--", *paths)
42
+ result: list[Commit] = []
43
+ for rec in out.split("\x1e"):
44
+ if not rec.strip():
45
+ continue
46
+ head, _, files = rec.rpartition("\x1f")
47
+ sha, ts, subject, body = (head.split("\x1f") + ["", "", "", ""])[:4]
48
+ c = Commit(sha.strip(), int(ts or 0), subject.strip(), body.strip())
49
+ for line in files.strip().splitlines():
50
+ parts = line.split("\t")
51
+ if len(parts) < 2:
52
+ continue
53
+ status, path = parts[0], parts[-1]
54
+ if not path.endswith(".py"):
55
+ continue
56
+ if status.startswith("A"):
57
+ c.added.append(path)
58
+ elif status.startswith("D"):
59
+ c.deleted.append(path)
60
+ else:
61
+ c.changed.append(path)
62
+ result.append(c)
63
+ if max_count and len(result) > max_count:
64
+ result = result[-max_count:]
65
+ return result
66
+
67
+
68
+ def head_sha(root: Path, rev: str = "HEAD") -> str:
69
+ return _git(root, "rev-parse", rev).strip()
70
+
71
+
72
+ def worktree(root: Path, sha: str, dest: Path) -> Path:
73
+ """Materialize `sha` at `dest` (a detached worktree) for snapshot indexing."""
74
+ dest = dest.resolve()
75
+ if dest.exists():
76
+ _git(dest, "checkout", "-q", "--detach", sha)
77
+ return dest
78
+ dest.parent.mkdir(parents=True, exist_ok=True)
79
+ _git(root, "worktree", "prune") # forget worktrees whose directories were deleted
80
+ _git(root, "worktree", "add", "-q", "--detach", str(dest), sha)
81
+ return dest
82
+
83
+
84
+ def remove_worktree(root: Path, dest: Path) -> None:
85
+ try:
86
+ _git(root, "worktree", "remove", "--force", str(dest.resolve()))
87
+ except subprocess.CalledProcessError:
88
+ _git(root, "worktree", "prune")
89
+
90
+
91
+ @dataclass
92
+ class CoChange:
93
+ pair_counts: Counter
94
+ file_counts: Counter
95
+
96
+ def weight(self, a: str, b: str) -> float:
97
+ c = self.pair_counts.get((a, b)) or self.pair_counts.get((b, a)) or 0
98
+ if not c:
99
+ return 0.0
100
+ return c / math.sqrt(self.file_counts[a] * self.file_counts[b])
101
+
102
+
103
+ def co_change(history: list[Commit], max_files: int = 12) -> CoChange:
104
+ """Count how often files change together. Large sweeping commits are ignored as noise."""
105
+ pairs: Counter = Counter()
106
+ files: Counter = Counter()
107
+ for c in history:
108
+ touched = sorted(set(c.changed + c.added))
109
+ if not touched or len(touched) > max_files:
110
+ continue
111
+ files.update(touched)
112
+ pairs.update(combinations(touched, 2))
113
+ return CoChange(pairs, files)
@@ -0,0 +1,150 @@
1
+ """GraphStore backed by a HydraDB node over Bolt (Neo4j driver)."""
2
+ from __future__ import annotations
3
+
4
+ import threading
5
+ from collections.abc import Iterable, Sequence
6
+
7
+ from neo4j import GraphDatabase
8
+
9
+ from .. import ids as idmod
10
+ from ..config import HydraConfig
11
+ from ..ports import Neighbor, Row
12
+ from ..schema import EDGE_TYPES, NODE_FIELDS, EdgeType
13
+ from . import queries as Q
14
+
15
+ BATCH = 1000 # HydraDB caps UNWIND batches at 1024 items
16
+ ID_CHUNK = 1000
17
+
18
+
19
+ def _chunks(seq: Sequence, n: int):
20
+ for i in range(0, len(seq), n):
21
+ yield seq[i : i + n]
22
+
23
+
24
+ class HydraGraphStore:
25
+ def __init__(self, cfg: HydraConfig | None = None):
26
+ cfg = cfg or HydraConfig()
27
+ self._driver = GraphDatabase.driver(cfg.uri, auth=("neo4j", cfg.token))
28
+ self._db = cfg.database
29
+ # Sessions are not thread-safe, the driver is: one session per thread lets the walker
30
+ # run a hop's expansions concurrently.
31
+ self._local = threading.local()
32
+ self._sessions: list = []
33
+ self._count_lock = threading.Lock()
34
+ self.queries = 0
35
+
36
+ # -- plumbing -------------------------------------------------------------------------
37
+ def _run(self, cypher: str, **params) -> list[Row]:
38
+ # HydraDB rejects explicit transactions, so every statement is an auto-commit RUN.
39
+ sess = getattr(self._local, "session", None)
40
+ if sess is None:
41
+ sess = self._local.session = self._driver.session(database=self._db)
42
+ with self._count_lock:
43
+ self._sessions.append(sess)
44
+ with self._count_lock:
45
+ self.queries += 1
46
+ return [r.data() for r in sess.run(cypher, params)]
47
+
48
+ def ping(self) -> None:
49
+ self._driver.verify_connectivity()
50
+
51
+ # -- writes ---------------------------------------------------------------------------
52
+ def upsert_nodes(self, label: str, rows: Sequence[Row]) -> None:
53
+ if not rows:
54
+ return
55
+ fields = tuple(f for f in NODE_FIELDS[label] if f in rows[0])
56
+ cypher = Q.upsert_nodes(label, fields)
57
+ for chunk in _chunks(list(rows), BATCH):
58
+ self._run(cypher, rows=[{"id": r["id"], **{f: r.get(f) for f in fields}} for r in chunk])
59
+
60
+ def upsert_edges(self, etype: EdgeType, rows: Sequence[Row]) -> None:
61
+ if not rows:
62
+ return
63
+ extra = tuple(k for k in rows[0] if k not in ("src", "dst", "eid"))
64
+ cypher = Q.upsert_edges(etype, extra)
65
+ for chunk in _chunks(list(rows), BATCH):
66
+ self._run(cypher, rows=[
67
+ {"src": r["src"], "dst": r["dst"],
68
+ "eid": r.get("eid") or idmod.edge_id(etype.name, r["src"], r["dst"]),
69
+ **{k: r.get(k) for k in extra}}
70
+ for r in chunk
71
+ ])
72
+
73
+ def delete_nodes(self, label: str, ids: Iterable[int]) -> None:
74
+ ids = list(dict.fromkeys(ids))
75
+ for chunk in _chunks(ids, ID_CHUNK):
76
+ self._run(Q.delete_nodes(label), ids=chunk)
77
+
78
+ def delete_repo(self, repo: str) -> None:
79
+ for label in NODE_FIELDS:
80
+ self._run(Q.delete_repo(label), repo=repo)
81
+
82
+ # -- reads ----------------------------------------------------------------------------
83
+ def expand(self, etype: EdgeType, direction: str, ids: Iterable[int]) -> list[Neighbor]:
84
+ ids = list(dict.fromkeys(ids))
85
+ out: list[Neighbor] = []
86
+ cypher = Q.expand(etype, direction)
87
+ for chunk in _chunks(ids, ID_CHUNK):
88
+ for r in self._run(cypher, ids=chunk):
89
+ a, b = r["src"], r["dst"]
90
+ # Edge ids are identity, not readable properties; recompute from the endpoints.
91
+ eid = idmod.edge_id(etype.name, a, b) if direction == "out" else idmod.edge_id(etype.name, b, a)
92
+ out.append(Neighbor(a, b, eid, 1.0 if r.get("w") is None else float(r["w"])))
93
+ return sorted(out) # HydraDB row order varies between runs; callers get a stable order
94
+
95
+ def get_nodes(self, label: str, ids: Iterable[int], fields: Sequence[str]) -> list[Row]:
96
+ ids = list(dict.fromkeys(ids))
97
+ out: list[Row] = []
98
+ cypher = Q.get_nodes(label, tuple(fields))
99
+ for chunk in _chunks(ids, ID_CHUNK):
100
+ out.extend(self._run(cypher, ids=chunk))
101
+ return out
102
+
103
+ def scan_nodes(self, label: str, repo: str, fields: Sequence[str]) -> list[Row]:
104
+ return self._run(Q.scan_nodes(label, tuple(fields)), repo=repo)
105
+
106
+ def list_nodes(self, label: str, fields: Sequence[str]) -> list[Row]:
107
+ return self._run(Q.list_nodes(label, tuple(fields)))
108
+
109
+ def edge_weights(self, etype: EdgeType, pairs: Iterable[tuple[int, int]]) -> dict[tuple[int, int], Row]:
110
+ pairs = set(pairs)
111
+ srcs = list({s for s, _ in pairs})
112
+ found: dict[tuple[int, int], Row] = {}
113
+ cypher = Q.edge_stats(etype)
114
+ for chunk in _chunks(srcs, ID_CHUNK):
115
+ for r in self._run(cypher, ids=chunk):
116
+ key = (r["src"], r["dst"])
117
+ if key in pairs:
118
+ found[key] = {"w": r.get("w"), "hits": r.get("hits") or 0,
119
+ "misses": r.get("misses") or 0}
120
+ return found
121
+
122
+ def count_edges(self, etype: EdgeType, repo: str) -> int:
123
+ rows = self._run(Q.count_edges(etype), repo=repo)
124
+ return int(rows[0]["c"]) if rows else 0
125
+
126
+ def key_paths(self, repo: str, src_label: str, src_keys, dst_label: str, dst_keys, rel_types,
127
+ max_len: int = 3, limit: int = 5) -> list[list]:
128
+ """Native algo.MSpaths between nodes named by `key`. Keys repeat across repos, so only paths whose
129
+ nodes all belong to `repo` are kept. Each path alternates node dicts and relationship-type strings."""
130
+ rows = self._run(Q.ms_paths(src_label, list(src_keys), dst_label, list(dst_keys), list(rel_types), max_len))
131
+ out = []
132
+ for r in rows:
133
+ path = r["path"]
134
+ nodes = [x for x in path if isinstance(x, dict)]
135
+ if nodes and all(n.get("repo") == repo for n in nodes):
136
+ out.append(path)
137
+ out.sort(key=len)
138
+ return out[:limit]
139
+
140
+ def shortest_paths(self, etype: EdgeType, src: int, dst: int, max_len: int, k: int) -> list[list[Row]]:
141
+ rows = self._run(Q.shortest_paths(etype), src=src, dst=dst, max_len=max_len, k=k)
142
+ return [[x for x in r["path"] if isinstance(x, dict)] for r in rows]
143
+
144
+ def close(self) -> None:
145
+ for sess in self._sessions:
146
+ sess.close()
147
+ self._driver.close()
148
+
149
+
150
+ __all__ = ["HydraGraphStore", "EDGE_TYPES"]
@@ -0,0 +1,158 @@
1
+ """In-memory GraphStore with the same semantics as HydraGraphStore.
2
+
3
+ Used for unit tests, for frontend work before HydraDB is up, and as the reference
4
+ implementation in the store contract test.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from collections import defaultdict
9
+ from collections.abc import Iterable, Sequence
10
+
11
+ from .. import ids as idmod
12
+ from ..ports import Neighbor, Row
13
+ from ..schema import NODE_FIELDS, EdgeType
14
+
15
+
16
+ class MemoryGraphStore:
17
+ def __init__(self) -> None:
18
+ self.nodes: dict[str, dict[int, Row]] = defaultdict(dict)
19
+ # etype -> src -> dst -> props
20
+ self.out: dict[str, dict[int, dict[int, Row]]] = defaultdict(lambda: defaultdict(dict))
21
+ self.inn: dict[str, dict[int, dict[int, Row]]] = defaultdict(lambda: defaultdict(dict))
22
+ self.queries = 0
23
+
24
+ def upsert_nodes(self, label: str, rows: Sequence[Row]) -> None:
25
+ self.queries += 1
26
+ for r in rows:
27
+ cur = self.nodes[label].setdefault(r["id"], {"id": r["id"]})
28
+ cur.update({k: v for k, v in r.items() if k in NODE_FIELDS[label]})
29
+
30
+ def upsert_edges(self, etype: EdgeType, rows: Sequence[Row]) -> None:
31
+ self.queries += 1
32
+ for r in rows:
33
+ s, d = r["src"], r["dst"]
34
+ if s not in self.nodes[etype.src] or d not in self.nodes[etype.dst]:
35
+ continue # MATCH would not bind; HydraDB silently writes nothing
36
+ props = self.out[etype.name][s].setdefault(d, {})
37
+ props.update({k: v for k, v in r.items() if k not in ("src", "dst")})
38
+ props.setdefault("eid", idmod.edge_id(etype.name, s, d))
39
+ self.inn[etype.name][d][s] = props
40
+
41
+ def delete_nodes(self, label: str, ids: Iterable[int]) -> None:
42
+ self._drop({i for i in ids if i in self.nodes[label]})
43
+
44
+ def delete_repo(self, repo: str) -> None:
45
+ self._drop({i for lbl in self.nodes.values() for i, r in lbl.items() if r.get("repo") == repo})
46
+
47
+ def _drop(self, doomed: set[int]) -> None:
48
+ self.queries += 1
49
+ for lbl in self.nodes.values():
50
+ for i in list(lbl):
51
+ if i in doomed:
52
+ del lbl[i]
53
+ for idx in (self.out, self.inn):
54
+ for adj in idx.values():
55
+ for a in list(adj):
56
+ if a in doomed:
57
+ del adj[a]
58
+ continue
59
+ for b in list(adj[a]):
60
+ if b in doomed:
61
+ del adj[a][b]
62
+
63
+ def expand(self, etype: EdgeType, direction: str, ids: Iterable[int]) -> list[Neighbor]:
64
+ self.queries += 1
65
+ adj = self.out[etype.name] if direction == "out" else self.inn[etype.name]
66
+ out = []
67
+ for a in dict.fromkeys(ids):
68
+ for b, p in adj.get(a, {}).items():
69
+ w = p.get("w")
70
+ eid = idmod.edge_id(etype.name, a, b) if direction == "out" else idmod.edge_id(etype.name, b, a)
71
+ out.append(Neighbor(a, b, eid, 1.0 if w is None else float(w)))
72
+ return sorted(out)
73
+
74
+ def get_nodes(self, label: str, ids: Iterable[int], fields: Sequence[str]) -> list[Row]:
75
+ self.queries += 1
76
+ tbl = self.nodes[label]
77
+ return [{"id": i, **{f: tbl[i].get(f) for f in fields}} for i in dict.fromkeys(ids) if i in tbl]
78
+
79
+ def scan_nodes(self, label: str, repo: str, fields: Sequence[str]) -> list[Row]:
80
+ self.queries += 1
81
+ return [{"id": i, **{f: r.get(f) for f in fields}}
82
+ for i, r in self.nodes[label].items() if r.get("repo") == repo]
83
+
84
+ def list_nodes(self, label: str, fields: Sequence[str]) -> list[Row]:
85
+ self.queries += 1
86
+ return [{"id": i, **{f: r.get(f) for f in fields}} for i, r in self.nodes[label].items()]
87
+
88
+ def edge_weights(self, etype: EdgeType, pairs: Iterable[tuple[int, int]]) -> dict[tuple[int, int], Row]:
89
+ self.queries += 1
90
+ found = {}
91
+ for s, d in set(pairs):
92
+ p = self.out[etype.name].get(s, {}).get(d)
93
+ if p is not None:
94
+ found[(s, d)] = {"w": p.get("w"), "hits": p.get("hits") or 0,
95
+ "misses": p.get("misses") or 0}
96
+ return found
97
+
98
+ def count_edges(self, etype: EdgeType, repo: str) -> int:
99
+ self.queries += 1
100
+ return sum(len(v) for s, v in self.out[etype.name].items()
101
+ if self.nodes[etype.src].get(s, {}).get("repo") == repo)
102
+
103
+ def key_paths(self, repo: str, src_label: str, src_keys, dst_label: str, dst_keys, rel_types,
104
+ max_len: int = 3, limit: int = 5) -> list[list]:
105
+ """Reference BFS with the same output shape as HydraDB's algo.MSpaths (both directions)."""
106
+ from collections import deque
107
+ self.queries += 1
108
+ by_key = {}
109
+ for lbl, tbl in self.nodes.items():
110
+ for i, r in tbl.items():
111
+ if r.get("repo") == repo:
112
+ by_key[(lbl, r.get("key"))] = i
113
+ lookup = {i: r for tbl in self.nodes.values() for i, r in tbl.items()}
114
+ targets = {by_key[(dst_label, k)] for k in dst_keys if (dst_label, k) in by_key}
115
+ out = []
116
+ for k in src_keys:
117
+ s = by_key.get((src_label, k))
118
+ if s is None:
119
+ continue
120
+ q = deque([[s]])
121
+ while q and len(out) < limit * 4:
122
+ p = q.popleft()
123
+ if p[-1] in targets and len(p) > 1:
124
+ out.append(p)
125
+ continue
126
+ if (len(p) - 1) // 2 >= max_len:
127
+ continue
128
+ for t in rel_types:
129
+ for nb in list(self.out[t].get(p[-1], {})) + list(self.inn[t].get(p[-1], {})):
130
+ if nb not in p[::2]:
131
+ q.append(p + [t, nb])
132
+ res = [[x if isinstance(x, str) else {k2: v for k2, v in lookup[x].items() if k2 != "id"} for x in p]
133
+ for p in out]
134
+ res.sort(key=len)
135
+ return res[:limit]
136
+
137
+ def shortest_paths(self, etype: EdgeType, src: int, dst: int, max_len: int, k: int) -> list[list[Row]]:
138
+ self.queries += 1
139
+ # BFS over both directions, mirroring relDirection: 'both'
140
+ from collections import deque
141
+ q = deque([[src]])
142
+ paths = []
143
+ while q and len(paths) < k:
144
+ p = q.popleft()
145
+ if p[-1] == dst:
146
+ paths.append(p)
147
+ continue
148
+ if len(p) > max_len:
149
+ continue
150
+ nbrs = set(self.out[etype.name].get(p[-1], {})) | set(self.inn[etype.name].get(p[-1], {}))
151
+ for n in nbrs:
152
+ if n not in p:
153
+ q.append(p + [n])
154
+ lookup = {i: r for lbl in self.nodes.values() for i, r in lbl.items()}
155
+ return [[{k2: v for k2, v in lookup[i].items() if k2 != "id"} for i in p] for p in paths]
156
+
157
+ def close(self) -> None:
158
+ pass
@@ -0,0 +1,91 @@
1
+ """Every Cypher statement Klew sends to HydraDB lives here.
2
+
3
+ HydraDB dialect rules this file respects (verified against v0.4.0, legacy engine):
4
+ - Vertex batch upsert must be `UNWIND $rows AS row MERGE (n {id: row.id}) SET n:Label, ...`.
5
+ - Relationship batch upsert must MATCH both endpoints and MERGE with an edge id: `{id: row.eid}`.
6
+ - Composite (list-of-maps) params are only legal as UNWIND input, so batched *reads* use
7
+ `WHERE a.id IN $ids` with a scalar list instead of UNWIND.
8
+ - Exactly one relationship type per pattern; no type(r)/labels(n); RETURN only binding.prop or
9
+ aggregates; property values are scalars only (lists/maps are stored as JSON strings).
10
+ - Reading r.<prop> works when the far endpoint is unlabeled.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from ..schema import EdgeType
15
+
16
+
17
+ def upsert_nodes(label: str, fields: tuple[str, ...]) -> str:
18
+ sets = ", ".join(f"n.{f} = row.{f}" for f in fields)
19
+ return f"UNWIND $rows AS row MERGE (n {{id: row.id}}) SET n:{label}" + (f", {sets}" if sets else "")
20
+
21
+
22
+ def upsert_edges(e: EdgeType, fields: tuple[str, ...]) -> str:
23
+ sets = ", ".join(f"r.{f} = row.{f}" for f in fields)
24
+ return (
25
+ f"UNWIND $rows AS row MATCH (s:{e.src} {{id: row.src}}), (d:{e.dst} {{id: row.dst}}) "
26
+ f"MERGE (s)-[r:{e.name} {{id: row.eid}}]->(d)" + (f" SET {sets}" if sets else "")
27
+ )
28
+
29
+
30
+ def expand(e: EdgeType, direction: str) -> str:
31
+ if direction == "out":
32
+ return (f"MATCH (a:{e.src})-[r:{e.name}]->(b) WHERE a.id IN $ids "
33
+ "RETURN a.id AS src, b.id AS dst, r.w AS w")
34
+ return (f"MATCH (a:{e.dst})<-[r:{e.name}]-(b) WHERE a.id IN $ids "
35
+ "RETURN a.id AS src, b.id AS dst, r.w AS w")
36
+
37
+
38
+ def edge_stats(e: EdgeType) -> str:
39
+ return (f"MATCH (a:{e.src})-[r:{e.name}]->(b) WHERE a.id IN $ids "
40
+ "RETURN a.id AS src, b.id AS dst, r.w AS w, r.hits AS hits, r.misses AS misses")
41
+
42
+
43
+ def get_nodes(label: str, fields: tuple[str, ...]) -> str:
44
+ cols = ", ".join(f"n.{f} AS {f}" for f in fields)
45
+ return f"MATCH (n:{label}) WHERE n.id IN $ids RETURN n.id AS id, {cols}"
46
+
47
+
48
+ def scan_nodes(label: str, fields: tuple[str, ...]) -> str:
49
+ cols = ", ".join(f"n.{f} AS {f}" for f in fields)
50
+ return f"MATCH (n:{label}) WHERE n.repo = $repo RETURN n.id AS id, {cols}"
51
+
52
+
53
+ def list_nodes(label: str, fields: tuple[str, ...]) -> str:
54
+ cols = ", ".join(f"n.{f} AS {f}" for f in fields)
55
+ return f"MATCH (n:{label}) RETURN n.id AS id, {cols}"
56
+
57
+
58
+ def count_edges(e: EdgeType) -> str:
59
+ return f"MATCH (a:{e.src})-[r:{e.name}]->(b) WHERE a.repo = $repo RETURN count(*) AS c"
60
+
61
+
62
+ def delete_nodes(label: str) -> str:
63
+ return f"MATCH (n:{label}) WHERE n.id IN $ids DETACH DELETE n"
64
+
65
+
66
+ def delete_repo(label: str) -> str:
67
+ return f"MATCH (n:{label}) WHERE n.repo = $repo DETACH DELETE n"
68
+
69
+
70
+ def _lit(values) -> str:
71
+ """Procedure maps take literals only, so lists are inlined and escaped."""
72
+ return "[" + ", ".join("'" + str(v).replace("\\", "\\\\").replace("'", "\\'") + "'" for v in values) + "]"
73
+
74
+
75
+ def ms_paths(src_label: str, src_keys, dst_label: str, dst_keys, rel_types, max_len: int) -> str:
76
+ """HydraDB's native multi-source path search (GraphBLAS), anchored on the string `key` property."""
77
+ return (
78
+ f"CALL algo.MSpaths({{sourceLabel: '{src_label}', sourceProperty: 'key', sourceValues: {_lit(src_keys)}, "
79
+ f"targetLabel: '{dst_label}', targetProperty: 'key', targetValues: {_lit(dst_keys)}, "
80
+ f"relTypes: {_lit(rel_types)}, relDirection: 'both', maxLen: {int(max_len)}}}) "
81
+ "YIELD path RETURN path"
82
+ )
83
+
84
+
85
+ def shortest_paths(e: EdgeType) -> str:
86
+ # Procedure maps take scalars only; lists are inlined literals.
87
+ return (
88
+ "CALL algo.SPpaths({sourceNode: $src, targetNode: $dst, "
89
+ f"relTypes: ['{e.name}'], relDirection: 'both', maxLen: $max_len, pathCount: $k}}) "
90
+ "YIELD path, pathWeight RETURN path, pathWeight"
91
+ )