ltcai 8.6.0 → 8.8.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.
Files changed (80) hide show
  1. package/README.md +40 -33
  2. package/docs/CHANGELOG.md +49 -1
  3. package/docs/COMMUNITY_AND_PLUGINS.md +7 -4
  4. package/docs/DEVELOPMENT.md +11 -9
  5. package/docs/LEGACY_COMPATIBILITY.md +33 -14
  6. package/docs/ONBOARDING.md +2 -2
  7. package/docs/TRUST_MODEL.md +1 -1
  8. package/docs/WHY_LATTICE.md +2 -2
  9. package/docs/kg-schema.md +1 -1
  10. package/kg_schema.py +1 -1
  11. package/knowledge_graph.py +2 -2
  12. package/lattice_brain/__init__.py +1 -1
  13. package/lattice_brain/core.py +1 -1
  14. package/lattice_brain/ingestion.py +35 -20
  15. package/lattice_brain/runtime/agent_runtime.py +37 -3
  16. package/lattice_brain/runtime/multi_agent.py +1 -1
  17. package/latticeai/__init__.py +1 -1
  18. package/latticeai/app_factory.py +3 -3
  19. package/latticeai/core/config.py +14 -2
  20. package/latticeai/core/legacy_compatibility.py +36 -25
  21. package/latticeai/core/marketplace.py +1 -1
  22. package/latticeai/core/workspace_os.py +1 -1
  23. package/latticeai/models/router.py +3 -2
  24. package/latticeai/runtime/persistence_runtime.py +1 -1
  25. package/latticeai/runtime/platform_services_runtime.py +1 -1
  26. package/latticeai/runtime/router_registration.py +11 -5
  27. package/latticeai/services/architecture_readiness.py +2 -2
  28. package/latticeai/services/memory_service.py +38 -7
  29. package/latticeai/services/model_runtime.py +52 -28
  30. package/latticeai/services/product_readiness.py +11 -11
  31. package/package.json +3 -2
  32. package/scripts/bump_version.py +2 -0
  33. package/scripts/check_python.py +25 -12
  34. package/src-tauri/Cargo.lock +1 -1
  35. package/src-tauri/Cargo.toml +1 -1
  36. package/src-tauri/tauri.conf.json +1 -1
  37. package/static/app/asset-manifest.json +11 -11
  38. package/static/app/assets/{Act-IrojDEWY.js → Act-C7K9wsO9.js} +1 -1
  39. package/static/app/assets/{Brain--VODO4HM.js → Brain-I1OSzxJu.js} +2 -2
  40. package/static/app/assets/Capture-B3V4_5Xp.js +1 -0
  41. package/static/app/assets/{Library-DrVto3cO.js → Library-Cgj-EF50.js} +1 -1
  42. package/static/app/assets/{System-DNrVbcMX.js → System-D1Lkei3I.js} +1 -1
  43. package/static/app/assets/index--P0ksosz.js +17 -0
  44. package/static/app/assets/index-DCh5AoXt.css +2 -0
  45. package/static/app/assets/{primitives-lz45ZfWY.js → primitives-BLqaKk5g.js} +1 -1
  46. package/static/app/assets/{textarea-C7uwJsxy.js → textarea-CVQkN2Tk.js} +1 -1
  47. package/static/app/index.html +2 -2
  48. package/static/sw.js +1 -1
  49. package/lattice_brain/_kg_common.py +0 -11
  50. package/lattice_brain/discovery.py +0 -11
  51. package/lattice_brain/documents.py +0 -11
  52. package/lattice_brain/identity.py +0 -11
  53. package/lattice_brain/ingest.py +0 -11
  54. package/lattice_brain/network.py +0 -11
  55. package/lattice_brain/projection.py +0 -11
  56. package/lattice_brain/provenance.py +0 -11
  57. package/lattice_brain/retrieval.py +0 -11
  58. package/lattice_brain/schema.py +0 -11
  59. package/lattice_brain/store.py +0 -11
  60. package/lattice_brain/write_master.py +0 -11
  61. package/latticeai/brain/__init__.py +0 -36
  62. package/latticeai/brain/_kg_common.py +0 -17
  63. package/latticeai/brain/context.py +0 -17
  64. package/latticeai/brain/conversations.py +0 -17
  65. package/latticeai/brain/discovery.py +0 -17
  66. package/latticeai/brain/documents.py +0 -17
  67. package/latticeai/brain/identity.py +0 -17
  68. package/latticeai/brain/ingest.py +0 -17
  69. package/latticeai/brain/memory.py +0 -17
  70. package/latticeai/brain/network.py +0 -17
  71. package/latticeai/brain/projection.py +0 -17
  72. package/latticeai/brain/provenance.py +0 -17
  73. package/latticeai/brain/retrieval.py +0 -17
  74. package/latticeai/brain/schema.py +0 -17
  75. package/latticeai/brain/store.py +0 -17
  76. package/latticeai/brain/write_master.py +0 -17
  77. package/latticeai/services/agent_runtime.py +0 -11
  78. package/static/app/assets/Capture-BUP_wJ7s.js +0 -1
  79. package/static/app/assets/index-BaaPtsLp.js +0 -16
  80. package/static/app/assets/index-_M5aCv21.css +0 -2
