ltcai 3.6.0 → 4.0.1

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 (238) hide show
  1. package/README.md +39 -31
  2. package/docs/CHANGELOG.md +64 -0
  3. package/docs/REALTIME_COLLABORATION.md +3 -3
  4. package/docs/V3_FRONTEND.md +9 -8
  5. package/docs/V4_BRAIN_ARCHITECTURE.md +322 -0
  6. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +552 -0
  7. package/docs/V4_IMPLEMENTATION_PLAN.md +470 -0
  8. package/docs/kg-schema.md +51 -53
  9. package/docs/spec-vs-impl.md +10 -10
  10. package/kg_schema.py +2 -520
  11. package/knowledge_graph.py +37 -4629
  12. package/knowledge_graph_api.py +11 -127
  13. package/latticeai/__init__.py +1 -1
  14. package/latticeai/api/admin.py +16 -17
  15. package/latticeai/api/agents.py +20 -7
  16. package/latticeai/api/auth.py +46 -15
  17. package/latticeai/api/chat.py +112 -76
  18. package/latticeai/api/health.py +1 -1
  19. package/latticeai/api/hooks.py +1 -1
  20. package/latticeai/api/invitations.py +100 -0
  21. package/latticeai/api/knowledge_graph.py +139 -0
  22. package/latticeai/api/local_files.py +1 -1
  23. package/latticeai/api/mcp.py +23 -11
  24. package/latticeai/api/memory.py +1 -1
  25. package/latticeai/api/models.py +1 -1
  26. package/latticeai/api/network.py +81 -0
  27. package/latticeai/api/plugins.py +3 -6
  28. package/latticeai/api/realtime.py +5 -8
  29. package/latticeai/api/search.py +26 -2
  30. package/latticeai/api/security_dashboard.py +2 -3
  31. package/latticeai/api/setup.py +2 -2
  32. package/latticeai/api/static_routes.py +11 -16
  33. package/latticeai/api/tools.py +3 -0
  34. package/latticeai/api/ui_redirects.py +26 -0
  35. package/latticeai/api/workflow_designer.py +85 -6
  36. package/latticeai/api/workspace.py +93 -57
  37. package/latticeai/app_factory.py +1781 -0
  38. package/latticeai/brain/__init__.py +18 -0
  39. package/latticeai/brain/_kg_common.py +1123 -0
  40. package/latticeai/brain/context.py +213 -0
  41. package/latticeai/brain/conversations.py +236 -0
  42. package/latticeai/brain/discovery.py +1455 -0
  43. package/latticeai/brain/documents.py +218 -0
  44. package/latticeai/brain/identity.py +175 -0
  45. package/latticeai/brain/ingest.py +644 -0
  46. package/latticeai/brain/memory.py +102 -0
  47. package/latticeai/brain/network.py +205 -0
  48. package/latticeai/brain/projection.py +561 -0
  49. package/latticeai/brain/provenance.py +401 -0
  50. package/latticeai/brain/retrieval.py +1316 -0
  51. package/latticeai/brain/schema.py +640 -0
  52. package/latticeai/brain/store.py +216 -0
  53. package/latticeai/brain/write_master.py +225 -0
  54. package/latticeai/core/agent.py +31 -7
  55. package/latticeai/core/audit.py +0 -7
  56. package/latticeai/core/config.py +1 -1
  57. package/latticeai/core/context_builder.py +1 -2
  58. package/latticeai/core/enterprise.py +1 -1
  59. package/latticeai/core/graph_curator.py +2 -2
  60. package/latticeai/core/invitations.py +131 -0
  61. package/latticeai/core/marketplace.py +1 -1
  62. package/latticeai/core/mcp_registry.py +791 -0
  63. package/latticeai/core/model_compat.py +1 -1
  64. package/latticeai/core/model_resolution.py +0 -1
  65. package/latticeai/core/multi_agent.py +238 -4
  66. package/latticeai/core/policy.py +54 -0
  67. package/latticeai/core/realtime.py +65 -44
  68. package/latticeai/core/security.py +1 -1
  69. package/latticeai/core/sessions.py +66 -10
  70. package/latticeai/core/users.py +147 -0
  71. package/latticeai/core/workflow_engine.py +114 -2
  72. package/latticeai/core/workspace_os.py +477 -29
  73. package/latticeai/models/__init__.py +7 -0
  74. package/latticeai/models/router.py +779 -0
  75. package/latticeai/server_app.py +29 -1536
  76. package/latticeai/services/agent_runtime.py +243 -4
  77. package/latticeai/services/app_context.py +75 -14
  78. package/latticeai/services/ingestion.py +47 -0
  79. package/latticeai/services/kg_portability.py +33 -3
  80. package/latticeai/services/memory_service.py +39 -11
  81. package/latticeai/services/model_runtime.py +2 -5
  82. package/latticeai/services/platform_runtime.py +100 -23
  83. package/latticeai/services/run_executor.py +328 -0
  84. package/latticeai/services/search_service.py +17 -8
  85. package/latticeai/services/tool_dispatch.py +12 -2
  86. package/latticeai/services/triggers.py +241 -0
  87. package/latticeai/services/upload_service.py +37 -12
  88. package/latticeai/services/workspace_service.py +55 -16
  89. package/llm_router.py +29 -772
  90. package/ltcai_cli.py +1 -2
  91. package/mcp_registry.py +25 -788
  92. package/p_reinforce.py +124 -14
  93. package/package.json +10 -20
  94. package/scripts/bump_version.py +99 -0
  95. package/scripts/generate_diagrams.py +0 -1
  96. package/scripts/lint_v3.mjs +105 -18
  97. package/scripts/validate_release_artifacts.py +0 -1
  98. package/scripts/wheel_smoke.py +142 -0
  99. package/server.py +11 -7
  100. package/setup_wizard.py +1142 -0
  101. package/static/sw.js +81 -52
  102. package/static/v3/asset-manifest.json +33 -25
  103. package/static/v3/css/{lattice.base.e4cdd05d.css → lattice.base.49deefb5.css} +1 -1
  104. package/static/v3/css/lattice.base.css +1 -1
  105. package/static/v3/css/{lattice.components.9b49d614.css → lattice.components.cde18231.css} +1 -1
  106. package/static/v3/css/lattice.components.css +1 -1
  107. package/static/v3/css/{lattice.shell.8fcc9d33.css → lattice.shell.29d36d85.css} +1 -1
  108. package/static/v3/css/lattice.shell.css +1 -1
  109. package/static/v3/css/{lattice.tokens.e7018963.css → lattice.tokens.304cbc40.css} +3 -0
  110. package/static/v3/css/lattice.tokens.css +3 -0
  111. package/static/v3/css/{lattice.views.22f69117.css → lattice.views.0a18b6c5.css} +2 -2
  112. package/static/v3/css/lattice.views.css +2 -2
  113. package/static/v3/index.html +3 -4
  114. package/static/v3/js/{app.c541f955.js → app.c5c80c46.js} +1 -1
  115. package/static/v3/js/core/{api.33d6320e.js → api.ba0fbf14.js} +58 -1
  116. package/static/v3/js/core/api.js +57 -0
  117. package/static/v3/js/core/i18n.880e1fec.js +575 -0
  118. package/static/v3/js/core/i18n.js +575 -0
  119. package/static/v3/js/core/routes.37522821.js +101 -0
  120. package/static/v3/js/core/routes.js +71 -63
  121. package/static/v3/js/core/{shell.8c163e0e.js → shell.e3f6bbfa.js} +68 -39
  122. package/static/v3/js/core/shell.js +66 -37
  123. package/static/v3/js/core/{store.34ebd5e6.js → store.7b2aa044.js} +11 -1
  124. package/static/v3/js/core/store.js +11 -1
  125. package/static/v3/js/views/account.eff40715.js +143 -0
  126. package/static/v3/js/views/account.js +143 -0
  127. package/static/v3/js/views/activity.0d271ef9.js +67 -0
  128. package/static/v3/js/views/activity.js +67 -0
  129. package/static/v3/js/views/{admin-users.03bac88c.js → admin-users.f7ac7b43.js} +4 -6
  130. package/static/v3/js/views/admin-users.js +4 -6
  131. package/static/v3/js/views/{agents.014d0b74.js → agents.17c5288d.js} +35 -12
  132. package/static/v3/js/views/agents.js +35 -12
  133. package/static/v3/js/views/{chat.e6dd7dd0.js → chat.e250e2cc.js} +23 -0
  134. package/static/v3/js/views/chat.js +23 -0
  135. package/static/v3/js/views/graph-canvas.17c15d65.js +509 -0
  136. package/static/v3/js/views/graph-canvas.js +509 -0
  137. package/static/v3/js/views/{hybrid-search.b22b97e0.js → hybrid-search.2fb63ed9.js} +1 -2
  138. package/static/v3/js/views/hybrid-search.js +1 -2
  139. package/static/v3/js/views/{knowledge-graph.a96040a5.js → knowledge-graph.4d09c537.js} +60 -44
  140. package/static/v3/js/views/knowledge-graph.js +60 -44
  141. package/static/v3/js/views/network.52a4f181.js +97 -0
  142. package/static/v3/js/views/network.js +97 -0
  143. package/static/v3/js/views/{planning.9ac3e313.js → planning.4876fd77.js} +26 -5
  144. package/static/v3/js/views/planning.js +26 -5
  145. package/static/v3/js/views/runs.b63b2afa.js +144 -0
  146. package/static/v3/js/views/runs.js +144 -0
  147. package/static/v3/js/views/{settings.8631fa5e.js → settings.b7140634.js} +7 -8
  148. package/static/v3/js/views/settings.js +7 -8
  149. package/static/v3/js/views/snapshots.6f5db095.js +135 -0
  150. package/static/v3/js/views/snapshots.js +135 -0
  151. package/static/v3/js/views/{workflows.26c57290.js → workflows.7752225a.js} +87 -2
  152. package/static/v3/js/views/workflows.js +87 -2
  153. package/static/v3/js/views/workspace-admin.c466029b.js +156 -0
  154. package/static/v3/js/views/workspace-admin.js +156 -0
  155. package/static/vendor/chart.umd.min.js +20 -0
  156. package/static/vendor/fonts/inter-latin-300-normal.woff2 +0 -0
  157. package/static/vendor/fonts/inter-latin-400-normal.woff2 +0 -0
  158. package/static/vendor/fonts/inter-latin-500-normal.woff2 +0 -0
  159. package/static/vendor/fonts/inter-latin-600-normal.woff2 +0 -0
  160. package/static/vendor/fonts/inter-latin-700-normal.woff2 +0 -0
  161. package/static/vendor/fonts/inter-latin-800-normal.woff2 +0 -0
  162. package/static/vendor/fonts/inter.css +44 -0
  163. package/static/vendor/icons/tabler-icons.min.css +4 -0
  164. package/static/vendor/icons/tabler-icons.woff2 +0 -0
  165. package/static/vendor/marked.min.js +69 -0
  166. package/telegram_bot.py +1 -2
  167. package/tools/commands.py +4 -2
  168. package/tools/computer.py +1 -1
  169. package/tools/documents.py +1 -3
  170. package/tools/filesystem.py +0 -4
  171. package/tools/knowledge.py +1 -3
  172. package/tools/network.py +1 -3
  173. package/codex_telegram_bot.py +0 -195
  174. package/docs/assets/v3.4.0/agent-run.png +0 -0
  175. package/docs/assets/v3.4.0/agents.png +0 -0
  176. package/docs/assets/v3.4.0/before/chat-before.png +0 -0
  177. package/docs/assets/v3.4.0/before/files-before.png +0 -0
  178. package/docs/assets/v3.4.0/chat.png +0 -0
  179. package/docs/assets/v3.4.0/connect-folder.png +0 -0
  180. package/docs/assets/v3.4.0/files.png +0 -0
  181. package/docs/assets/v3.4.0/home.png +0 -0
  182. package/docs/assets/v3.4.0/hooks-dispatch.png +0 -0
  183. package/docs/assets/v3.4.0/knowledge-graph.png +0 -0
  184. package/docs/assets/v3.4.0/local-agent.png +0 -0
  185. package/docs/assets/v3.4.0/memory.png +0 -0
  186. package/docs/assets/v3.4.0/settings.png +0 -0
  187. package/docs/assets/v3.4.0/vision-input.png +0 -0
  188. package/docs/assets/v3.4.0/workflows.png +0 -0
  189. package/docs/assets/v3.4.1/e2e_runtime_log.txt +0 -42
  190. package/docs/assets/v3.4.1/hooks-dispatch.png +0 -0
  191. package/docs/assets/v3.4.1/local-agent.png +0 -0
  192. package/docs/images/admin-dashboard.png +0 -0
  193. package/docs/images/architecture.png +0 -0
  194. package/docs/images/enterprise.png +0 -0
  195. package/docs/images/graph.png +0 -0
  196. package/docs/images/hero.gif +0 -0
  197. package/docs/images/knowledge-graph.png +0 -0
  198. package/docs/images/lattice-ai-demo.gif +0 -0
  199. package/docs/images/lattice-ai-hero.png +0 -0
  200. package/docs/images/logo.svg +0 -33
  201. package/docs/images/mobile-responsive.png +0 -0
  202. package/docs/images/model-recommendation.png +0 -0
  203. package/docs/images/onboarding.png +0 -0
  204. package/docs/images/organization.png +0 -0
  205. package/docs/images/pipeline.png +0 -0
  206. package/docs/images/screenshot-admin.png +0 -0
  207. package/docs/images/screenshot-chat.png +0 -0
  208. package/docs/images/screenshot-graph.png +0 -0
  209. package/docs/images/skills.png +0 -0
  210. package/docs/images/workspace-dark.png +0 -0
  211. package/docs/images/workspace-light.png +0 -0
  212. package/docs/images/workspace.png +0 -0
  213. package/requirements.txt +0 -16
  214. package/static/account.html +0 -115
  215. package/static/activity.html +0 -73
  216. package/static/admin.html +0 -488
  217. package/static/agents.html +0 -139
  218. package/static/chat.html +0 -844
  219. package/static/css/reference/account.css +0 -439
  220. package/static/css/reference/admin.css +0 -610
  221. package/static/css/reference/base.css +0 -1661
  222. package/static/css/reference/chat.css +0 -4623
  223. package/static/css/reference/graph.css +0 -1016
  224. package/static/css/responsive.css +0 -861
  225. package/static/graph.html +0 -124
  226. package/static/platform.css +0 -104
  227. package/static/plugins.html +0 -136
  228. package/static/scripts/account.js +0 -238
  229. package/static/scripts/admin.js +0 -1614
  230. package/static/scripts/chat.js +0 -5081
  231. package/static/scripts/graph.js +0 -1804
  232. package/static/scripts/platform.js +0 -64
  233. package/static/scripts/ux.js +0 -167
  234. package/static/scripts/workspace.js +0 -948
  235. package/static/v3/js/core/routes.2ce3815a.js +0 -93
  236. package/static/workflows.html +0 -146
  237. package/static/workspace.css +0 -1121
  238. package/static/workspace.html +0 -357
