ltcai 6.3.0 → 6.4.0

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.
@@ -179,10 +179,12 @@ class SearchService:
179
179
  allowed_workspaces=None,
180
180
  ) -> Dict[str, Any]:
181
181
  weights = {**DEFAULT_HYBRID_WEIGHTS, **dict(weights or {})}
182
+ # Scope each channel at the source so out-of-scope rows never enter the
183
+ # fusion set (defense-in-depth — the fused result is re-scoped below too).
182
184
  channels = {
183
- "keyword": self.keyword_search(query, limit=keyword_limit),
184
- "vector": self.vector_search(query, limit=vector_limit),
185
- "graph": self.graph_search(query, limit=graph_limit),
185
+ "keyword": self.keyword_search(query, limit=keyword_limit, allowed_workspaces=allowed_workspaces),
186
+ "vector": self.vector_search(query, limit=vector_limit, allowed_workspaces=allowed_workspaces),
187
+ "graph": self.graph_search(query, limit=graph_limit, allowed_workspaces=allowed_workspaces),
186
188
  }
187
189
  fused: Dict[str, Dict[str, Any]] = {}
188
190
  for source, payload in channels.items():
@@ -234,14 +236,50 @@ class SearchService:
234
236
  "matches": matches,
235
237
  }
236
238
 
237
- def graph(self, *, limit: int = 300) -> Dict[str, Any]:
238
- return self._require_graph().graph(limit=limit)
239
-
240
- def node(self, node_id: str, *, include_neighbors: bool = True, depth: int = 1, limit: int = 100) -> Dict[str, Any]:
239
+ def graph(self, *, limit: int = 300, allowed_workspaces=None) -> Dict[str, Any]:
241
240
  graph = self._require_graph()
242
- payload = {"node": graph.get_node(node_id)}
241
+ try:
242
+ return graph.graph(limit=limit, allowed_workspaces=allowed_workspaces)
243
+ except TypeError:
244
+ payload = graph.graph(limit=limit)
245
+ if allowed_workspaces is not None:
246
+ nodes = graph.filter_scoped_nodes(payload.get("nodes", []), allowed_workspaces)
247
+ kept = {node.get("id") for node in nodes}
248
+ edges = [
249
+ edge for edge in payload.get("edges", [])
250
+ if edge.get("from") in kept and edge.get("to") in kept
251
+ ]
252
+ payload = {**payload, "nodes": nodes, "edges": edges}
253
+ return payload
254
+
255
+ def node(
256
+ self,
257
+ node_id: str,
258
+ *,
259
+ include_neighbors: bool = True,
260
+ depth: int = 1,
261
+ limit: int = 100,
262
+ allowed_workspaces=None,
263
+ ) -> Dict[str, Any]:
264
+ graph = self._require_graph()
265
+ node = graph.get_node(node_id)
266
+ if allowed_workspaces is not None:
267
+ visible = graph.filter_scoped_nodes([node], allowed_workspaces)
268
+ if not visible:
269
+ raise ValueError(f"graph node not found: {node_id}")
270
+ node = visible[0]
271
+ payload = {"node": node}
243
272
  if include_neighbors:
244
- payload["neighborhood"] = graph.traverse(node_id, depth=depth, limit=limit)
273
+ neighborhood = graph.traverse(node_id, depth=depth, limit=limit)
274
+ if allowed_workspaces is not None:
275
+ nodes = graph.filter_scoped_nodes(neighborhood.get("nodes", []), allowed_workspaces)
276
+ kept = {item.get("id") for item in nodes}
277
+ edges = [
278
+ edge for edge in neighborhood.get("edges", [])
279
+ if edge.get("from") in kept and edge.get("to") in kept
280
+ ]
281
+ neighborhood = {**neighborhood, "nodes": nodes, "edges": edges}
282
+ payload["neighborhood"] = neighborhood
245
283
  return payload
246
284
 
247
285
  def relationships(
@@ -251,13 +289,26 @@ class SearchService:
251
289
  node_id: str = "",
252
290
  relationship_type: str = "",
253
291
  limit: int = 30,
292
+ allowed_workspaces=None,
254
293
  ) -> Dict[str, Any]:
255
- return self._require_graph().relationship_search(
294
+ graph = self._require_graph()
295
+ payload = graph.relationship_search(
256
296
  query=query,
257
297
  node_id=node_id,
258
298
  relationship_type=relationship_type,
259
299
  limit=limit,
260
300
  )
301
+ if allowed_workspaces is not None:
302
+ kept = []
303
+ for rel in payload.get("relationships", []):
304
+ endpoints = [
305
+ {"id": (rel.get("source") or {}).get("id")},
306
+ {"id": (rel.get("target") or {}).get("id")},
307
+ ]
308
+ if len(graph.filter_scoped_nodes(endpoints, allowed_workspaces)) == 2:
309
+ kept.append(rel)
310
+ payload = {**payload, "relationships": kept}
311
+ return payload
261
312
 
262
313
  def index_status(self) -> Dict[str, Any]:
263
314
  return self._require_graph().index_status()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "6.3.0",
3
+ "version": "6.4.0",
4
4
  "description": "Lattice AI — local-first Digital Brain that keeps your knowledge durable across any AI model.",
5
5
  "homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
6
6
  "repository": {
@@ -1654,7 +1654,7 @@ dependencies = [
1654
1654
 
1655
1655
  [[package]]
1656
1656
  name = "lattice-ai-desktop"
1657
- version = "6.3.0"
1657
+ version = "6.4.0"
1658
1658
  dependencies = [
1659
1659
  "plist",
1660
1660
  "serde",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "lattice-ai-desktop"
3
- version = "6.3.0"
3
+ version = "6.4.0"
4
4
  description = "Lattice AI Digital Brain desktop shell"
5
5
  authors = ["TaeSoo Park"]
6
6
  edition = "2021"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://schema.tauri.app/config/2",
3
3
  "productName": "Lattice AI",
4
- "version": "6.3.0",
4
+ "version": "6.4.0",
5
5
  "identifier": "ai.lattice.desktop",
6
6
  "build": {
7
7
  "beforeDevCommand": "npm run frontend:dev",
@@ -1,12 +1,12 @@
1
1
  {
2
- "version": "6.3.0",
2
+ "version": "6.4.0",
3
3
  "generated_at": "vite",
4
4
  "entrypoints": {
5
5
  "app": "/static/app/index.html"
6
6
  },
7
7
  "assets": {
8
8
  "../node_modules/@tauri-apps/api/core.js": "/static/app/assets/core-CwxXejkd.js",
9
- "index.html": "/static/app/assets/index-D76dWuQk.js",
9
+ "index.html": "/static/app/assets/index-t1jx1BR9.js",
10
10
  "assets/index-Div5vMlq.css": "/static/app/assets/index-Div5vMlq.css"
11
11
  },
12
12
  "vite": {
@@ -17,7 +17,7 @@
17
17
  "isDynamicEntry": true
18
18
  },
19
19
  "index.html": {
20
- "file": "assets/index-D76dWuQk.js",
20
+ "file": "assets/index-t1jx1BR9.js",
21
21
  "name": "index",
22
22
  "src": "index.html",
23
23
  "isEntry": true,