@@ -94,7 +94,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
94
94
  from pydantic import BaseModel
95
95
 
96
96
  from latticeai.models.router import LLMRouter, normalize_branding
97
- from lattice_brain._kg_common import set_llm_router
97
+ from lattice_brain.graph._kg_common import set_llm_router
98
98
  from lattice_brain.graph.schema import set_embed_dim
99
99
  from latticeai.core.security import (
100
100
  hash_password,
@@ -107,7 +107,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
107
107
  enforce_rate_limit as _enforce_rate_limit,
108
108
  )
109
109
  from latticeai.core.audit import (
110
- get_audit_log as _get_audit_log,
110
+ get_audit_log as _get_audit_log, # noqa: F401 - legacy server_app attr exported via locals()
111
111
  classify_sensitive_message as _classify_sensitive_message,
112
112
  build_sensitivity_report as _build_sensitivity_report,
113
113
  build_admin_audit_report as _build_admin_audit_report,
@@ -992,7 +992,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
992
992
  create_admin_router=create_admin_router,
993
993
  require_admin=require_admin,
994
994
  get_history=get_history,
995
- get_audit_log=_get_audit_log,
995
+ get_audit_log=get_audit_log,
996
996
  audit_file=AUDIT_FILE,
997
997
  public_user=public_user,
998
998
  load_vpc_config=load_vpc_config,
@@ -43,7 +43,12 @@ def _bool(env: Mapping[str, str], key: str, default: bool = False) -> bool:
43
43
  raw = env.get(key)
44
44
  if raw is None:
45
45
  return default
46
- return raw.strip().lower() in {"1", "true", "yes", "on"}
46
+ normalized = raw.strip().lower()
47
+ if normalized in {"1", "true", "yes", "on"}:
48
+ return True
49
+ if normalized in {"0", "false", "no", "off"}:
50
+ return False
51
+ return default
47
52
 
48
53
 
49
54
  def _int(env: Mapping[str, str], key: str, default: int) -> int:
@@ -56,6 +61,13 @@ def _int(env: Mapping[str, str], key: str, default: int) -> int:
56
61
  return default
57
62
 
58
63
 
64
+ def _port(env: Mapping[str, str], key: str, default: int) -> int:
65
+ port = _int(env, key, default)
66
+ if 1 <= port <= 65535:
67
+ return port
68
+ return default
69
+
70
+
59
71
  @dataclass(frozen=True)
60
72
  class Config:
61
73
  """Everything a caller must know about app-level settings.
@@ -146,7 +158,7 @@ class Config:
146
158
  is_public = app_mode == "public"
147
159
 
148
160
  host = _value(env, "LATTICEAI_HOST", "127.0.0.1")
149
- port = _int(env, "LATTICEAI_PORT", 4825)
161
+ port = _port(env, "LATTICEAI_PORT", 4825)
150
162
  network_exposed = not host_is_loopback(host)
151
163
 
152
164
  cors_extra = [item.strip() for item in _value(env, "LATTICEAI_CORS_ALLOWED_ORIGINS", "").split(",") if item.strip()]
@@ -14,7 +14,7 @@ from pathlib import Path
14
14
  from typing import Any, Dict, List
15
15
 
16
16
 
17
- LEGACY_COMPATIBILITY_VERSION = "8.5.0"
17
+ LEGACY_COMPATIBILITY_VERSION = "8.8.0"
18
18
 
19
19
 
20
20
  @dataclass(frozen=True)
@@ -43,7 +43,7 @@ LEGACY_SHIMS: List[LegacyShim] = [
43
43
  LegacyShim(
44
44
  path="knowledge_graph.py",
45
45
  owner="lattice_brain.graph",
46
- replacement="from lattice_brain.store import KnowledgeGraphStore",
46
+ replacement="from lattice_brain.graph.store import KnowledgeGraphStore",
47
47
  reason="Historical scripts imported the graph store from the repo root.",
48
48
  removal_phase="major-release-after-8.x",
49
49
  ),
@@ -57,7 +57,7 @@ LEGACY_SHIMS: List[LegacyShim] = [
57
57
  LegacyShim(
58
58
  path="kg_schema.py",
59
59
  owner="lattice_brain.graph.schema",
60
- replacement="from lattice_brain.schema import ...",
60
+ replacement="from lattice_brain.graph.schema import ...",
61
61
  reason="Historical graph schema tests and tools referenced root schema symbols.",
62
62
  removal_phase="major-release-after-8.x",
63
63
  ),
@@ -96,53 +96,59 @@ LEGACY_SHIMS: List[LegacyShim] = [
96
96
  reason="Local folder ingestion integrations used the root local knowledge API.",
97
97
  removal_phase="requires-api-route-migration",
98
98
  ),
99
+ ]
100
+
101
+ # Shim layers that have completed their compatibility window and were
102
+ # physically deleted. Kept in the report so consumers (docs, tests, release
103
+ # notes) can tell "removed on purpose" apart from "missing by accident".
104
+ # Removing the internal-only layers is the first step of extracting
105
+ # ``lattice_brain`` as a standalone Brain Core package: the package now has
106
+ # exactly one import surface (``lattice_brain.*`` physical paths).
107
+ REMOVED_SHIMS: List[LegacyShim] = [
99
108
  LegacyShim(
100
109
  path="lattice_brain/store.py",
101
110
  owner="lattice_brain.graph.store",
102
111
  replacement="from lattice_brain.graph.store import KnowledgeGraphStore",
103
- reason="Pre-graph-package imports used the flat Brain store module.",
104
- removal_phase="major-release-after-8.x",
112
+ reason="Pre-graph-package flat modules were internal-only; removed in 8.8.0.",
113
+ removal_phase="removed-8.8.0",
105
114
  layer="brain-flat",
115
+ status="removed",
106
116
  ),
107
117
  LegacyShim(
108
118
  path="lattice_brain/ingest.py",
109
119
  owner="lattice_brain.graph.ingest",
110
120
  replacement="from lattice_brain.graph.ingest import KnowledgeGraphIngestMixin",
111
- reason="Pre-graph-package imports used the flat Brain ingest module.",
112
- removal_phase="major-release-after-8.x",
121
+ reason="Pre-graph-package flat modules were internal-only; removed in 8.8.0.",
122
+ removal_phase="removed-8.8.0",
113
123
  layer="brain-flat",
124
+ status="removed",
114
125
  ),
115
126
  LegacyShim(
116
127
  path="lattice_brain/retrieval.py",
117
128
  owner="lattice_brain.graph.retrieval",
118
129
  replacement="from lattice_brain.graph.retrieval import KnowledgeGraphRetrievalMixin",
119
- reason="Pre-graph-package imports used the flat Brain retrieval module.",
120
- removal_phase="major-release-after-8.x",
130
+ reason="Pre-graph-package flat modules were internal-only; removed in 8.8.0.",
131
+ removal_phase="removed-8.8.0",
121
132
  layer="brain-flat",
133
+ status="removed",
122
134
  ),
123
135
  LegacyShim(
124
- path="latticeai/brain/store.py",
125
- owner="lattice_brain.graph.store",
126
- replacement="from lattice_brain.graph.store import KnowledgeGraphStore",
127
- reason="The deprecated latticeai.brain namespace remains for package users.",
128
- removal_phase="major-release-after-8.x",
129
- layer="deprecated-namespace",
130
- ),
131
- LegacyShim(
132
- path="latticeai/brain/ingest.py",
133
- owner="lattice_brain.graph.ingest",
134
- replacement="from lattice_brain.graph.ingest import KnowledgeGraphIngestMixin",
135
- reason="The deprecated latticeai.brain namespace remains for package users.",
136
- removal_phase="major-release-after-8.x",
136
+ path="latticeai/brain/",
137
+ owner="lattice_brain",
138
+ replacement="import lattice_brain",
139
+ reason="The deprecated latticeai.brain namespace completed its window; removed in 8.8.0.",
140
+ removal_phase="removed-8.8.0",
137
141
  layer="deprecated-namespace",
142
+ status="removed",
138
143
  ),
139
144
  LegacyShim(
140
145
  path="latticeai/services/agent_runtime.py",
141
146
  owner="lattice_brain.runtime.agent_runtime",
142
147
  replacement="from lattice_brain.runtime.agent_runtime import AgentRuntime",
143
- reason="Service-layer imports existed before AgentRuntime moved into Brain runtime.",
144
- removal_phase="major-release-after-8.x",
148
+ reason="The service-layer alias completed its window; removed in 8.8.0.",
149
+ removal_phase="removed-8.8.0",
145
150
  layer="service-alias",
151
+ status="removed",
146
152
  ),
147
153
  ]
148
154
 
@@ -152,23 +158,28 @@ def legacy_shim_report(root: Path | None = None) -> Dict[str, Any]:
152
158
  root = Path(__file__).resolve().parents[2]
153
159
  entries = [shim.as_dict() for shim in LEGACY_SHIMS]
154
160
  missing = [shim.path for shim in LEGACY_SHIMS if not (root / shim.path).exists()]
161
+ lingering = [shim.path for shim in REMOVED_SHIMS if (root / shim.path).exists()]
155
162
  phases = sorted({shim.removal_phase for shim in LEGACY_SHIMS})
156
163
  layers = sorted({shim.layer for shim in LEGACY_SHIMS})
157
164
  return {
158
165
  "schema_version": "legacy-compatibility/v1",
159
166
  "version_target": LEGACY_COMPATIBILITY_VERSION,
160
- "status": "managed" if not missing else "incomplete",
167
+ "status": "managed" if not missing and not lingering else "incomplete",
161
168
  "remaining_count": len(LEGACY_SHIMS),
169
+ "removed_count": len(REMOVED_SHIMS),
162
170
  "missing": missing,
171
+ "lingering": lingering,
163
172
  "removal_phases": phases,
164
173
  "layers": layers,
165
174
  "shims": entries,
175
+ "removed": [shim.as_dict() for shim in REMOVED_SHIMS],
166
176
  }
167
177
 
168
178
 
169
179
  __all__ = [
170
180
  "LEGACY_COMPATIBILITY_VERSION",
171
181
  "LEGACY_SHIMS",
182
+ "REMOVED_SHIMS",
172
183
  "LegacyShim",
173
184
  "legacy_shim_report",
174
185
  ]
@@ -11,7 +11,7 @@ from copy import deepcopy
11
11
  from typing import Any, Dict, List, Optional
12
12
 
13
13
 
14
- MARKETPLACE_VERSION = "8.6.0"
14
+ MARKETPLACE_VERSION = "8.8.0"
15
15
  TEMPLATE_KINDS = ("plugin", "workflow", "agent")
16
16
 
17
17
 
@@ -49,7 +49,7 @@ __all__ = [
49
49
  "remove_skill_directory",
50
50
  ]
51
51
 
52
- WORKSPACE_OS_VERSION = "8.6.0"
52
+ WORKSPACE_OS_VERSION = "8.8.0"
53
53
 
54
54
  # Workspace types separate single-user Personal workspaces from shared
55
55
  # Organization workspaces. Both keep the same local-first JSON store; the type
@@ -13,8 +13,9 @@ import time
13
13
  from dataclasses import dataclass
14
14
  from pathlib import Path
15
15
 
16
- # Set MLX_VLM_DRAFT_KIND to 'mtp' to enable the Gemma 4 assistant MTP drafter.
17
- os.environ["MLX_VLM_DRAFT_KIND"] = "mtp"
16
+ # Default Gemma 4 assistant drafting to MTP without overriding an operator's
17
+ # explicit MLX runtime choice.
18
+ os.environ.setdefault("MLX_VLM_DRAFT_KIND", "mtp")
18
19
 
19
20
  from concurrent.futures import ThreadPoolExecutor
20
21
  from typing import AsyncIterator, Dict, Optional, Tuple, List
@@ -27,7 +27,7 @@ def build_persistence_runtime(
27
27
  import os
28
28
  from pathlib import Path
29
29
 
30
- from lattice_brain.identity import DeviceIdentity
30
+ from lattice_brain.graph.identity import DeviceIdentity
31
31
  from lattice_brain.ingestion import IngestionPipeline
32
32
  from lattice_brain.portability import KGPortabilityService
33
33
  from latticeai.core.agent_registry import AgentRegistry
@@ -30,7 +30,7 @@ def build_brain_network(
30
30
  ) -> Any:
31
31
  """Construct peer sync/network service for brain portability routes."""
32
32
 
33
- from lattice_brain.network import BrainNetwork
33
+ from lattice_brain.graph.network import BrainNetwork
34
34
 
35
35
  return BrainNetwork(
36
36
  identity=identity,
@@ -162,16 +162,22 @@ def build_auth_admin_security_router_bundle(
162
162
  append_audit_event=append_audit_event,
163
163
  )
164
164
 
165
- def security_audit_events_safe() -> list[dict[str, Any]]:
165
+ def audit_log_events_safe() -> list[dict[str, Any]]:
166
166
  try:
167
- return get_audit_log(audit_file)
167
+ return get_audit_log()
168
+ except TypeError:
169
+ try:
170
+ return get_audit_log(audit_file)
171
+ except Exception as exc: # pragma: no cover - defensive legacy behavior
172
+ logger.warning("security audit events load failed: %s", exc)
173
+ return []
168
174
  except Exception as exc: # pragma: no cover - defensive legacy behavior
169
175
  logger.warning("security audit events load failed: %s", exc)
170
176
  return []
171
177
 
172
178
  def security_list_uploaded_files() -> list[dict[str, Any]]:
173
179
  files: list[dict[str, Any]] = []
174
- for idx, event in enumerate(security_audit_events_safe()):
180
+ for idx, event in enumerate(audit_log_events_safe()):
175
181
  if event.get("event_type") != "document_upload":
176
182
  continue
177
183
  files.append({
@@ -191,7 +197,7 @@ def build_auth_admin_security_router_bundle(
191
197
  security_router = create_security_router(
192
198
  require_admin=require_admin,
193
199
  get_history=get_history,
194
- get_audit_events=security_audit_events_safe,
200
+ get_audit_events=audit_log_events_safe,
195
201
  classify_sensitive_message=classify_sensitive_message,
196
202
  build_sensitivity_report=build_sensitivity_report,
197
203
  list_uploaded_files=security_list_uploaded_files,
@@ -205,7 +211,7 @@ def build_auth_admin_security_router_bundle(
205
211
  "security_router": security_router,
206
212
  "_graph_stats_safe": graph_stats_safe,
207
213
  "_product_hardening_status": product_hardening_status,
208
- "_security_audit_events_safe": security_audit_events_safe,
214
+ "_security_audit_events_safe": audit_log_events_safe,
209
215
  "_security_list_uploaded_files": security_list_uploaded_files,
210
216
  }
211
217
 
@@ -1,6 +1,6 @@
1
1
  """Machine-checkable architecture readiness gates for release work.
2
2
 
3
- 8.6.0 keeps the major architecture priorities under an explicit release
3
+ 8.8.0 keeps the major architecture priorities under an explicit release
4
4
  contract while product maturity work reduces visible beta seams. AgentRuntime, ToolRegistry,
5
5
  central Config, decomposed server runtime, and Knowledge Graph stabilization
6
6
  must remain discoverable, ordered, and backed by tests before the release can be
@@ -17,7 +17,7 @@ from typing import Any, Dict, List
17
17
  from latticeai.core.legacy_compatibility import legacy_shim_report
18
18
 
19
19
 
20
- ARCHITECTURE_VERSION_TARGET = "8.6.0"
20
+ ARCHITECTURE_VERSION_TARGET = "8.8.0"
21
21
 
22
22
  PREFERRED_REFACTORING_ORDER = [
23
23
  "agent-runtime",
@@ -533,6 +533,10 @@ class MemoryService:
533
533
  "title": item.get("title"),
534
534
  "snippet": item.get("snippet"),
535
535
  "score": item.get("score", 0),
536
+ # Evidence explainability: why this item was recalled, so the
537
+ # citation UI can show the matched terms instead of a bare score.
538
+ "matched_terms": item.get("matched_terms") or [],
539
+ "confidence": item.get("confidence") or "low",
536
540
  }
537
541
  for item in recall.get("results", [])[: max(1, min(limit, 8))]
538
542
  ]
