ltcai 3.5.0 → 4.0.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 (181) hide show
  1. package/README.md +73 -35
  2. package/docs/CARRYOVER_AUDIT_v3.6.0.md +61 -0
  3. package/docs/CHANGELOG.md +32 -0
  4. package/docs/HANDOVER_v3.6.0.md +46 -0
  5. package/docs/RUNTIME_HOOK_COVERAGE_v3.6.0.md +49 -0
  6. package/docs/V4_BRAIN_ARCHITECTURE.md +322 -0
  7. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +509 -0
  8. package/docs/V4_IMPLEMENTATION_PLAN.md +470 -0
  9. package/docs/architecture.md +13 -12
  10. package/docs/kg-schema.md +102 -53
  11. package/docs/privacy.md +18 -2
  12. package/docs/security-model.md +17 -0
  13. package/kg_schema.py +139 -10
  14. package/knowledge_graph.py +874 -26
  15. package/knowledge_graph_api.py +11 -127
  16. package/latticeai/__init__.py +1 -1
  17. package/latticeai/api/admin.py +1 -1
  18. package/latticeai/api/agents.py +7 -1
  19. package/latticeai/api/auth.py +27 -4
  20. package/latticeai/api/browser.py +217 -0
  21. package/latticeai/api/chat.py +112 -76
  22. package/latticeai/api/health.py +1 -1
  23. package/latticeai/api/hooks.py +1 -1
  24. package/latticeai/api/knowledge_graph.py +146 -0
  25. package/latticeai/api/local_files.py +1 -1
  26. package/latticeai/api/mcp.py +23 -11
  27. package/latticeai/api/memory.py +1 -1
  28. package/latticeai/api/models.py +1 -1
  29. package/latticeai/api/network.py +81 -0
  30. package/latticeai/api/portability.py +93 -0
  31. package/latticeai/api/realtime.py +1 -1
  32. package/latticeai/api/search.py +26 -2
  33. package/latticeai/api/security_dashboard.py +2 -3
  34. package/latticeai/api/setup.py +2 -2
  35. package/latticeai/api/static_routes.py +2 -4
  36. package/latticeai/api/tools.py +3 -0
  37. package/latticeai/api/workflow_designer.py +46 -0
  38. package/latticeai/api/workspace.py +71 -49
  39. package/latticeai/app_factory.py +1710 -0
  40. package/latticeai/brain/__init__.py +18 -0
  41. package/latticeai/brain/context.py +213 -0
  42. package/latticeai/brain/conversations.py +236 -0
  43. package/latticeai/brain/identity.py +175 -0
  44. package/latticeai/brain/memory.py +102 -0
  45. package/latticeai/brain/network.py +205 -0
  46. package/latticeai/core/agent.py +31 -7
  47. package/latticeai/core/audit.py +0 -7
  48. package/latticeai/core/config.py +1 -1
  49. package/latticeai/core/context_builder.py +1 -2
  50. package/latticeai/core/enterprise.py +1 -1
  51. package/latticeai/core/graph_curator.py +2 -2
  52. package/latticeai/core/marketplace.py +1 -1
  53. package/latticeai/core/mcp_registry.py +791 -0
  54. package/latticeai/core/model_compat.py +1 -1
  55. package/latticeai/core/model_resolution.py +0 -1
  56. package/latticeai/core/multi_agent.py +238 -4
  57. package/latticeai/core/security.py +1 -1
  58. package/latticeai/core/sessions.py +37 -7
  59. package/latticeai/core/workflow_engine.py +114 -2
  60. package/latticeai/core/workspace_os.py +58 -10
  61. package/latticeai/models/__init__.py +7 -0
  62. package/latticeai/models/router.py +779 -0
  63. package/latticeai/server_app.py +29 -1504
  64. package/latticeai/services/agent_runtime.py +1 -0
  65. package/latticeai/services/app_context.py +75 -14
  66. package/latticeai/services/ingestion.py +318 -0
  67. package/latticeai/services/kg_portability.py +207 -0
  68. package/latticeai/services/memory_service.py +39 -11
  69. package/latticeai/services/model_runtime.py +2 -5
  70. package/latticeai/services/platform_runtime.py +100 -23
  71. package/latticeai/services/search_service.py +17 -8
  72. package/latticeai/services/tool_dispatch.py +12 -2
  73. package/latticeai/services/triggers.py +241 -0
  74. package/latticeai/services/upload_service.py +37 -12
  75. package/latticeai/services/workspace_service.py +31 -0
  76. package/llm_router.py +29 -772
  77. package/ltcai_cli.py +1 -2
  78. package/mcp_registry.py +25 -788
  79. package/p_reinforce.py +124 -14
  80. package/package.json +11 -8
  81. package/scripts/build_vsix.mjs +72 -0
  82. package/scripts/bump_version.py +99 -0
  83. package/scripts/generate_diagrams.py +0 -1
  84. package/scripts/lint_v3.mjs +82 -18
  85. package/scripts/validate_release_artifacts.py +0 -1
  86. package/scripts/wheel_smoke.py +142 -0
  87. package/server.py +11 -7
  88. package/setup_wizard.py +1142 -0
  89. package/static/account.html +2 -4
  90. package/static/admin.html +3 -5
  91. package/static/chat.html +3 -6
  92. package/static/graph.html +2 -4
  93. package/static/sw.js +81 -52
  94. package/static/v3/asset-manifest.json +20 -19
  95. package/static/v3/css/{lattice.base.e4cdd05d.css → lattice.base.49deefb5.css} +1 -1
  96. package/static/v3/css/lattice.base.css +1 -1
  97. package/static/v3/css/{lattice.components.9b49d614.css → lattice.components.cde18231.css} +1 -1
  98. package/static/v3/css/lattice.components.css +1 -1
  99. package/static/v3/css/{lattice.shell.8fcc9d33.css → lattice.shell.29d36d85.css} +1 -1
  100. package/static/v3/css/lattice.shell.css +1 -1
  101. package/static/v3/css/{lattice.tokens.e7018963.css → lattice.tokens.304cbc40.css} +3 -0
  102. package/static/v3/css/lattice.tokens.css +3 -0
  103. package/static/v3/css/{lattice.views.22f69117.css → lattice.views.0a18b6c5.css} +2 -2
  104. package/static/v3/css/lattice.views.css +2 -2
  105. package/static/v3/index.html +3 -4
  106. package/static/v3/js/{app.d086489d.js → app.356e6452.js} +1 -1
  107. package/static/v3/js/core/{api.12b568ad.js → api.7a308b89.js} +39 -1
  108. package/static/v3/js/core/api.js +38 -0
  109. package/static/v3/js/core/{routes.d214b399.js → routes.7222343d.js} +22 -22
  110. package/static/v3/js/core/routes.js +22 -22
  111. package/static/v3/js/core/{shell.d05266f5.js → shell.a1657f20.js} +4 -4
  112. package/static/v3/js/core/shell.js +1 -1
  113. package/static/v3/js/core/{store.34ebd5e6.js → store.204a08b2.js} +1 -1
  114. package/static/v3/js/core/store.js +1 -1
  115. package/static/v3/js/views/graph-canvas.17c15d65.js +509 -0
  116. package/static/v3/js/views/graph-canvas.js +509 -0
  117. package/static/v3/js/views/{hybrid-search.b22b97e0.js → hybrid-search.2fb63ed9.js} +1 -2
  118. package/static/v3/js/views/hybrid-search.js +1 -2
  119. package/static/v3/js/views/knowledge-graph.5e40cbeb.js +509 -0
  120. package/static/v3/js/views/knowledge-graph.js +326 -54
  121. package/static/vendor/chart.umd.min.js +20 -0
  122. package/static/vendor/fonts/inter-latin-300-normal.woff2 +0 -0
  123. package/static/vendor/fonts/inter-latin-400-normal.woff2 +0 -0
  124. package/static/vendor/fonts/inter-latin-500-normal.woff2 +0 -0
  125. package/static/vendor/fonts/inter-latin-600-normal.woff2 +0 -0
  126. package/static/vendor/fonts/inter-latin-700-normal.woff2 +0 -0
  127. package/static/vendor/fonts/inter-latin-800-normal.woff2 +0 -0
  128. package/static/vendor/fonts/inter.css +44 -0
  129. package/static/vendor/icons/tabler-icons.min.css +4 -0
  130. package/static/vendor/icons/tabler-icons.woff2 +0 -0
  131. package/static/vendor/marked.min.js +69 -0
  132. package/static/workspace.html +2 -2
  133. package/telegram_bot.py +1 -2
  134. package/tools/commands.py +4 -2
  135. package/tools/computer.py +1 -1
  136. package/tools/documents.py +1 -3
  137. package/tools/filesystem.py +0 -4
  138. package/tools/knowledge.py +1 -3
  139. package/tools/network.py +1 -3
  140. package/codex_telegram_bot.py +0 -195
  141. package/docs/assets/v3.4.0/agent-run.png +0 -0
  142. package/docs/assets/v3.4.0/agents.png +0 -0
  143. package/docs/assets/v3.4.0/before/chat-before.png +0 -0
  144. package/docs/assets/v3.4.0/before/files-before.png +0 -0
  145. package/docs/assets/v3.4.0/chat.png +0 -0
  146. package/docs/assets/v3.4.0/connect-folder.png +0 -0
  147. package/docs/assets/v3.4.0/files.png +0 -0
  148. package/docs/assets/v3.4.0/home.png +0 -0
  149. package/docs/assets/v3.4.0/hooks-dispatch.png +0 -0
  150. package/docs/assets/v3.4.0/knowledge-graph.png +0 -0
  151. package/docs/assets/v3.4.0/local-agent.png +0 -0
  152. package/docs/assets/v3.4.0/memory.png +0 -0
  153. package/docs/assets/v3.4.0/settings.png +0 -0
  154. package/docs/assets/v3.4.0/vision-input.png +0 -0
  155. package/docs/assets/v3.4.0/workflows.png +0 -0
  156. package/docs/assets/v3.4.1/e2e_runtime_log.txt +0 -42
  157. package/docs/assets/v3.4.1/hooks-dispatch.png +0 -0
  158. package/docs/assets/v3.4.1/local-agent.png +0 -0
  159. package/docs/images/admin-dashboard.png +0 -0
  160. package/docs/images/architecture.png +0 -0
  161. package/docs/images/enterprise.png +0 -0
  162. package/docs/images/graph.png +0 -0
  163. package/docs/images/hero.gif +0 -0
  164. package/docs/images/knowledge-graph.png +0 -0
  165. package/docs/images/lattice-ai-demo.gif +0 -0
  166. package/docs/images/lattice-ai-hero.png +0 -0
  167. package/docs/images/logo.svg +0 -33
  168. package/docs/images/mobile-responsive.png +0 -0
  169. package/docs/images/model-recommendation.png +0 -0
  170. package/docs/images/onboarding.png +0 -0
  171. package/docs/images/organization.png +0 -0
  172. package/docs/images/pipeline.png +0 -0
  173. package/docs/images/screenshot-admin.png +0 -0
  174. package/docs/images/screenshot-chat.png +0 -0
  175. package/docs/images/screenshot-graph.png +0 -0
  176. package/docs/images/skills.png +0 -0
  177. package/docs/images/workspace-dark.png +0 -0
  178. package/docs/images/workspace-light.png +0 -0
  179. package/docs/images/workspace.png +0 -0
  180. package/requirements.txt +0 -16
  181. package/static/v3/js/views/knowledge-graph.a14ea7e7.js +0 -237