package/p_reinforce.py CHANGED
@@ -1,16 +1,21 @@
1
1
  """
2
- P-Reinforce Knowledge Gardener
3
- Raw 데이터를 자동으로 분석해서 구조화된 마크다운 위키로 정리
2
+ P-Reinforce Knowledge Gardener — notes capture with a brain-backed memory.
3
+
4
+ v4 (T4.3 garden absorption): the markdown vault is no longer a second brain.
5
+ The vault stays as the user-owned, Obsidian-compatible *mirror* (capability
6
+ preserved), but the Knowledge Graph is authoritative: notes created through
7
+ the API are ingested through the unified pipeline (provenance + hooks), the
8
+ existing vault is imported idempotently, and chat context comes from brain
9
+ queries instead of an O(n) vault scan per message.
4
10
  """
5
11
 
6
- import json
12
+ import logging
7
13
  import os
8
14
  import re
9
- import time
10
15
  import shutil
11
16
  from datetime import datetime
12
17
  from pathlib import Path
13
- from typing import Optional
18
+ from typing import Any, Optional
14
19
 
15
20
  BRAIN_DIR = Path(
16
21
  os.getenv("LATTICEAI_OBSIDIAN_VAULT_DIR")
@@ -28,7 +33,9 @@ STRUCTURE = {
28
33
 
29
34
 
30
35
  class PReinforceGardener:
31
- def __init__(self):
36
+ def __init__(self, ingestion_pipeline: Any = None, knowledge_graph: Any = None):
37
+ self._pipeline = ingestion_pipeline
38
+ self._kg = knowledge_graph
32
39
  self._ensure_structure()
33
40
 
34
41
  def _ensure_structure(self):
@@ -43,6 +50,7 @@ class PReinforceGardener:
43
50
  lines = ["# 🧠 Lattice AI Brain — P-Reinforce Index\n"]
44
51
  lines.append(f"*Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}*\n")
45
52
  lines.append("\nThis folder is an Obsidian-compatible Markdown vault.\n")
53
+ lines.append("\nThe Knowledge Graph is the authoritative store; this vault is the\nuser-owned markdown mirror of garden notes.\n")
46
54
  for folder, desc in STRUCTURE.items():
47
55
  lines.append(f"## [{folder}](./{folder}/)\n_{desc}_\n")
48
56
  lines.append("## Connector Status\n")
@@ -87,14 +95,14 @@ class PReinforceGardener:
87
95
  filename = self._make_filename(raw_data, folder)
88
96
  filepath = BRAIN_DIR / folder / filename
89
97
 
90
- # 마크다운 래핑
98
+ # 마크다운 미러 (사용자 소유 Obsidian 호환 아티팩트)
91
99
  content = self._wrap_markdown(raw_data, folder)
92
100
  filepath.write_text(content, encoding="utf-8")
93
101
 
94
102
  # 오늘 로그에도 기록
95
103
  self._append_log(raw_data[:200], folder, filename)
96
104
 
97
- return {
105
+ result = {
98
106
  "status": "saved",
99
107
  "folder": folder,
100
108
  "filename": filename,
@@ -102,6 +110,68 @@ class PReinforceGardener:
102
110
  "classified_as": folder,
103
111
  "description": STRUCTURE[folder],
104
112
  }
113
+ # 두뇌(Knowledge Graph)가 정식 저장소: 통합 수집 파이프라인으로 ingest.
114
+ result.update(self._ingest_note(raw_data, source_uri=str(filepath), folder=folder))
115
+ return result
116
+
117
+ def _ingest_note(self, text: str, *, source_uri: str, folder: str, title: Optional[str] = None) -> dict:
118
+ if self._pipeline is None:
119
+ return {"graph": "unavailable", "graph_detail": "ingestion pipeline not wired"}
120
+ try:
121
+ from latticeai.services.ingestion import IngestionItem
122
+
123
+ ingest = self._pipeline.ingest(
124
+ IngestionItem(
125
+ source_type="note",
126
+ title=title or text.strip().split("\n")[0][:80],
127
+ text=text,
128
+ source_uri=source_uri,
129
+ metadata={"garden_folder": folder, "pipeline": "p-reinforce"},
130
+ )
131
+ )
132
+ if ingest.status != "ok":
133
+ return {"graph": ingest.status, "graph_detail": ingest.detail}
134
+ return {
135
+ "graph": "ok",
136
+ "graph_node_id": ingest.node_id,
137
+ "provenance_id": ingest.provenance_id,
138
+ "duplicate": ingest.duplicate,
139
+ }
140
+ except Exception as exc:
141
+ logging.warning("garden note ingest failed: %s", exc)
142
+ return {"graph": "failed", "graph_detail": str(exc)}
143
+
144
+ def import_vault(self) -> dict:
145
+ """Idempotent import of every existing vault note into the brain.
146
+
147
+ Content-hash dedup in the store makes re-runs safe; vault files are
148
+ never modified or deleted. INDEX.md and the daily logs are skipped.
149
+ """
150
+ if self._pipeline is None:
151
+ return {"status": "unavailable", "imported": 0}
152
+ imported = duplicates = failed = 0
153
+ for file_path in sorted(BRAIN_DIR.rglob("*.md")):
154
+ if file_path.name == "INDEX.md" or "40_Log" in file_path.parts:
155
+ continue
156
+ try:
157
+ text = file_path.read_text(encoding="utf-8")
158
+ except Exception:
159
+ failed += 1
160
+ continue
161
+ folder = file_path.parent.name if file_path.parent != BRAIN_DIR else "00_Raw"
162
+ outcome = self._ingest_note(
163
+ text, source_uri=str(file_path), folder=folder, title=file_path.stem
164
+ )
165
+ if outcome.get("graph") == "ok":
166
+ if outcome.get("duplicate"):
167
+ duplicates += 1
168
+ else:
169
+ imported += 1
170
+ else:
171
+ failed += 1
172
+ if imported:
173
+ logging.info("garden: imported %d vault notes into the brain (%d already known)", imported, duplicates)
174
+ return {"status": "ok", "imported": imported, "duplicates": duplicates, "failed": failed}
105
175
 
106
176
  def _wrap_markdown(self, raw: str, folder: str) -> str:
107
177
  now = datetime.now().strftime("%Y-%m-%d %H:%M")
@@ -112,7 +182,7 @@ class PReinforceGardener:
112
182
  "---\n",
113
183
  raw,
114
184
  "\n\n---",
115
- f"*Auto-organized by P-Reinforce Gardener*",
185
+ "*Auto-organized by P-Reinforce Gardener*",
116
186
  ]
117
187
  return "\n".join(lines)
118
188
 
@@ -127,22 +197,62 @@ class PReinforceGardener:
127
197
 
128
198
  # ── Tree ──────────────────────────────────────────────────────────────────
129
199
 
200
+ def get_tree(self) -> dict:
201
+ """지식 정원 파일트리 (마크다운 미러 기준)."""
202
+ folders = []
203
+ for folder, desc in STRUCTURE.items():
204
+ folder_path = BRAIN_DIR / folder
205
+ files = []
206
+ if folder_path.exists():
207
+ for file_path in sorted(folder_path.glob("*.md")):
208
+ try:
209
+ stat = file_path.stat()
210
+ files.append({
211
+ "name": file_path.name,
212
+ "size_bytes": stat.st_size,
213
+ "modified_at": datetime.fromtimestamp(stat.st_mtime).isoformat(timespec="seconds"),
214
+ })
215
+ except OSError:
216
+ continue
217
+ folders.append({"name": folder, "description": desc, "files": files, "count": len(files)})
218
+ return {"root": str(BRAIN_DIR), "folders": folders}
219
+
130
220
  def get_relevant_context(self, query: str, limit: int = 3) -> str:
131
- """질문과 관련된 지식을 검색하여 컨텍스트 문자열을 반환합니다."""
221
+ """질문과 관련된 정원 노트를 두뇌에서 검색해 컨텍스트로 반환.
222
+
223
+ v4: 채팅마다 vault 전체를 rglob 하던 O(n) 스캔을 브레인 검색으로
224
+ 대체. 그래프가 없으면(비활성) 기존 파일 스캔으로 정직하게 폴백.
225
+ """
226
+ if self._kg is not None:
227
+ try:
228
+ matches = self._kg.search(query, max(limit * 4, 8)).get("matches", [])
229
+ results = []
230
+ for match in matches:
231
+ meta = match.get("metadata") or {}
232
+ if not (meta.get("garden_folder") or meta.get("pipeline") == "p-reinforce"):
233
+ continue
234
+ title = match.get("title") or "note"
235
+ body = match.get("summary") or ""
236
+ results.append(f"--- Document: {title} ---\n{body[:800]}")
237
+ if len(results) >= limit:
238
+ break
239
+ return "\n\n".join(results)
240
+ except Exception as exc:
241
+ logging.debug("garden brain context failed, falling back to vault scan: %s", exc)
242
+ return self._scan_vault_context(query, limit)
243
+
244
+ def _scan_vault_context(self, query: str, limit: int = 3) -> str:
132
245
  results = []
133
- # 모든 마크다운 파일 탐색 (INDEX.md 및 Log 제외)
134
246
  for file_path in BRAIN_DIR.rglob("*.md"):
135
247
  if file_path.name == "INDEX.md" or "40_Log" in str(file_path):
136
248
  continue
137
-
138
249
  try:
139
250
  content = file_path.read_text(encoding="utf-8")
140
- keywords = [k for k in re.split(r'\s+', query) if len(k) > 1]
251
+ keywords = [k for k in re.split(r"\s+", query) if len(k) > 1]
141
252
  if any(k.lower() in content.lower() for k in keywords):
142
253
  results.append(f"--- Document: {file_path.name} ---\n{content[:800]}")
143
254
  if len(results) >= limit:
144
255
  break
145
256
  except Exception:
146
257
  continue
147
-
148
258
  return "\n\n".join(results)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "3.6.0",
4
- "description": "Lattice AI v3 local-first AI workspace platform with knowledge graph, vector index, hybrid search, agents, and workspace modes.",
3
+ "version": "4.0.1",
4
+ "description": "Lattice AI local-first Digital Brain Platform (knowledge graph, durable memory, hybrid search, agents, signed brain exchange)",
5
5
  "homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
6
6
  "repository": {
7
7
  "type": "git",
@@ -21,7 +21,7 @@
21
21
  "build:assets": "node scripts/build_v3_assets.mjs",
22
22
  "build:python": "python3 -m build",
23
23
  "check:python": "python3 scripts/check_python.py",
24
- "lint": "node --check static/scripts/account.js && node --check static/scripts/admin.js && node --check static/scripts/chat.js && node --check static/scripts/graph.js && node --check static/scripts/platform.js && node --check static/scripts/ux.js && node --check static/scripts/workspace.js && node --check tests/visual/mock_server.cjs && node --check tests/visual/v3.spec.js && npm run lint:v3",
24
+ "lint": "node --check tests/visual/mock_server.cjs && node --check tests/visual/v3.spec.js && npm run lint:v3",
25
25
  "lint:v3": "node scripts/lint_v3.mjs",
26
26
  "typecheck": "cd vscode-extension && npm run build",
27
27
  "test": "python3 -m pytest tests/ -v",
@@ -66,42 +66,32 @@
66
66
  "server.py",
67
67
  "kg_schema.py",
68
68
  "knowledge_graph.py",
69
- "knowledge_graph_api.py",
70
69
  "local_knowledge_api.py",
71
70
  "llm_router.py",
72
71
  "p_reinforce.py",
73
72
  "telegram_bot.py",
74
73
  "tools/",
75
- "codex_telegram_bot.py",
76
74
  "mcp_registry.py",
77
75
  "latticeai/**/*.py",
78
76
  "skills/",
79
- "static/account.html",
80
- "static/chat.html",
81
- "static/admin.html",
82
- "static/graph.html",
83
- "static/workspace.html",
84
- "static/plugins.html",
85
- "static/workflows.html",
86
- "static/agents.html",
87
- "static/activity.html",
88
77
  "static/favicon.ico",
89
78
  "static/manifest.json",
90
79
  "static/sw.js",
91
- "static/workspace.css",
92
- "static/platform.css",
93
- "static/scripts/",
94
80
  "static/css/",
95
81
  "static/v3/",
96
82
  "static/icons/",
97
83
  "plugins/",
98
84
  "scripts/",
99
- "docs/",
100
85
  "!docs/images/tmp_frames/",
101
86
  "!**/__pycache__/",
102
87
  "!**/*.pyc",
103
- "requirements.txt",
104
- "README.md"
88
+ "README.md",
89
+ "setup_wizard.py",
90
+ "knowledge_graph_api.py",
91
+ "static/vendor/",
92
+ "docs/*.md",
93
+ "!docs/assets/",
94
+ "!docs/images/"
105
95
  ],
106
96
  "publishConfig": {
107
97
  "access": "public"
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env python3
2
+ """Single-source version bump — rewrites every synchronized version copy.
3
+
4
+ The canonical version has always been nine hand-edited copies guarded by
5
+ tests/unit/test_version_consistency.py. This script makes the bump one
6
+ command; the consistency test keeps guarding the result.
7
+
8
+ Usage:
9
+ python scripts/bump_version.py 4.0.0
10
+ python scripts/bump_version.py 4.0.0 --check # verify only, no writes
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import json
17
+ import re
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ REPO = Path(__file__).resolve().parents[1]
22
+
23
+ # (path, kind, pattern) — pattern groups: (prefix, version)
24
+ TARGETS = [
25
+ ("latticeai/__init__.py", "regex", r'(__version__ = ")([^"]+)(")'),
26
+ ("latticeai/core/workspace_os.py", "regex", r'(WORKSPACE_OS_VERSION = ")([^"]+)(")'),
27
+ ("latticeai/core/marketplace.py", "regex", r'(MARKETPLACE_VERSION = ")([^"]+)(")'),
28
+ ("latticeai/core/multi_agent.py", "regex", r'(MULTI_AGENT_VERSION = ")([^"]+)(")'),
29
+ ("pyproject.toml", "regex", r'(^version = ")([^"]+)(")'),
30
+ ("package.json", "json", "version"),
31
+ ("package-lock.json", "package-lock", None),
32
+ ("vscode-extension/package.json", "json", "version"),
33
+ ("vscode-extension/package-lock.json", "package-lock", None),
34
+ ("static/v3/asset-manifest.json", "json", "version"),
35
+ ]
36
+
37
+
38
+ def bump(version: str, *, check: bool = False) -> int:
39
+ if not re.fullmatch(r"\d+\.\d+\.\d+([-.][0-9A-Za-z.]+)?", version):
40
+ print(f"error: '{version}' is not a sane semantic version", file=sys.stderr)
41
+ return 2
42
+ failures = 0
43
+ for rel, kind, spec in TARGETS:
44
+ path = REPO / rel
45
+ if not path.exists():
46
+ print(f" skip {rel} (missing)")
47
+ continue
48
+ text = path.read_text(encoding="utf-8")
49
+ if kind == "regex":
50
+ new, n = re.subn(spec, lambda m: m.group(1) + version + m.group(3), text, count=1, flags=re.MULTILINE)
51
+ changed = n == 1 and new != text
52
+ ok = n == 1
53
+ elif kind == "json":
54
+ data = json.loads(text)
55
+ ok = spec in data
56
+ changed = ok and data.get(spec) != version
57
+ if ok:
58
+ data[spec] = version
59
+ new = json.dumps(data, ensure_ascii=False, indent=2) + "\n"
60
+ elif kind == "package-lock":
61
+ data = json.loads(text)
62
+ ok = "version" in data
63
+ changed = ok and data.get("version") != version
64
+ if ok:
65
+ data["version"] = version
66
+ if "packages" in data and "" in data["packages"]:
67
+ data["packages"][""]["version"] = version
68
+ new = json.dumps(data, ensure_ascii=False, indent=2) + "\n"
69
+ else: # pragma: no cover
70
+ raise AssertionError(kind)
71
+ if not ok:
72
+ print(f" FAIL {rel}: version field not found")
73
+ failures += 1
74
+ continue
75
+ if check:
76
+ current = re.search(spec, text, flags=re.MULTILINE).group(2) if kind == "regex" else json.loads(text)["version"]
77
+ status = "ok" if current == version else f"MISMATCH ({current})"
78
+ if current != version:
79
+ failures += 1
80
+ print(f" {status:>9} {rel}")
81
+ continue
82
+ if changed:
83
+ path.write_text(new, encoding="utf-8")
84
+ print(f" bumped {rel}")
85
+ else:
86
+ print(f" ok {rel}")
87
+ return 1 if failures else 0
88
+
89
+
90
+ def main() -> int:
91
+ parser = argparse.ArgumentParser(description=__doc__)
92
+ parser.add_argument("version")
93
+ parser.add_argument("--check", action="store_true", help="verify only; write nothing")
94
+ args = parser.parse_args()
95
+ return bump(args.version, check=args.check)
96
+
97
+
98
+ if __name__ == "__main__":
99
+ raise SystemExit(main())
@@ -17,7 +17,6 @@ the hero GIF is assembled from via ffmpeg by the caller).
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
- import os
21
20
  import sys
22
21
  from pathlib import Path
23
22
 
@@ -1,33 +1,120 @@
1
1
  #!/usr/bin/env node
2
- /* Syntax-check every Lattice AI v3 frontend ES module (node --check, ESM auto-detect).
3
- * Exits non-zero on the first failure so it can gate `npm run lint`. */
4
- import { readdirSync, statSync } from "node:fs";
5
- import { join, dirname } from "node:path";
2
+ /* Lattice v3 frontend lint. Gates `npm run lint`:
3
+ * 1. Syntax-check every v3 ES module (node --check).
4
+ * 2. Design tokens: no raw hex/rgb colors in static/v3/css outside the two
5
+ * token files (lattice.tokens.css, tokens.css) themed surfaces must use
6
+ * var(--…) tokens.
7
+ * 3. No inline style colors in view JS (style="…color: #…" or
8
+ * style.color = "#…" literals).
9
+ * 4. Privacy: zero CDN/external URLs in shipped static HTML/CSS/JS —
10
+ * fonts/icons/libs are vendored under static/vendor.
11
+ * 5. i18n: routes, shell, and new v4 parity views must use the SPA i18n
12
+ * runtime instead of inert localStorage-only language toggles.
13
+ * Exits non-zero on any failure. */
14
+ import { readdirSync, statSync, readFileSync } from "node:fs";
15
+ import { join, dirname, relative } from "node:path";
6
16
  import { fileURLToPath } from "node:url";
7
17
  import { spawnSync } from "node:child_process";
8
18
 
9
- const root = join(dirname(fileURLToPath(import.meta.url)), "..", "static", "v3", "js");
19
+ const repo = join(dirname(fileURLToPath(import.meta.url)), "..");
20
+ const v3js = join(repo, "static", "v3", "js");
21
+ const v3css = join(repo, "static", "v3", "css");
22
+ const staticRoot = join(repo, "static");
10
23
 
11
- function walk(dir) {
24
+ function walk(dir, ext) {
12
25
  const out = [];
13
26
  for (const name of readdirSync(dir)) {
14
27
  const p = join(dir, name);
15
- if (statSync(p).isDirectory()) out.push(...walk(p));
16
- else if (name.endsWith(".js")) out.push(p);
28
+ if (statSync(p).isDirectory()) out.push(...walk(p, ext));
29
+ else if (ext.some((e) => name.endsWith(e))) out.push(p);
17
30
  }
18
31
  return out;
19
32
  }
20
33
 
21
- const files = walk(root).sort();
22
34
  let failed = 0;
23
- for (const file of files) {
35
+ const fail = (msg) => { failed++; console.error(`FAIL ${msg}`); };
36
+
37
+ // ── 1. syntax ────────────────────────────────────────────────────────────
38
+ const modules = walk(v3js, [".js"]).sort();
39
+ let syntaxOk = 0;
40
+ for (const file of modules) {
24
41
  const r = spawnSync(process.execPath, ["--check", file], { encoding: "utf8" });
25
- if (r.status === 0) {
26
- console.log(`ok ${file.replace(root, "static/v3/js")}`);
27
- } else {
28
- failed++;
29
- console.error(`FAIL ${file.replace(root, "static/v3/js")}\n${r.stderr || r.stdout}`);
30
- }
42
+ if (r.status === 0) syntaxOk++;
43
+ else fail(`${relative(repo, file)}\n${r.stderr || r.stdout}`);
44
+ }
45
+ console.log(`syntax: ${syntaxOk}/${modules.length} v3 modules pass`);
46
+
47
+ // ── 2. raw colors in v3 css (outside token files) ────────────────────────
48
+ const TOKEN_FILES = new Set(["lattice.tokens.css", "tokens.css"]);
49
+ const colorRe = /#[0-9a-fA-F]{3,8}\b|rgba?\(/;
50
+ let cssChecked = 0;
51
+ for (const file of walk(v3css, [".css"]).sort()) {
52
+ const base = file.split("/").pop();
53
+ if (TOKEN_FILES.has(base) || /\.[0-9a-f]{8}\.css$/.test(base)) continue; // tokens + hashed builds
54
+ cssChecked++;
55
+ const lines = readFileSync(file, "utf8").split("\n");
56
+ lines.forEach((line, i) => {
57
+ const code = line.split("/*")[0];
58
+ // mask-image gradients use #000/transparent as ALPHA values, not themed
59
+ // colors — they are theme-independent and exempt.
60
+ if (/mask-image|-webkit-mask/.test(code)) return;
61
+ if (colorRe.test(code)) fail(`${relative(repo, file)}:${i + 1} raw color (use a var(--…) token): ${line.trim().slice(0, 90)}`);
62
+ });
63
+ }
64
+ console.log(`tokens: ${cssChecked} non-token v3 css files scanned for raw colors`);
65
+
66
+ // ── 3. inline style colors in view JS ────────────────────────────────────
67
+ const inlineColorRe = /style\s*=\s*["'`][^"'`]*(?:color|background)\s*:\s*(#|rgb)/i;
68
+ const styleAssignRe = /\.style\.(color|background(?:Color)?)\s*=\s*["'`](#|rgb)/i;
69
+ let jsChecked = 0;
70
+ for (const file of modules) {
71
+ if (/\.[0-9a-f]{8}\.js$/.test(file)) continue; // hashed builds mirror sources
72
+ jsChecked++;
73
+ const lines = readFileSync(file, "utf8").split("\n");
74
+ lines.forEach((line, i) => {
75
+ if (inlineColorRe.test(line) || styleAssignRe.test(line)) {
76
+ fail(`${relative(repo, file)}:${i + 1} inline style color (use a token/class): ${line.trim().slice(0, 90)}`);
77
+ }
78
+ });
79
+ }
80
+ console.log(`inline-style: ${jsChecked} v3 source modules scanned`);
81
+
82
+ // ── 4. no CDN/external asset URLs in shipped static files ────────────────
83
+ const cdnRe = /https?:\/\/(fonts\.googleapis\.com|fonts\.gstatic\.com|cdn\.jsdelivr\.net|unpkg\.com|cdnjs\.cloudflare\.com)/;
84
+ let shippedChecked = 0;
85
+ for (const file of walk(staticRoot, [".html", ".css", ".js"]).sort()) {
86
+ if (file.includes(`${join("static", "vendor")}`)) continue; // vendored copies may cite origins in comments
87
+ shippedChecked++;
88
+ const lines = readFileSync(file, "utf8").split("\n");
89
+ lines.forEach((line, i) => {
90
+ if (cdnRe.test(line)) fail(`${relative(repo, file)}:${i + 1} CDN reference (vendor it under static/vendor): ${line.trim().slice(0, 90)}`);
91
+ });
92
+ }
93
+ console.log(`privacy: ${shippedChecked} shipped static files scanned for CDN URLs`);
94
+
95
+ // ── 5. i18n acceptance for T9 surfaces ──────────────────────────────────
96
+ const i18nRequired = [
97
+ "static/v3/js/core/routes.js",
98
+ "static/v3/js/core/shell.js",
99
+ "static/v3/js/views/account.js",
100
+ "static/v3/js/views/workspace-admin.js",
101
+ "static/v3/js/views/snapshots.js",
102
+ "static/v3/js/views/activity.js",
103
+ "static/v3/js/views/runs.js",
104
+ "static/v3/js/views/network.js",
105
+ ];
106
+ let i18nChecked = 0;
107
+ for (const rel of i18nRequired) {
108
+ const file = join(repo, rel);
109
+ const text = readFileSync(file, "utf8");
110
+ i18nChecked++;
111
+ if (!/i18n\.js/.test(text)) fail(`${rel}: missing i18n runtime import`);
112
+ if (!/\bt\(/.test(text)) fail(`${rel}: no translation lookup found`);
113
+ }
114
+ console.log(`i18n: ${i18nChecked} route/shell/parity modules checked`);
115
+
116
+ if (failed) {
117
+ console.error(`\nv3 frontend lint: ${failed} failure(s)`);
118
+ process.exit(1);
31
119
  }
32
- console.log(`\nv3 frontend: ${files.length - failed}/${files.length} modules pass`);
33
- process.exit(failed ? 1 : 0);
120
+ console.log(`\nv3 frontend lint: all checks pass`);
@@ -20,7 +20,6 @@ from __future__ import annotations
20
20
  import argparse
21
21
  import json
22
22
  import re
23
- import sys
24
23
  import zipfile
25
24
  from pathlib import Path
26
25
  from typing import Dict, List, Optional
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env python3
2
+ """Installed-wheel smoke test.
3
+
4
+ Builds the wheel, installs it into a *fresh* venv, and verifies — from a
5
+ non-repo working directory — that every module the wheel ships actually
6
+ imports and that the FastAPI app boots and answers ``/health``. This kills the
7
+ class of "works only with ``pip install -e .`` from the repo root" failures
8
+ (e.g. the v3.x wheels that omitted the root ``setup.py`` wizard module while
9
+ ``latticeai.server_app`` imported it).
10
+
11
+ Usage:
12
+ python scripts/wheel_smoke.py # build + install + import + /health
13
+ python scripts/wheel_smoke.py --wheel dist/ltcai-X.Y.Z-py3-none-any.whl
14
+ python scripts/wheel_smoke.py --skip-health # imports only
15
+
16
+ The build step prefers ``python -m build --wheel`` and falls back to
17
+ ``pip wheel . --no-deps`` when the ``build`` package is unavailable.
18
+ Exit code is non-zero on any failure so CI can gate on it.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import os
25
+ import subprocess
26
+ import sys
27
+ import tempfile
28
+ import venv
29
+ from pathlib import Path
30
+
31
+ REPO_ROOT = Path(__file__).resolve().parents[1]
32
+
33
+ # Every importable module the wheel ships (pyproject py-modules + packages).
34
+ WHEEL_MODULES = [
35
+ "setup_wizard",
36
+ "latticeai",
37
+ "latticeai.server_app",
38
+ "latticeai.app_factory",
39
+ "latticeai.models.router",
40
+ "latticeai.core.mcp_registry",
41
+ "latticeai.api.knowledge_graph",
42
+ "ltcai_cli",
43
+ "auto_setup",
44
+ "server",
45
+ "mcp_registry",
46
+ "kg_schema",
47
+ "knowledge_graph",
48
+ "knowledge_graph_api",
49
+ "local_knowledge_api",
50
+ "llm_router",
51
+ "p_reinforce",
52
+ "telegram_bot",
53
+ "tools",
54
+ ]
55
+
56
+ IMPORT_CHECK = (
57
+ "import importlib\n"
58
+ + "".join(f"importlib.import_module({mod!r})\n" for mod in WHEEL_MODULES)
59
+ + f"print('wheel imports ok: {len(WHEEL_MODULES)} modules')\n"
60
+ )
61
+
62
+ HEALTH_CHECK = """
63
+ from fastapi.testclient import TestClient
64
+ from latticeai.app_factory import create_app
65
+
66
+ app = create_app()
67
+ response = TestClient(app).get("/health")
68
+ assert response.status_code == 200, f"/health returned {response.status_code}"
69
+ payload = response.json()
70
+ assert "version" in payload, f"/health payload missing version: {payload}"
71
+ print("health ok:", payload.get("version"))
72
+ """
73
+
74
+
75
+ def run(cmd: list[str], **kwargs) -> None:
76
+ print("+", " ".join(str(c) for c in cmd), flush=True)
77
+ subprocess.run(cmd, check=True, **kwargs)
78
+
79
+
80
+ def build_wheel(out_dir: Path) -> Path:
81
+ try:
82
+ import build # noqa: F401 — probe only
83
+ run([sys.executable, "-m", "build", "--wheel", "--outdir", str(out_dir)], cwd=REPO_ROOT)
84
+ except ImportError:
85
+ print("'build' package unavailable; falling back to pip wheel --no-deps", flush=True)
86
+ run(
87
+ [sys.executable, "-m", "pip", "wheel", str(REPO_ROOT), "--no-deps", "-w", str(out_dir)],
88
+ cwd=REPO_ROOT,
89
+ )
90
+ wheels = sorted(out_dir.glob("ltcai-*.whl"))
91
+ if not wheels:
92
+ raise SystemExit(f"no wheel produced in {out_dir}")
93
+ return wheels[-1]
94
+
95
+
96
+ def main() -> int:
97
+ parser = argparse.ArgumentParser(description=__doc__)
98
+ parser.add_argument("--wheel", type=Path, default=None, help="use an existing wheel instead of building")
99
+ parser.add_argument("--skip-health", action="store_true", help="run import checks only (no app boot)")
100
+ args = parser.parse_args()
101
+
102
+ with tempfile.TemporaryDirectory(prefix="ltcai-wheel-smoke-") as tmp:
103
+ tmp_path = Path(tmp)
104
+
105
+ wheel = args.wheel.resolve() if args.wheel else build_wheel(tmp_path / "dist")
106
+ print(f"wheel under test: {wheel}", flush=True)
107
+
108
+ venv_dir = tmp_path / "venv"
109
+ venv.EnvBuilder(with_pip=True, clear=True).create(venv_dir)
110
+ bin_dir = "Scripts" if os.name == "nt" else "bin"
111
+ python = venv_dir / bin_dir / ("python.exe" if os.name == "nt" else "python")
112
+
113
+ run([str(python), "-m", "pip", "install", "--quiet", str(wheel)])
114
+
115
+ # A cwd that is NOT the repo: imports must resolve from site-packages,
116
+ # never from checkout-relative files.
117
+ work_dir = tmp_path / "non-repo-cwd"
118
+ work_dir.mkdir()
119
+ # Sandbox all user-data writes so the smoke test never touches ~/.ltcai.
120
+ env = {
121
+ **os.environ,
122
+ "HOME": str(tmp_path / "home"),
123
+ "LATTICEAI_DATA_DIR": str(tmp_path / "home" / ".ltcai"),
124
+ "LATTICEAI_AGENT_ROOT": str(tmp_path / "home" / "agent_workspace"),
125
+ "LATTICEAI_BRAIN_DIR": str(tmp_path / "home" / ".ltcai-brain"),
126
+ "LATTICEAI_ENABLE_TELEGRAM": "false",
127
+ "LATTICEAI_AUTOLOAD_MODELS": "false",
128
+ "PYTHONPATH": "",
129
+ }
130
+ (tmp_path / "home").mkdir()
131
+
132
+ run([str(python), "-c", IMPORT_CHECK], cwd=work_dir, env=env)
133
+
134
+ if not args.skip_health:
135
+ run([str(python), "-c", HEALTH_CHECK], cwd=work_dir, env=env)
136
+
137
+ print("wheel smoke test passed")
138
+ return 0
139
+
140
+
141
+ if __name__ == "__main__":
142
+ raise SystemExit(main())