@@ -616,15 +620,17 @@ class MemoryService:
616
620
  q = str(query or "").strip()
617
621
  query_tokens = [tok for tok in q.lower().split() if tok]
618
622
 
619
- def _lexical_score(*texts: Any) -> float:
623
+ def _matched_terms(*texts: Any) -> List[str]:
624
+ haystack = " ".join(str(t or "") for t in texts).lower()
625
+ return [tok for tok in query_tokens if tok in haystack]
626
+
627
+ def _lexical_score(matched: List[str]) -> float:
620
628
  # Honest, comparable relevance: fraction of query tokens present.
621
629
  # Both tiers share this scorer so the cross-tier ranking is real,
622
630
  # not an artifact of per-tier constants.
623
631
  if not query_tokens:
624
632
  return 0.0
625
- haystack = " ".join(str(t or "") for t in texts).lower()
626
- hits = sum(1 for tok in query_tokens if tok in haystack)
627
- return round(hits / len(query_tokens), 4)
633
+ return round(len(matched) / len(query_tokens), 4)
628
634
 
629
635
  results: List[Dict[str, Any]] = []
630
636
 
@@ -633,13 +639,15 @@ class MemoryService:
633
639
  except Exception:
634
640
  mem = []
635
641
  for m in mem:
642
+ matched = _matched_terms(m.get("content"), " ".join(m.get("tags") or []), m.get("kind"))
636
643
  results.append({
637
644
  "source": "workspace",
638
645
  "id": m.get("id"),
639
646
  "title": (m.get("kind") or "memory"),
640
647
  "snippet": str(m.get("content") or "")[:240],
641
648
  "kind": m.get("kind"),
642
- "score": _lexical_score(m.get("content"), " ".join(m.get("tags") or []), m.get("kind")),
649
+ "score": _lexical_score(matched),
650
+ "matched_terms": matched,
643
651
  "tags": m.get("tags") or [],
644
652
  })