package/docs/kg-schema.md CHANGED
@@ -78,8 +78,66 @@ Edge {
78
78
  | `VERSION_OF` | `FILE → FILE` | 버전 히스토리 |
79
79
  | `GRANTS_ACCESS` | `PERSON → FILE`·`CONVERSATION`·`PROJECT` | 접근 권한 부여 |
80
80
 
81
- **엔드포인트 룰은 코드에서 강제된다** (`validate_endpoints` in `kg_schema.py`).
82
- 잘못된 페어(예: `FILE FILE` 에 `REPLIES_TO`)`upsert_edge` 거부한다.
81
+ **엔드포인트 룰은 권고 사항이다 (스키마 문서 기준).** 코드에는 엔드포인트 페어
82
+ 검증기가 존재하지 않는다 `validate_endpoints`구현된 적이 없으며, 쓰기
83
+ 경로는 타입 페어를 거부하지 않는다. v4 의 쓰기 정규화는 *타입 어휘* 를
84
+ 강제한다: `_upsert_edge` 가 모든 엣지 타입을 canonical `EdgeType` 값으로
85
+ 정규화하므로 자유 문자열 타입은 더 이상 생성되지 않는다.
86
+
87
+ ---
88
+
89
+ ## v3.6.0 — Knowledge Graph First 엔티티/관계
90
+
91
+ v3.6.0 은 "모든 데이터 소스가 Knowledge Graph 로 수렴한다"는 원칙을 1급 스키마로
92
+ 승격한다. 아래 타입은 **추가형(additive)**이다 — 기존 enum/legacy 매핑을 깨지 않고
93
+ `from_legacy` 가 무손실로 정규화하며, 알 수 없는 타입은 여전히 `CONCEPT`/`MENTIONS` 로
94
+ 폴백한다. 스키마는 **확장 가능**하게 유지한다: 새 도메인 엔티티는 enum 멤버 1개 +
95
+ `_LEGACY_NODE_MAP`/`_LEGACY_EDGE_MAP` 별칭만 추가하면 된다.
96
+
97
+ ### 추가 노드 타입
98
+
99
+ | 타입 | 의미 | 대표 `attrs` / 출처 |
100
+ |------|------|--------------------|
101
+ | `SOURCE` | 수집 출처(파일/URL/브라우저 탭/git 등)의 **출처 노드** | `source_type`, `source_uri`, `content_hash`, `captured_at` |
102
+ | `REPOSITORY` | git 저장소 | `remote`, `branch`, `head` |
103
+ | `MEETING` | 회의 / 미팅 | `started_at`, `attendees[]` |
104
+ | `ORGANIZATION` | 조직 / 회사 / 팀 | `domain`, `members[]` |
105
+ | `WORKFLOW` | 워크플로우 정의/실행 | `workflow_id`, `status` |
106
+ | `AGENT` | 에이전트(역할/실행 주체) | `role`, `model_id` |
107
+
108
+ ### 추가 엣지 타입
109
+
110
+ | 타입 | 허용 source → target | 의미 |
111
+ |------|---------------------|------|
112
+ | `INDEXED_FROM` | ANY → `SOURCE` | 이 노드가 **어떤 출처에서 색인**됐는가 (provenance) |
113
+ | `MODIFIED_BY` | ANY → `PERSON` | 마지막 수정자 |
114
+ | `BELONGS_TO_PROJECT` | ANY → `PROJECT` | 프로젝트 귀속 |
115
+ | `PART_OF` | ANY → ANY | 구성요소 관계 |
116
+ | `DISCUSSED_IN` | `CONCEPT`·`DECISION` → `MEETING`·`CHAT` | 어디에서 논의됨 |
117
+ | `DECIDED_BY` | `DECISION` → `PERSON` | 결정 주체 |
118
+ | `GENERATED_BY` | ANY → `AGENT`·`MODEL`·`WORKFLOW` | 생성 주체 |
119
+ | `USED_BY_AGENT` | ANY → `AGENT` | 에이전트가 사용함 |
120
+
121
+ ### 통합 수집 형태 (Unified Ingestion)
122
+
123
+ 모든 출처는 동일한 형태로 그래프에 들어온다:
124
+
125
+ ```
126
+ SOURCE ──INDEXED_FROM◄── Document/File ──CONTAINS──► Chunk[]
127
+ ▲ │
128
+ │ └──(언급/포함)──► Concept / Task / Decision …
129
+ provenance(source_type, source_uri, content_hash, captured_at, modified_at,
130
+ owner, workspace_id, permissions, pipeline, embedded, linked)
131
+ ```
132
+
133
+ - **콘텐츠 노드**(Document/File/web 노드)는 `content_hash` 로 멱등(idempotent) 처리된다 —
134
+ 같은 콘텐츠를 다시 수집하면 새 노드를 만들지 않고 갱신/링크한다.
135
+ - 모든 콘텐츠 노드는 `SOURCE` 노드로 `INDEXED_FROM` 엣지를 가져 **출처를 항상 설명 가능**하다.
136
+ - provenance 는 노드 `metadata.provenance` 에 임베드되며, 동시에 감사 가능한
137
+ `ingestion_provenance` 테이블에 기록된다 (`KnowledgeGraphStore.get_provenance(node_id)`).
138
+
139
+ 구현: `latticeai/services/ingestion.py` (`IngestionPipeline`) 가 단일 진입점이며,
140
+ 파일/로컬폴더/URL/브라우저 탭/텍스트를 모두 이 형태로 정규화한다.
83
141
 
84
142
  ---
85
143
 
@@ -143,20 +201,12 @@ Edge {
143
201
 
144
202
  ### 실행
145
203
 
146
- ```bash
147
- # 1) 현재 DB 어떤 row 가 어떻게 변환될지만 보기
148
- python3 kg_schema.py migrate ~/.ltcai/knowledge_graph.db --dry-run
149
-
150
- # 2) 실제 마이그레이션 (v2 테이블에 복사. 기존 테이블은 보존)
151
- python3 kg_schema.py migrate ~/.ltcai/knowledge_graph.db
152
-
153
- # 3) 결과 확인
154
- python3 kg_schema.py stats ~/.ltcai/knowledge_graph.db
155
- ```
156
-
157
- 마이그레이션은 **기존 `nodes` / `edges` 를 건드리지 않는다.** 신규 `nodes_v2` / `edges_v2`
158
- 테이블에 복사할 뿐이다. 새 코드가 안정화되면 다음 메이저 릴리스에서 legacy 테이블을
159
- DROP 한다.
204
+ 마이그레이션은 별도 CLI 없이 **서버 기동 시 자동으로** 일어난다:
205
+ `knowledge_graph.KnowledgeGraphStore` 열릴 v2 스키마를 생성/치유하고
206
+ (`kg_schema.KGStoreV2.init_schema` 추가 컬럼은 `ALTER` 로 in-place 치유,
207
+ edges_v2 식별자 변경은 create→copy→swap 으로 재구축), legacy 데이터를
208
+ v2 백필한다. 기존 `nodes` / `edges` 테이블은 건드리지 않는다 — v4 의
209
+ write-mastering 전환(T3d) 전까지 legacy 가 쓰기 마스터다.
160
210
 
161
211
  ---
162
212
 
@@ -164,47 +214,46 @@ DROP 한다.
164
214
 
165
215
  - 차원: 환경 변수 `LATTICEAI_EMBED_DIM` (기본 `1024`)
166
216
  - 저장: SQLite `BLOB` 컬럼, `struct.pack('<{n}f', …)` 직렬화
167
- - 검색: `KGStoreV2.search_similar(vec, top_k=8)` — sqlite-vec 가 없는 환경에서도
168
- 순수 Python 코사인으로 동작. sqlite-vec 설치되면 인덱스 자동 활용 (추후).
169
-
170
- 임베딩 모델은 LLM 라우터(`llm_router.py`) 가 결정한다 — 기본 `sentence-transformers/all-MiniLM-L12-v2`
171
- (384-d, dim 변경시 `LATTICEAI_EMBED_DIM` 함께 설정).
217
+ - 검색: `knowledge_graph.KnowledgeGraphStore.vector_search` — 순수 Python
218
+ 코사인 (sqlite-vec/ANN 인덱스는 아직 없음). 기본 임베더는 해시 기반
219
+ 폴백(`grade='fallback'`)이며, 실제 임베딩 모델은 setup wizard 를 통해
220
+ 사용자 동의 하에 프로비저닝한다.
221
+ - 키워드 검색: v4 부터 FTS5 trigram 인덱스(`node_fts`) LIKE 스캔을
222
+ 대체한다 (한국어 부분 문자열 리콜 유지). FTS5/trigram 이 없는 SQLite
223
+ 빌드에서는 LIKE 경로가 그대로 동작하며 `index_status().storage.fts_enabled`
224
+ 로 정직하게 보고된다.
172
225
 
173
226
  ---
174
227
 
175
228
  ## 사용 (Python)
176
229
 
230
+ `KGStoreV2` 는 **스키마/초기화/통계 전용**이다 — 과거 문서에 있던
231
+ `Node`/`Edge` dataclass, `upsert_node`/`upsert_edge`/`neighbors`/
232
+ `search_similar` native API 는 제거되었고 존재하지 않는다. 데이터
233
+ read/write 는 `knowledge_graph.KnowledgeGraphStore` 가 담당한다:
234
+
177
235
  ```python
178
- from kg_schema import KGStoreV2, Node, Edge, NodeType, EdgeType, Visibility
179
-
180
- store = KGStoreV2("/Users/me/.ltcai/kg_v2.db")
181
- store.init_schema()
182
-
183
- # 노드 만들기
184
- file_node = Node(
185
- type=NodeType.FILE,
186
- label="LatticeAI_기획서.pdf",
187
- attrs={"mime": "application/pdf", "pageCount": 24, "lang": "ko"},
188
- owner_id="user_seoljun",
189
- visibility=Visibility.PRIVATE,
190
- )
191
- store.upsert_node(file_node)
192
-
193
- # 관계 만들기
194
- store.upsert_edge(Edge(
195
- source=file_node.id,
196
- target=concept_node.id,
197
- type=EdgeType.MENTIONS,
198
- weight=0.82, confidence=0.91,
199
- evidence=["chunk:01HX7K…#p3"],
200
- created_by="extractor:llm-gemma-4-12b",
201
- ))
202
-
203
- # 이웃 탐색
204
- for edge, other in store.neighbors(file_node.id, edge_type=EdgeType.MENTIONS):
205
- print(f"-[{edge.type.value}]-> {other.label}")
206
-
207
- # 의미 검색
208
- for node, score in store.search_similar(query_embedding, top_k=8):
209
- print(f"{score:+.3f} {node.type.value:>12} {node.label}")
236
+ from kg_schema import KGStoreV2, NodeType, EdgeType
237
+ from knowledge_graph import KnowledgeGraphStore
238
+
239
+ store = KnowledgeGraphStore(db_path, blob_dir)
240
+
241
+ # 쓰기: 모든 ingest 경로가 내부적으로 _upsert_node/_upsert_edge 를 통과하며,
242
+ # 엣지 타입은 canonical EdgeType 으로 정규화된다 (자유 문자열 차단).
243
+ store.ingest_message("user", "프로젝트 일정 공유", user_email="me@example.com")
244
+
245
+ # 읽기: search (FTS5/LIKE), vector_search, graph, traverse
246
+ matches = store.search("프로젝트")["matches"]
247
+
248
+ # v2 통계 (정규화된 타입 분포)
249
+ print(KGStoreV2(store.db_path).stats())
210
250
  ```
251
+
252
+ ### v4 컬럼 (T3b/T3c)
253
+
254
+ - `nodes_v2.workspace_id` — `NULL` = legacy-global (스코프 도입 이전 데이터)
255
+ - `nodes_v2.visibility` — 신규 스코프 쓰기는 `workspace`/`private`,
256
+ 스코프 없는 쓰기는 `legacy` (기존 공유 데이터를 몰래 private 으로
257
+ 만들지 않는다)
258
+ - `nodes_v2.superseded_by` — 개정 체인 (`mark_superseded`)
259
+ - `edge_occurrences` — 관계의 모든 관측 기록 (observed_at/weight/source)
package/docs/privacy.md CHANGED
@@ -13,8 +13,10 @@
13
13
  | 사용자 계정 | `~/.ltcai/users.json` | 이름, scrypt 해시 비밀번호, 역할 |
14
14
  | 세션 토큰 | `~/.ltcai/sessions.json` | UUID 토큰, 만료시간 |
15
15
  | 채팅 히스토리 | `~/.ltcai/chat_history.json` | 사용자-AI 대화 내용 |
16
- | 지식 그래프 | `~/.ltcai/knowledge_graph.sqlite` | 채팅/문서 노드/엣지 |
17
- | 업로드 파일 | `~/.ltcai/knowledge_graph_blobs/` | 원본 PDF/DOCX 등 |
16
+ | 지식 그래프 | `~/.ltcai/knowledge_graph.sqlite` | 채팅/문서/웹/탭 노드·엣지, 프로비넌스 |
17
+ | 업로드/수집 파일 | `~/.ltcai/knowledge_graph_blobs/` | 원본 PDF/DOCX/웹 텍스트 등 |
18
+ | 수집 출처 기록(프로비넌스) | `~/.ltcai/knowledge_graph.sqlite` (`ingestion_provenance`) | 각 노드의 출처/시각/처리 방식 — 외부 전송 없음 |
19
+ | 그래프 내보내기/백업 | `~/.ltcai/workspace_exports/` | 사용자가 직접 만든 로컬 export/backup 파일 (클라우드 미사용) |
18
20
  | 지식 정원 | `~/.ltcai-brain/` | P-Reinforce 분류 저장 |
19
21
  | 설정 | `~/.ltcai/config.json` | 모델 설정, API 키 (keyring) |
20
22
 
@@ -36,6 +38,20 @@
36
38
 
37
39
  Apple Silicon MLX 로컬 모델 사용 시에는 프롬프트가 외부로 전송되지 않습니다.
38
40
 
41
+ ## 웹/브라우저 수집 (v3.6.0)
42
+
43
+ - **URL 읽기**(`/api/browser/read-url`): 사용자가 명시적으로 요청한 URL을 **로컬
44
+ 런타임이 직접** 가져와 텍스트만 추출해 그래프에 색인합니다. Lattice AI가 임의로
45
+ 크롤링하지 않으며, 가져온 페이지는 외부로 다시 전송되지 않습니다.
46
+ - **브라우저 탭 수집**(`/api/browser/ingest-current-tab`) 및 Manifest V3 확장:
47
+ 확장 프로그램은 **오직 `127.0.0.1`(로컬)** 로만 전송합니다. 클라우드 엔드포인트가
48
+ 존재하지 않습니다(`browser-extension/` 소스에서 단일 `fetch` 대상 확인 가능).
49
+
50
+ ## 그래프 내보내기/백업 (v3.6.0)
51
+
52
+ 지식 그래프 export/import 및 백업/복원은 **전적으로 로컬**에서 동작하며 클라우드
53
+ 서비스를 요구하지 않습니다. 내보낸 파일의 이동·공유는 사용자 책임입니다.
54
+
39
55
  ## API 키 보안
40
56
 
41
57
  - API 키는 OS keyring(macOS Keychain, Windows Credential Manager, Linux Secret Service)에 저장됩니다
@@ -78,6 +78,23 @@ MAGIC_NUMBERS = {
78
78
  - 업로드 시 파일 첫 바이트와 확장자 매핑 검증
79
79
  - 불일치 시 400 에러
80
80
 
81
+ ## 수집 & 그래프 포터빌리티 보안 (v3.6.0)
82
+
83
+ - **수집 라이프사이클**: 모든 수집은 `IngestionPipeline.ingest` → `dispatch_tool`
84
+ 를 거쳐 `pre_tool`/`post_tool` 훅이 발화됩니다. `pre_tool`이 차단하면 수집은
85
+ 정직하게 `status="blocked"`로 거부됩니다(권한 게이트·민감정보 가드 적용).
86
+ - **웹 URL 읽기**(`/api/browser/read-url`): `http(s)` 스킴만 허용(파일/기타 스킴
87
+ 거부), 12초 타임아웃, 4MB 응답 상한, HTML/text 컨텐츠 타입만 처리. 차단/로그인
88
+ 필요 페이지는 5xx가 아닌 **422로 우아하게 실패**합니다. 로컬 런타임이 사용자가
89
+ 명시한 URL만 가져옵니다(자동 크롤링 없음).
90
+ - **브라우저 탭 수집**(`/api/browser/ingest-current-tab`): payload 정화(스크립트/
91
+ 스타일 제거) + 페이로드 크기 상한(413). Manifest V3 확장은 **`127.0.0.1`로만**
92
+ 전송하며 클라우드 엔드포인트가 없습니다.
93
+ - **포터빌리티 권한**: 그래프 export/status 읽기는 로그인 사용자, **import /
94
+ backup / restore 는 admin 전용**(`require_admin`). 그래프는 머신-전역 자원입니다.
95
+ - **복원 무결성**: 백업 아카이브는 `manifest.json`의 sha256과 대조 검증 후에만
96
+ 복원되며, 불일치 시 거부됩니다.
97
+
81
98
  ## 에이전트 도구 샌드박스
82
99
 
83
100
  ### `run_command()` 위험 플래그 차단
package/kg_schema.py CHANGED
@@ -99,6 +99,15 @@ class NodeType(str, Enum):
99
99
  DECISION = "DECISION" # 결정 사항
100
100
  ERROR = "ERROR" # 오류 / 버그
101
101
  EVENT = "EVENT" # 분석/시스템 이벤트(동적 타입 폴백)
102
+ # v3.6.0 Knowledge Graph First — 모든 데이터 소스가 그래프로 수렴하기 위한
103
+ # 1급 엔티티. 추가형(additive)·확장 가능(extensible): 새 도메인 엔티티는
104
+ # 여기에 enum 멤버를 추가하고 _LEGACY_NODE_MAP 에 별칭만 등록하면 된다.
105
+ SOURCE = "SOURCE" # 수집 출처(파일/URL/브라우저 탭/git 등)의 출처 노드
106
+ REPOSITORY = "REPOSITORY" # git 저장소
107
+ MEETING = "MEETING" # 회의 / 미팅
108
+ ORGANIZATION = "ORGANIZATION" # 조직 / 회사 / 팀
109
+ WORKFLOW = "WORKFLOW" # 워크플로우 정의/실행
110
+ AGENT = "AGENT" # 에이전트(역할/실행 주체)
102
111
 
103
112
  @classmethod
104
113
  def from_legacy(cls, label: str) -> "NodeType":
@@ -107,8 +116,14 @@ class NodeType(str, Enum):
107
116
  매핑이 없는(동적 이벤트 등) 타입은 ``CONCEPT`` 로 폴백하지만, 호출부는
108
117
  원본 문자열을 ``legacy_type`` 칼럼에 별도 보존하므로 정보 손실은 없다.
109
118
  """
110
- m = (label or "").strip().lower()
111
- return _LEGACY_NODE_MAP.get(m, cls.CONCEPT)
119
+ m = (label or "").strip()
120
+ # Canonical values round-trip exactly (v4 native writes use them);
121
+ # without this, CODE_FILE/AI_RESPONSE etc. would degrade to CONCEPT.
122
+ try:
123
+ return cls(m.upper())
124
+ except ValueError:
125
+ pass
126
+ return _LEGACY_NODE_MAP.get(m.lower(), cls.CONCEPT)
112
127
 
113
128
 
114
129
  class EdgeType(str, Enum):
@@ -143,6 +158,16 @@ class EdgeType(str, Enum):
143
158
  DISCUSSES = "DISCUSSES" # SLIDE/PAGE → TOPIC (discusses)
144
159
  IMPLIES = "IMPLIES" # NODE → NODE (implies)
145
160
  RELATED_TO = "RELATED_TO" # ANY ↔ ANY (related_to)
161
+ # v3.6.0 Knowledge Graph First — 출처/소유/구성/결정 관계를 1급 엣지로 승격.
162
+ # 추가형: 새 관계는 enum 멤버 추가 + _LEGACY_EDGE_MAP 별칭 등록만으로 확장된다.
163
+ INDEXED_FROM = "INDEXED_FROM" # NODE → SOURCE (어떤 출처에서 색인됐는가)
164
+ MODIFIED_BY = "MODIFIED_BY" # NODE → PERSON (마지막 수정자)
165
+ BELONGS_TO_PROJECT = "BELONGS_TO_PROJECT" # NODE → PROJECT
166
+ PART_OF = "PART_OF" # NODE → NODE (구성요소 관계)
167
+ DISCUSSED_IN = "DISCUSSED_IN" # CONCEPT/DECISION → MEETING/CHAT
168
+ DECIDED_BY = "DECIDED_BY" # DECISION → PERSON
169
+ GENERATED_BY = "GENERATED_BY" # NODE → AGENT/MODEL/WORKFLOW
170
+ USED_BY_AGENT = "USED_BY_AGENT" # NODE → AGENT (에이전트가 사용함)
146
171
 
147
172
  @classmethod
148
173
  def from_legacy(cls, label: str) -> "EdgeType":
@@ -151,8 +176,13 @@ class EdgeType(str, Enum):
151
176
  매핑이 없는 동적 타입은 ``MENTIONS`` 로 폴백하지만, 호출부는 원본 문자열을
152
177
  ``edges_v2.legacy_type`` 에 보존하므로 정보 손실은 없다.
153
178
  """
154
- m = (label or "").strip().lower()
155
- return _LEGACY_EDGE_MAP.get(m, cls.MENTIONS)
179
+ m = (label or "").strip()
180
+ # Canonical values round-trip exactly (v4 native writes use them).
181
+ try:
182
+ return cls(m.upper())
183
+ except ValueError:
184
+ pass
185
+ return _LEGACY_EDGE_MAP.get(m.lower(), cls.MENTIONS)
156
186
 
157
187
 
158
188
  # legacy(자유 문자열 / 한글 동사) → enum 매핑 표.
@@ -199,6 +229,19 @@ _LEGACY_NODE_MAP: Dict[str, NodeType] = {
199
229
  "보고서": NodeType.DOCUMENT,
200
230
  "계획서": NodeType.DOCUMENT,
201
231
  "기획서": NodeType.DOCUMENT,
232
+ # v3.6.0 Knowledge Graph First 엔티티
233
+ "source": NodeType.SOURCE,
234
+ "ingestionsource": NodeType.SOURCE,
235
+ "repository": NodeType.REPOSITORY,
236
+ "repo": NodeType.REPOSITORY,
237
+ "gitrepo": NodeType.REPOSITORY,
238
+ "meeting": NodeType.MEETING,
239
+ "organization": NodeType.ORGANIZATION,
240
+ "org": NodeType.ORGANIZATION,
241
+ "company": NodeType.ORGANIZATION,
242
+ "team": NodeType.ORGANIZATION,
243
+ "workflow": NodeType.WORKFLOW,
244
+ "agent": NodeType.AGENT,
202
245
  }
203
246
 
204
247
  _LEGACY_EDGE_MAP: Dict[str, EdgeType] = {
@@ -254,6 +297,20 @@ _LEGACY_EDGE_MAP: Dict[str, EdgeType] = {
254
297
  "영감받음": EdgeType.INSPIRED_BY,
255
298
  "상충함": EdgeType.CONTRADICTS,
256
299
  "발전함": EdgeType.EVOLVES_FROM,
300
+ # v3.6.0 Knowledge Graph First 관계
301
+ "indexed_from": EdgeType.INDEXED_FROM,
302
+ "modified_by": EdgeType.MODIFIED_BY,
303
+ "belongs_to_project": EdgeType.BELONGS_TO_PROJECT,
304
+ "belongs_to": EdgeType.BELONGS_TO_PROJECT,
305
+ "part_of": EdgeType.PART_OF,
306
+ "discussed_in": EdgeType.DISCUSSED_IN,
307
+ "decided_by": EdgeType.DECIDED_BY,
308
+ "generated_by": EdgeType.GENERATED_BY,
309
+ "used_by_agent": EdgeType.USED_BY_AGENT,
310
+ "색인됨": EdgeType.INDEXED_FROM,
311
+ "수정함": EdgeType.MODIFIED_BY,
312
+ "결정함": EdgeType.DECIDED_BY,
313
+ "구성요소": EdgeType.PART_OF,
257
314
  }
258
315
 
259
316
  # ── SQLite v2 store ─────────────────────────────────────────────────────────
@@ -272,7 +329,13 @@ CREATE TABLE IF NOT EXISTS nodes_v2 (
272
329
  attrs TEXT NOT NULL DEFAULT '{}',
273
330
  embedding BLOB,
274
331
  owner_id TEXT,
332
+ -- NULL workspace_id = legacy-global (pre-scoping rows, readable machine-wide).
333
+ workspace_id TEXT,
334
+ -- 'legacy' marks rows that predate scoping — the 'private' default must not
335
+ -- silently privatize previously machine-shared data (design-review ruling).
275
336
  visibility TEXT NOT NULL DEFAULT 'private',
337
+ -- Revision chain: a node replaced by a newer one points at its successor.
338
+ superseded_by TEXT,
276
339
  created_at TEXT NOT NULL,
277
340
  updated_at TEXT NOT NULL,
278
341
  style TEXT,
@@ -293,14 +356,33 @@ CREATE TABLE IF NOT EXISTS edges_v2 (
293
356
  metadata TEXT NOT NULL DEFAULT '{}',
294
357
  created_by TEXT NOT NULL DEFAULT 'user',
295
358
  created_at TEXT NOT NULL,
296
- -- Edge identity follows the *raw* legacy type, not the normalized type:
297
- -- two distinct legacy types between the same pair (e.g. "mentions" and
298
- -- "관련됨") must stay distinct edges even though both normalize to MENTIONS.
299
- UNIQUE(source, target, legacy_type),
359
+ -- Edge identity (v4): the normalized type AND the raw legacy type.
360
+ -- Migrated rows keep their legacy_type discriminator, so two distinct
361
+ -- legacy strings between one pair (e.g. "mentions" / "관련됨") stay
362
+ -- distinct even though both normalize to MENTIONS. Native canonical
363
+ -- writes carry legacy_type='' so their identity is effectively
364
+ -- (source, target, type) — two canonical types between the same pair
365
+ -- (e.g. MENTIONS + CONTAINS) never collide. The pre-v4
366
+ -- UNIQUE(source, target, legacy_type) would have silently merged them.
367
+ UNIQUE(source, target, type, legacy_type),
300
368
  FOREIGN KEY(source) REFERENCES nodes_v2(id) ON DELETE CASCADE,
301
369
  FOREIGN KEY(target) REFERENCES nodes_v2(id) ON DELETE CASCADE
302
370
  );
303
371
 
372
+ -- Temporal dimension (v4): every repeated observation of a relationship is
373
+ -- recorded — edges_v2's UNIQUE identity + weight=max would otherwise erase
374
+ -- when something was learned, how often, and whether it still holds.
375
+ CREATE TABLE IF NOT EXISTS edge_occurrences (
376
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
377
+ edge_id TEXT NOT NULL,
378
+ observed_at TEXT NOT NULL,
379
+ weight REAL NOT NULL DEFAULT 1.0,
380
+ source TEXT,
381
+ FOREIGN KEY(edge_id) REFERENCES edges_v2(id) ON DELETE CASCADE
382
+ );
383
+ CREATE INDEX IF NOT EXISTS idx_edge_occurrences_edge ON edge_occurrences(edge_id);
384
+ CREATE INDEX IF NOT EXISTS idx_edge_occurrences_time ON edge_occurrences(observed_at);
385
+
304
386
  CREATE INDEX IF NOT EXISTS idx_nodes_v2_type ON nodes_v2(type);
305
387
  CREATE INDEX IF NOT EXISTS idx_nodes_v2_legacy ON nodes_v2(legacy_type);
306
388
  CREATE INDEX IF NOT EXISTS idx_nodes_v2_owner ON nodes_v2(owner_id);
@@ -352,8 +434,16 @@ class KGStoreV2:
352
434
  "edges_v2": {"id", "source", "target", "type", "legacy_type", "weight",
353
435
  "confidence", "evidence", "metadata", "created_by", "created_at"},
354
436
  "nodes_v2": {"id", "type", "legacy_type", "label", "summary", "attrs",
355
- "embedding", "owner_id", "visibility", "created_at",
356
- "updated_at", "style", "tone", "importance_score", "last_used"},
437
+ "embedding", "owner_id", "workspace_id", "visibility",
438
+ "superseded_by", "created_at", "updated_at", "style",
439
+ "tone", "importance_score", "last_used"},
440
+ }
441
+
442
+ # Columns added after a table's first release that can be healed in place
443
+ # with ALTER TABLE ADD COLUMN (nullable / defaulted only).
444
+ _V2_ADDABLE_COLUMNS = {
445
+ "nodes_v2": {"workspace_id": "TEXT", "superseded_by": "TEXT"},
446
+ "edges_v2": {},
357
447
  }
358
448
 
359
449
  def _drop_stale_empty_v2_tables(self, conn: sqlite3.Connection) -> None:
@@ -374,6 +464,13 @@ class KGStoreV2:
374
464
  continue
375
465
  cols = {r[1] for r in conn.execute(f"PRAGMA table_info({table})").fetchall()}
376
466
  missing = self._V2_EXPECTED_COLUMNS[table] - cols
467
+ if not missing:
468
+ continue
469
+ # Additive columns heal in place without touching data.
470
+ addable = self._V2_ADDABLE_COLUMNS.get(table, {})
471
+ for col in sorted(missing & set(addable)):
472
+ conn.execute(f"ALTER TABLE {table} ADD COLUMN {col} {addable[col]}")
473
+ missing -= set(addable)
377
474
  if not missing:
378
475
  continue
379
476
  count = conn.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0]
@@ -400,8 +497,40 @@ class KGStoreV2:
400
497
  with self._conn() as own:
401
498
  self._init_schema_on(own)
402
499
 
500
+ def _rebuild_edges_identity(self, conn: sqlite3.Connection) -> None:
501
+ """Migrate edges_v2 from the pre-v4 UNIQUE(source, target, legacy_type)
502
+ identity to UNIQUE(source, target, type, legacy_type).
503
+
504
+ SQLite cannot alter constraints, so this is a create→copy→swap inside
505
+ the caller's transaction. Data-preserving: every existing row keeps its
506
+ legacy_type discriminator. Re-entrant: keyed on the actual constraint
507
+ in sqlite_master, not a one-time stamp.
508
+ """
509
+ row = conn.execute(
510
+ "SELECT sql FROM sqlite_master WHERE type='table' AND name='edges_v2'"
511
+ ).fetchone()
512
+ if not row or "UNIQUE(source, target, type, legacy_type)" in (row["sql"] or ""):
513
+ return
514
+ conn.execute("ALTER TABLE edges_v2 RENAME TO edges_v2_old")
515
+ # Recreate from the canonical DDL (edges_v2 portion of SCHEMA_SQL).
516
+ start = SCHEMA_SQL.index("CREATE TABLE IF NOT EXISTS edges_v2")
517
+ end = SCHEMA_SQL.index(");", start) + 2
518
+ conn.execute(SCHEMA_SQL[start:end].rstrip(";"))
519
+ conn.execute(
520
+ """
521
+ INSERT INTO edges_v2 (id, source, target, type, legacy_type, weight,
522
+ confidence, evidence, metadata, created_by, created_at)
523
+ SELECT id, source, target, type, legacy_type, weight,
524
+ confidence, evidence, metadata, created_by, created_at
525
+ FROM edges_v2_old
526
+ """
527
+ )
528
+ conn.execute("DROP TABLE edges_v2_old")
529
+ logging.info("kg_schema: rebuilt edges_v2 with (source, target, type, legacy_type) identity")
530
+
403
531
  def _init_schema_on(self, conn: sqlite3.Connection) -> None:
404
532
  self._drop_stale_empty_v2_tables(conn)
533
+ self._rebuild_edges_identity(conn)
405
534
  _exec_script(conn, SCHEMA_SQL)
406
535
  conn.execute(
407
536
  "INSERT OR REPLACE INTO kg_meta(key, value) VALUES (?, ?)",