645
653
 
@@ -650,17 +658,40 @@ class MemoryService:
650
658
  except Exception:
651
659
  hits = []
652
660
  for hit in hits[:limit]:
661
+ matched = _matched_terms(hit.get("title"), hit.get("name"), hit.get("summary"), hit.get("content"))
653
662
  results.append({
654
663
  "source": "graph",
655
664
  "id": hit.get("id") or hit.get("node_id"),
656
665
  "title": hit.get("title") or hit.get("name") or "node",
657
666
  "snippet": str(hit.get("summary") or hit.get("content") or "")[:240],
658
667
  "kind": hit.get("type") or "node",
659
- "score": _lexical_score(hit.get("title"), hit.get("name"), hit.get("summary"), hit.get("content")),
668
+ "score": _lexical_score(matched),
669
+ "matched_terms": matched,
660
670
  })
661
671
 
672
+ # Quality gate: when at least one result carries real lexical evidence,
673
+ # zero-score rows are noise relative to it and are dropped. When nothing
674
+ # scores (e.g. tokenization mismatch), everything is kept so the tiers'
675
+ # own search filters still decide — the gate never empties a recall.
676
+ candidates = len(results)
677
+ if query_tokens and any(r.get("score", 0) > 0 for r in results):
678
+ results = [r for r in results if r.get("score", 0) > 0]
679
+ for r in results:
680
+ r["confidence"] = "high" if r.get("score", 0) >= 0.65 else "medium" if r.get("score", 0) >= 0.3 else "low"
681
+
662
682
  results.sort(key=lambda r: r.get("score", 0), reverse=True)
663
- return {"query": q, "results": results[: max(1, min(limit, 100))], "count": len(results), "source": "live"}
683
+ return {
684
+ "query": q,
685
+ "results": results[: max(1, min(limit, 100))],
686
+ "count": len(results),
687
+ "source": "live",
688
+ "quality_gate": {
689
+ "candidates": candidates,
690
+ "passed": len(results),
691
+ "filtered": candidates - len(results),
692
+ "gate": "lexical-evidence/v1",
693
+ },
694
+ }
664
695
 
665
696
  # ── inspect a single tier ─────────────────────────────────────────────
666
697
  def inspect(self, source: str, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None, limit: int = 50) -> Dict[str, Any]:
@@ -16,6 +16,7 @@ import shutil
16
16
  import time
17
17
  import urllib.error
18
18
  import urllib.request
19
+ import warnings
19
20
  from pathlib import Path
20
21
  from typing import AsyncIterator, Dict, List, Optional
21
22
 
@@ -100,6 +101,21 @@ class ModelRuntimeState:
100
101
  self.get_user_api_key = _missing_user_api_key
101
102
 
102
103
  def sync_to_module_globals(self):
104
+ """Sync to bare module globals for legacy external importers.
105
+
106
+ This is a compatibility surface only. Internal code should use STATE.
107
+ Emits DeprecationWarning (future removal in major after 8.x).
108
+ """
109
+ warnings.warn(
110
+ "sync_to_module_globals is a legacy compatibility shim and will be removed "
111
+ "after 8.x. Use latticeai.services.model_runtime.STATE or injected context instead.",
112
+ DeprecationWarning,
113
+ stacklevel=2,
114
+ )
115
+ self._sync_globals()
116
+
117
+ def _sync_globals(self) -> None:
118
+ """Internal no-warning sync used at init."""
103
119
  global router, APP_MODE, DEFAULT_HOST, DEFAULT_PORT, DATA_DIR, BASE_DIR
104
120
  global ENABLE_TELEGRAM, ENABLE_GRAPH, AUTOLOAD_MODELS, MODEL_IDLE_UNLOAD_SECONDS
105
121
  global ALLOW_LOCAL_MODELS, REQUIRE_AUTH, INVITE_GATE_ENABLED, ALLOW_PLAINTEXT_API_KEYS
@@ -128,7 +144,7 @@ class ModelRuntimeState:
128
144
  get_user_api_key = self.get_user_api_key
129
145
 
130
146
  STATE = ModelRuntimeState()
131
- STATE.sync_to_module_globals()
147
+ STATE._sync_globals() # initial no-warning; public API warns on explicit legacy syncs
132
148
 
133
149
  # Configured by server_app.configure_model_runtime during app assembly.
134
150
 
@@ -141,7 +157,9 @@ def _env_bool(key: str, default: bool = False) -> bool:
141
157
 
142
158
 
143
159
  def _download_allowed(allow_download: bool = False) -> bool:
144
- return bool(allow_download) or _env_bool("LATTICEAI_ALLOW_MODEL_DOWNLOADS", default=False) or bool(AUTOLOAD_MODELS)
160
+ # Prefer STATE (the source of truth) over bare module global for internal logic.
161
+ autoload = getattr(STATE, "AUTOLOAD_MODELS", AUTOLOAD_MODELS)
162
+ return bool(allow_download) or _env_bool("LATTICEAI_ALLOW_MODEL_DOWNLOADS", default=False) or bool(autoload)
145
163
 
146
164
 
147
165
  def _download_block(provider: str, model_name: str) -> None:
@@ -204,7 +222,7 @@ def configure_model_runtime(**deps) -> None:
204
222
  elif key == "get_user_api_key":
205
223
  STATE.get_user_api_key = value
206
224
 
207
- STATE.sync_to_module_globals()
225
+ STATE._sync_globals() # wiring path uses internal (no spurious deprecation in normal startup)
208
226
 
209
227
 
210
228
  # Catalog data + version-dedup helpers live in ``model_catalog``; re-exported
@@ -727,7 +745,8 @@ def engine_installed(engine: str) -> bool:
727
745
  return False
728
746
 
729
747
  def engine_status() -> List[Dict]:
730
- cloud_models = router.detected_cloud_models()
748
+ r = getattr(STATE, "router", None) or router
749
+ cloud_models = r.detected_cloud_models() if r else []
731
750
  cloud_by_provider = {}
732
751
  for model in cloud_models:
733
752
  cloud_by_provider.setdefault(model["provider"], []).append(model)
@@ -887,37 +906,41 @@ def engine_status() -> List[Dict]:
887
906
  return engines
888
907
 
889
908
  def runtime_features() -> Dict:
909
+ # Read from STATE object (central) for implementation; bare globals kept only
910
+ # for external legacy consumers who import names directly from this module.
911
+ s = STATE
912
+ r = getattr(s, "router", None) or router
890
913
  return {
891
- "mode": APP_MODE,
892
- "public": IS_PUBLIC_MODE,
893
- "host": DEFAULT_HOST,
894
- "port": DEFAULT_PORT,
895
- "data_dir": str(DATA_DIR),
896
- "telegram_enabled": ENABLE_TELEGRAM,
897
- "graph_enabled": ENABLE_GRAPH,
898
- "autoload_models": AUTOLOAD_MODELS,
899
- "model_idle_unload_seconds": MODEL_IDLE_UNLOAD_SECONDS,
900
- "model_memory_policy": router.model_memory_policy(),
901
- "allow_local_models": ALLOW_LOCAL_MODELS,
914
+ "mode": s.APP_MODE,
915
+ "public": s.IS_PUBLIC_MODE,
916
+ "host": s.DEFAULT_HOST,
917
+ "port": s.DEFAULT_PORT,
918
+ "data_dir": str(s.DATA_DIR),
919
+ "telegram_enabled": s.ENABLE_TELEGRAM,
920
+ "graph_enabled": s.ENABLE_GRAPH,
921
+ "autoload_models": s.AUTOLOAD_MODELS,
922
+ "model_idle_unload_seconds": s.MODEL_IDLE_UNLOAD_SECONDS,
923
+ "model_memory_policy": r.model_memory_policy() if r else None,
924
+ "allow_local_models": s.ALLOW_LOCAL_MODELS,
902
925
  "security": {
903
- "host": DEFAULT_HOST,
904
- "require_auth": REQUIRE_AUTH,
905
- "invite_gate_enabled": INVITE_GATE_ENABLED,
906
- "keyring_available": keyring is not None,
907
- "plaintext_api_keys_allowed": ALLOW_PLAINTEXT_API_KEYS,
908
- "cors_allow_network": CORS_ALLOW_NETWORK,
926
+ "host": s.DEFAULT_HOST,
927
+ "require_auth": s.REQUIRE_AUTH,
928
+ "invite_gate_enabled": s.INVITE_GATE_ENABLED,
929
+ "keyring_available": s.keyring is not None,
930
+ "plaintext_api_keys_allowed": s.ALLOW_PLAINTEXT_API_KEYS,
931
+ "cors_allow_network": s.CORS_ALLOW_NETWORK,
909
932
  },
910
- "default_model": PUBLIC_MODEL if IS_PUBLIC_MODE else LOCAL_MODEL,
933
+ "default_model": s.PUBLIC_MODEL if s.IS_PUBLIC_MODE else s.LOCAL_MODEL,
911
934
  "local_only_features": {
912
- "mlx": ALLOW_LOCAL_MODELS and not IS_PUBLIC_MODE,
913
- "telegram_bridge": ENABLE_TELEGRAM,
914
- "desktop_chrome_bridge": not IS_PUBLIC_MODE,
915
- "computer_use_bridge": not IS_PUBLIC_MODE,
935
+ "mlx": s.ALLOW_LOCAL_MODELS and not s.IS_PUBLIC_MODE,
936
+ "telegram_bridge": s.ENABLE_TELEGRAM,
937
+ "desktop_chrome_bridge": not s.IS_PUBLIC_MODE,
938
+ "computer_use_bridge": not s.IS_PUBLIC_MODE,
916
939
  },
917
940
  "public_features": {
918
941
  "web_ui": True,
919
942
  "openai_compatible_models": True,
920
- "persistent_data_dir": str(DATA_DIR),
943
+ "persistent_data_dir": str(s.DATA_DIR),
921
944
  },
922
945
  }
923
946
 
@@ -1099,7 +1122,8 @@ async def _probe_cloud_model(model_ref: str) -> Dict[str, object]:
1099
1122
 
1100
1123
  async def verify_cloud_models(force: bool = False, provider_filter: Optional[str] = None) -> Dict[str, Dict]:
1101
1124
  now = time.time()
1102
- cloud_items = [item for item in router.detected_cloud_models() if item.get("tag") == "cloud"]
1125
+ r = getattr(STATE, "router", None) or router
1126
+ cloud_items = [item for item in (r.detected_cloud_models() if r else []) if item.get("tag") == "cloud"]
1103
1127
  if provider_filter:
1104
1128
  cloud_items = [item for item in cloud_items if item.get("provider") == provider_filter]
1105
1129
 
@@ -18,7 +18,7 @@ from typing import Any, Dict, List
18
18
 
19
19
  from latticeai.services.architecture_readiness import architecture_readiness
20
20
 
21
- PRODUCT_VERSION_TARGET = "8.6.0"
21
+ PRODUCT_VERSION_TARGET = "8.8.0"
22
22
 
23
23
 
24
24
  @dataclass(frozen=True)
@@ -76,10 +76,10 @@ PRODUCT_GATES: List[ProductGate] = [
76
76
  evidence=[
77
77
  "package.json::release:artifacts",
78
78
  "package.json::release:validate",
79
- "README.md::dist/ltcai-8.6.0-py3-none-any.whl",
80
- "README.md::dist/ltcai-8.6.0.tar.gz",
81
- "README.md::dist/ltcai-8.6.0.vsix",
82
- "README.md::ltcai-8.6.0.tgz",
79
+ "README.md::dist/ltcai-8.8.0-py3-none-any.whl",
80
+ "README.md::dist/ltcai-8.8.0.tar.gz",
81
+ "README.md::dist/ltcai-8.8.0.vsix",
82
+ "README.md::ltcai-8.8.0.tgz",
83
83
  "scripts/validate_release_artifacts.py",
84
84
  "scripts/release_smoke.py",
85
85
  "Dockerfile",
@@ -95,12 +95,12 @@ PRODUCT_GATES: List[ProductGate] = [
95
95
  title="Release story is documented and honest",
96
96
  evidence=[
97
97
  "README.md",
98
- "README.md::The current release is **8.6.0",
99
- "SECURITY.md::8.6.x (latest)",
100
- "vscode-extension/README.md::**8.6.0",
101
- "docs/CHANGELOG.md::## [8.6.0]",
98
+ "README.md::The current release is **8.8.0",
99
+ "SECURITY.md::8.8.x (latest)",
100
+ "vscode-extension/README.md::**8.8.0",
101
+ "docs/CHANGELOG.md::## [8.8.0]",
102
102
  "FEATURE_STATUS.md",
103
- "RELEASE_NOTES_v8.6.0.md",
103
+ "RELEASE_NOTES_v8.8.0.md",
104
104
  "latticeai/core/agent.py::SingleAgentRuntime",
105
105
  "latticeai/core/agent.py::AgentRuntime = SingleAgentRuntime",
106
106
  "lattice_brain/runtime/contracts.py::runtime-boundary/v1",
@@ -115,7 +115,7 @@ PRODUCT_GATES: List[ProductGate] = [
115
115
  id="ecosystem-path",
116
116
  title="Community and plugin growth path is explicit",
117
117
  evidence=[
118
- "docs/COMMUNITY_AND_PLUGINS.md::8.6.0",
118
+ "docs/COMMUNITY_AND_PLUGINS.md::8.8.0",
119
119
  "docs/PLUGIN_SDK.md",
120
120
  "plugins/README.md",
121
121
  "plugins/hello-world/plugin.json",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "8.6.0",
3
+ "version": "8.8.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": {
@@ -23,7 +23,8 @@
23
23
  "build:assets": "vite build && node scripts/build_frontend_assets.mjs",
24
24
  "build:python": "node scripts/run_python.mjs -m build",
25
25
  "check:python": "node scripts/run_python.mjs scripts/check_python.py",
26
- "lint": "node --check tests/visual/mock_server.cjs && node --check tests/visual/v3.spec.js && npm run lint:frontend && node scripts/check_i18n_literals.mjs",
26
+ "lint": "npm run lint:python && node --check tests/visual/mock_server.cjs && node --check tests/visual/v3.spec.js && npm run lint:frontend && node scripts/check_i18n_literals.mjs",
27
+ "lint:python": "node scripts/run_python.mjs -m ruff check .",
27
28
  "lint:frontend": "node scripts/lint_frontend.mjs",
28
29
  "docs:check-links": "node scripts/check_markdown_links.mjs",
29
30
  "typecheck": "npm run typecheck:frontend && cd vscode-extension && npm run build",
@@ -27,6 +27,8 @@ TARGETS = [
27
27
  ("latticeai/core/workspace_os.py", "regex", r'(WORKSPACE_OS_VERSION = ")([^"]+)(")'),
28
28
  ("latticeai/core/marketplace.py", "regex", r'(MARKETPLACE_VERSION = ")([^"]+)(")'),
29
29
  ("lattice_brain/runtime/multi_agent.py", "regex", r'(MULTI_AGENT_VERSION = ")([^"]+)(")'),
30
+ ("latticeai/services/architecture_readiness.py", "regex", r'(ARCHITECTURE_VERSION_TARGET = ")([^"]+)(")'),
31
+ ("latticeai/services/product_readiness.py", "regex", r'(PRODUCT_VERSION_TARGET = ")([^"]+)(")'),
30
32
  ("pyproject.toml", "regex", r'(^version = ")([^"]+)(")'),
31
33
  ("package.json", "json", "version"),
32
34
  ("package-lock.json", "package-lock", None),