ltcai 11.2.0 → 11.5.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 (253) hide show
  1. package/README.md +50 -53
  2. package/docs/CHANGELOG.md +87 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/MULTI_AGENT_RUNTIME.md +1 -1
  6. package/docs/ONBOARDING.md +1 -1
  7. package/docs/OPERATIONS.md +6 -2
  8. package/docs/PERMISSION_MODE.md +1 -1
  9. package/docs/TRUST_MODEL.md +1 -1
  10. package/docs/WHY_LATTICE.md +1 -1
  11. package/docs/kg-schema.md +2 -2
  12. package/docs/v11.3.0_PLAN.md +202 -0
  13. package/docs/v11.4.0_RUST_FOUNDATION_PLAN.md +181 -0
  14. package/docs/v11.5.0_RUST_COMPLETE_PLAN.md +145 -0
  15. package/lattice_brain/__init__.py +1 -1
  16. package/lattice_brain/graph/_kg_common/__init__.py +287 -0
  17. package/lattice_brain/graph/_kg_common/extraction.py +516 -0
  18. package/lattice_brain/graph/_kg_common/relations.py +161 -0
  19. package/lattice_brain/graph/_kg_common/text.py +479 -0
  20. package/lattice_brain/graph/discovery_index/__init__.py +35 -0
  21. package/lattice_brain/graph/discovery_index/cleanup.py +182 -0
  22. package/lattice_brain/graph/discovery_index/extract.py +137 -0
  23. package/lattice_brain/graph/discovery_index/scan.py +411 -0
  24. package/lattice_brain/graph/discovery_index/upsert.py +495 -0
  25. package/lattice_brain/graph/projection/__init__.py +42 -0
  26. package/lattice_brain/graph/projection/curation.py +500 -0
  27. package/lattice_brain/graph/{projection.py → projection/v2_schema.py} +15 -477
  28. package/lattice_brain/graph/retrieval/__init__.py +54 -0
  29. package/lattice_brain/graph/retrieval/context.py +197 -0
  30. package/lattice_brain/graph/retrieval/graph_view.py +319 -0
  31. package/lattice_brain/graph/retrieval/hybrid.py +488 -0
  32. package/lattice_brain/graph/retrieval/maintenance.py +121 -0
  33. package/lattice_brain/graph/retrieval/signals.py +95 -0
  34. package/lattice_brain/graph/retrieval_vector/__init__.py +42 -0
  35. package/lattice_brain/graph/retrieval_vector/fingerprint.py +97 -0
  36. package/lattice_brain/graph/retrieval_vector/indexing.py +347 -0
  37. package/lattice_brain/graph/retrieval_vector/search.py +560 -0
  38. package/lattice_brain/graph/retrieval_vector/status.py +374 -0
  39. package/lattice_brain/ingestion/__init__.py +130 -0
  40. package/lattice_brain/ingestion/_contract.py +90 -0
  41. package/lattice_brain/ingestion/constants.py +127 -0
  42. package/lattice_brain/ingestion/folder_scan.py +57 -0
  43. package/lattice_brain/ingestion/folders.py +258 -0
  44. package/lattice_brain/ingestion/hashing.py +26 -0
  45. package/lattice_brain/ingestion/jobs_api.py +107 -0
  46. package/lattice_brain/ingestion/models.py +80 -0
  47. package/lattice_brain/ingestion/pipeline.py +486 -0
  48. package/lattice_brain/ingestion/quality.py +209 -0
  49. package/lattice_brain/ingestion/routing.py +295 -0
  50. package/lattice_brain/multimodal/__init__.py +164 -0
  51. package/lattice_brain/multimodal/audio.py +77 -0
  52. package/lattice_brain/multimodal/common.py +118 -0
  53. package/lattice_brain/multimodal/images.py +498 -0
  54. package/lattice_brain/multimodal/ports.py +169 -0
  55. package/lattice_brain/multimodal/video.py +410 -0
  56. package/lattice_brain/portability/__init__.py +90 -0
  57. package/lattice_brain/portability/_contract.py +42 -0
  58. package/lattice_brain/portability/backups.py +338 -0
  59. package/lattice_brain/portability/bundles.py +136 -0
  60. package/lattice_brain/portability/constants.py +93 -0
  61. package/lattice_brain/portability/fsops.py +138 -0
  62. package/lattice_brain/portability/service.py +41 -0
  63. package/lattice_brain/{portability.py → portability/sharing.py} +44 -677
  64. package/lattice_brain/runtime/__init__.py +1 -1
  65. package/lattice_brain/runtime/multi_agent.py +1 -1
  66. package/latticeai/__init__.py +1 -1
  67. package/latticeai/api/chronicle.py +63 -0
  68. package/latticeai/api/index_jobs.py +145 -0
  69. package/latticeai/core/agent/__init__.py +93 -0
  70. package/latticeai/core/agent/_contract.py +79 -0
  71. package/latticeai/core/agent/context.py +57 -0
  72. package/latticeai/core/agent/deps.py +125 -0
  73. package/latticeai/core/agent/execution.py +622 -0
  74. package/latticeai/core/agent/planning.py +145 -0
  75. package/latticeai/core/agent/recovery.py +157 -0
  76. package/latticeai/core/agent/runtime.py +210 -0
  77. package/latticeai/core/agent/verification.py +231 -0
  78. package/latticeai/core/embedding_providers/__init__.py +151 -0
  79. package/latticeai/core/embedding_providers/base.py +199 -0
  80. package/latticeai/core/embedding_providers/captions.py +162 -0
  81. package/latticeai/core/embedding_providers/profiles.py +126 -0
  82. package/latticeai/core/embedding_providers/text.py +350 -0
  83. package/latticeai/core/embedding_providers/vision.py +352 -0
  84. package/latticeai/core/file_generation/__init__.py +115 -0
  85. package/latticeai/core/file_generation/bundles.py +76 -0
  86. package/latticeai/core/file_generation/extraction.py +154 -0
  87. package/latticeai/core/file_generation/inference.py +235 -0
  88. package/latticeai/core/file_generation/orchestration.py +152 -0
  89. package/latticeai/core/file_generation/prompting.py +117 -0
  90. package/latticeai/core/file_generation/repair.py +114 -0
  91. package/latticeai/core/file_generation/sanitize.py +61 -0
  92. package/latticeai/core/file_generation/validation.py +201 -0
  93. package/latticeai/core/legacy_compatibility.py +1 -1
  94. package/latticeai/core/marketplace.py +1 -1
  95. package/latticeai/core/messages.py +14 -0
  96. package/latticeai/core/workspace_os_constants.py +1 -1
  97. package/latticeai/integrations/telegram_bot/__init__.py +123 -0
  98. package/latticeai/integrations/telegram_bot/__main__.py +17 -0
  99. package/latticeai/integrations/telegram_bot/config.py +86 -0
  100. package/latticeai/integrations/telegram_bot/dispatch.py +311 -0
  101. package/latticeai/integrations/telegram_bot/flows.py +478 -0
  102. package/latticeai/integrations/telegram_bot/helpers.py +322 -0
  103. package/latticeai/integrations/telegram_bot/screens.py +394 -0
  104. package/latticeai/models/router/__init__.py +88 -0
  105. package/latticeai/models/router/_contract.py +66 -0
  106. package/latticeai/models/router/branding.py +56 -0
  107. package/latticeai/models/router/catalog.py +69 -0
  108. package/latticeai/models/router/documents.py +199 -0
  109. package/latticeai/models/router/errors.py +37 -0
  110. package/latticeai/models/router/generation.py +258 -0
  111. package/latticeai/models/router/loading.py +291 -0
  112. package/latticeai/models/router/local_models.py +85 -0
  113. package/latticeai/models/router/registry.py +147 -0
  114. package/latticeai/runtime/build_phases/__init__.py +82 -0
  115. package/latticeai/runtime/build_phases/features.py +421 -0
  116. package/latticeai/runtime/build_phases/foundation.py +555 -0
  117. package/latticeai/runtime/build_phases/web.py +492 -0
  118. package/latticeai/runtime/runtime_context.py +1 -0
  119. package/latticeai/services/architecture_readiness.py +48 -19
  120. package/latticeai/services/brain_intelligence/__init__.py +58 -0
  121. package/latticeai/services/brain_intelligence/_contract.py +71 -0
  122. package/latticeai/services/brain_intelligence/consistency.py +193 -0
  123. package/latticeai/services/brain_intelligence/constants.py +47 -0
  124. package/latticeai/services/brain_intelligence/digest.py +258 -0
  125. package/latticeai/services/brain_intelligence/health.py +331 -0
  126. package/latticeai/services/brain_intelligence/proposals.py +264 -0
  127. package/latticeai/services/brain_intelligence/sampling.py +84 -0
  128. package/latticeai/services/brain_intelligence/service.py +48 -0
  129. package/latticeai/services/chronicle.py +557 -0
  130. package/latticeai/services/memory_service/__init__.py +52 -0
  131. package/latticeai/services/memory_service/_contract.py +100 -0
  132. package/latticeai/services/memory_service/brief.py +431 -0
  133. package/latticeai/services/memory_service/constants.py +57 -0
  134. package/latticeai/services/memory_service/maintenance.py +138 -0
  135. package/latticeai/services/memory_service/manager.py +186 -0
  136. package/latticeai/services/memory_service/proof.py +136 -0
  137. package/latticeai/services/memory_service/recall.py +225 -0
  138. package/latticeai/services/memory_service/service.py +48 -0
  139. package/latticeai/services/memory_service/stores.py +110 -0
  140. package/latticeai/services/model_runtime/__init__.py +322 -0
  141. package/latticeai/services/model_runtime/cloud.py +87 -0
  142. package/latticeai/services/model_runtime/download.py +282 -0
  143. package/latticeai/services/model_runtime/engines.py +341 -0
  144. package/latticeai/services/model_runtime/loading.py +178 -0
  145. package/latticeai/services/model_runtime/service.py +129 -0
  146. package/latticeai/services/model_runtime/state.py +131 -0
  147. package/latticeai/services/model_runtime/status.py +255 -0
  148. package/latticeai/services/product_readiness.py +15 -7
  149. package/latticeai/setup/wizard/__init__.py +126 -0
  150. package/latticeai/setup/wizard/catalog.py +172 -0
  151. package/latticeai/setup/wizard/detect.py +323 -0
  152. package/latticeai/setup/wizard/install.py +348 -0
  153. package/latticeai/setup/wizard/paths.py +168 -0
  154. package/latticeai/setup/wizard/plans.py +74 -0
  155. package/latticeai/setup/wizard/recommend.py +320 -0
  156. package/package.json +6 -2
  157. package/scripts/bump_version.py +14 -0
  158. package/scripts/capture_release_evidence.mjs +33 -21
  159. package/scripts/check_current_release_docs.mjs +1 -1
  160. package/scripts/check_i18n_namespace_coverage.mjs +41 -4
  161. package/scripts/check_max_file_lines.mjs +102 -0
  162. package/scripts/check_release_evidence_bound.mjs +30 -15
  163. package/scripts/check_screenshot_pixel_delta.py +34 -4
  164. package/scripts/check_server_i18n.mjs +2 -0
  165. package/scripts/chunking_parity_corpus.py +449 -0
  166. package/scripts/generate_agent_parity_fixtures.py +752 -0
  167. package/scripts/generate_chunking_parity_fixtures.py +259 -0
  168. package/scripts/generate_rust_parity_fixtures.py +997 -0
  169. package/scripts/lib/mock_server_fingerprint.mjs +94 -0
  170. package/scripts/release_screen_claims.json +42 -2
  171. package/src-tauri/Cargo.lock +404 -3
  172. package/src-tauri/Cargo.toml +13 -1
  173. package/src-tauri/src/backend.rs +460 -0
  174. package/src-tauri/src/folder.rs +33 -0
  175. package/src-tauri/src/main.rs +109 -399
  176. package/src-tauri/src/topology.rs +356 -0
  177. package/src-tauri/tauri.conf.json +1 -1
  178. package/static/app/asset-manifest.json +41 -37
  179. package/static/app/assets/Act-CWnxSCgN.js +1 -0
  180. package/static/app/assets/AdminConsole-BEQYU6kF.js +1 -0
  181. package/static/app/assets/{Brain-tuhI4sOC.js → Brain-DWu1BhFg.js} +2 -2
  182. package/static/app/assets/BrainHome-95Hilr9R.js +2 -0
  183. package/static/app/assets/BrainSignals-QdeqCpAF.js +1 -0
  184. package/static/app/assets/Capture-BHpCxnzb.js +1 -0
  185. package/static/app/assets/Chronicle-B4xYKoed.js +1 -0
  186. package/static/app/assets/CommandPalette-BVXnttSz.js +1 -0
  187. package/static/app/assets/Library-DgYcHome.js +1 -0
  188. package/static/app/assets/{LivingBrain-DBwhto14.js → LivingBrain-CrJLDbf7.js} +1 -1
  189. package/static/app/assets/ProductFlow-DFlScKoJ.js +1 -0
  190. package/static/app/assets/ReviewCard-Cy5f48Pj.js +3 -0
  191. package/static/app/assets/System-NF8IfhTa.js +1 -0
  192. package/static/app/assets/arrow-left-DwkSYrjR.js +1 -0
  193. package/static/app/assets/{bot-Cia42c2h.js → bot-CucuhLhm.js} +1 -1
  194. package/static/app/assets/brain-BBnSryW_.js +1 -0
  195. package/static/app/assets/{button-2j2Ijzgq.js → button-C2GUj2Ai.js} +1 -1
  196. package/static/app/assets/circle-check-CxOVPwYq.js +1 -0
  197. package/static/app/assets/{circle-pause-BEFeWpVW.js → circle-pause-CbkWzBmG.js} +1 -1
  198. package/static/app/assets/{circle-play-ujXMcHxl.js → circle-play-7lEaqHdJ.js} +1 -1
  199. package/static/app/assets/{cpu-k4awryFq.js → cpu-DAlCXlIy.js} +1 -1
  200. package/static/app/assets/{download-DFbLJ_ig.js → download-RNhuuJwh.js} +1 -1
  201. package/static/app/assets/{folder-open-7y_b6xkM.js → folder-open-CLW4odzM.js} +1 -1
  202. package/static/app/assets/{hard-drive-Bidh02Kr.js → hard-drive-NKEiDIAJ.js} +1 -1
  203. package/static/app/assets/{index-DwDl9-8Y.css → index-BLPb5lmE.css} +1 -1
  204. package/static/app/assets/index-DMurvUuR.js +10 -0
  205. package/static/app/assets/input-D2UhPC1X.js +1 -0
  206. package/static/app/assets/link-2-6amKbP_P.js +1 -0
  207. package/static/app/assets/{permissionCopy-Bpb83Hx9.js → permissionCopy-Cu9TZtdR.js} +1 -1
  208. package/static/app/assets/primitives-gPsccucr.js +1 -0
  209. package/static/app/assets/search-Cj_TKk_2.js +1 -0
  210. package/static/app/assets/{share-2-BH1M-WNi.js → share-2-Bau7KkPq.js} +1 -1
  211. package/static/app/assets/{shield-alert-BlKdBXcG.js → shield-alert-BufNYypi.js} +1 -1
  212. package/static/app/assets/{textarea-CCWbUfFB.js → textarea-BQnVWhYs.js} +1 -1
  213. package/static/app/assets/{useFocusTrap-YdHQ7pJ1.js → useFocusTrap-B3_w60si.js} +1 -1
  214. package/static/app/assets/useMutation-BHhCflT6.js +1 -0
  215. package/static/app/assets/{useQuery-CXQiwbVT.js → useQuery-rBWfI-5t.js} +1 -1
  216. package/static/app/assets/utils-V_5-wxr5.js +4 -0
  217. package/static/app/assets/workspace-K1zjYUHj.js +1 -0
  218. package/static/app/index.html +4 -4
  219. package/static/sw.js +1 -1
  220. package/lattice_brain/graph/_kg_common.py +0 -1331
  221. package/lattice_brain/graph/discovery_index.py +0 -1141
  222. package/lattice_brain/graph/retrieval.py +0 -1120
  223. package/lattice_brain/graph/retrieval_vector.py +0 -1293
  224. package/lattice_brain/ingestion.py +0 -1525
  225. package/lattice_brain/multimodal.py +0 -1258
  226. package/latticeai/core/agent.py +0 -1465
  227. package/latticeai/core/embedding_providers.py +0 -1196
  228. package/latticeai/core/file_generation.py +0 -1047
  229. package/latticeai/integrations/telegram_bot.py +0 -1390
  230. package/latticeai/models/router.py +0 -1007
  231. package/latticeai/runtime/build_phases.py +0 -1450
  232. package/latticeai/services/brain_intelligence.py +0 -1083
  233. package/latticeai/services/memory_service.py +0 -1177
  234. package/latticeai/services/model_runtime.py +0 -1281
  235. package/latticeai/setup/wizard.py +0 -1310
  236. package/static/app/assets/Act-AWf0SAKp.js +0 -1
  237. package/static/app/assets/AdminConsole-D0u8Tiyj.js +0 -1
  238. package/static/app/assets/BrainHome-Ts7G_Ila.js +0 -2
  239. package/static/app/assets/BrainSignals-jMYgQ2Ar.js +0 -1
  240. package/static/app/assets/Capture-CqOSzyPr.js +0 -1
  241. package/static/app/assets/CommandPalette-DC0Bzh-I.js +0 -1
  242. package/static/app/assets/Library-CX-bbhmK.js +0 -1
  243. package/static/app/assets/ProductFlow-BHA2cfKI.js +0 -1
  244. package/static/app/assets/ReviewCard-BUhCKRNM.js +0 -3
  245. package/static/app/assets/System-Bu2t5hn1.js +0 -1
  246. package/static/app/assets/arrow-left-Dzwa5zRb.js +0 -1
  247. package/static/app/assets/brain-DJMoqrwx.js +0 -1
  248. package/static/app/assets/index-BpYkzcVm.js +0 -10
  249. package/static/app/assets/input-DSlJJxRs.js +0 -1
  250. package/static/app/assets/primitives-BCx6TvfG.js +0 -1
  251. package/static/app/assets/search-Cgy8cCFJ.js +0 -1
  252. package/static/app/assets/utils-zqPZJxdx.js +0 -4
  253. package/static/app/assets/workspace-DXTihhfU.js +0 -1
@@ -0,0 +1,495 @@
1
+ """One local file → its rows and its graph nodes.
2
+
3
+ The computer/drive/folder hierarchy, the ``local_file_index`` row, and the
4
+ file node with its concepts and edges. ``_local_scoped_slug`` lives here
5
+ because both hierarchy and node upsert call it. Moved verbatim out of
6
+ ``discovery_index.py`` (v11.3.0 decomposition).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import TYPE_CHECKING
12
+
13
+ # ruff: noqa: F403,F405
14
+ from .._kg_common import * # noqa: F403,F401
15
+
16
+ # The cross-mixin surface (`_connect`, `_upsert_node`, …) is declared in
17
+ # `_kg_contract.KnowledgeGraphCore`. It is a typing-only base: at runtime this
18
+ # is `object`, so the MRO of `KnowledgeGraphStore` is unchanged.
19
+ if TYPE_CHECKING:
20
+ from .._kg_contract import KnowledgeGraphCore as _Core
21
+ else:
22
+ _Core = object
23
+
24
+
25
+ def _local_scoped_slug(prefix: str, value: str, workspace_id: Optional[str]) -> str:
26
+ """Preserve legacy IDs while isolating newly workspace-scoped nodes."""
27
+ slug = _slug(value)
28
+ if not workspace_id:
29
+ return f"{prefix}:{slug}"
30
+ scope = _sha256_text(str(workspace_id))[:12]
31
+ return f"{prefix}:{scope}:{slug}"
32
+
33
+
34
+ class _LocalUpsertMixin(_Core):
35
+ """Hierarchy + file node/index upserts. Composed into the public mixin."""
36
+
37
+ def _ensure_local_hierarchy(
38
+ self,
39
+ conn: sqlite3.Connection,
40
+ *,
41
+ source_id: str,
42
+ root: Path,
43
+ file_path: Path,
44
+ os_type: str,
45
+ drive_id: str,
46
+ user_email: Optional[str] = None,
47
+ workspace_id: Optional[str] = None,
48
+ ) -> str:
49
+ computer_label = platform.node() or "내 컴퓨터"
50
+ computer_id = _local_scoped_slug("computer", computer_label, workspace_id)
51
+ drive_identity = f"{workspace_id}|{os_type}:{drive_id}" if workspace_id else f"{os_type}:{drive_id}"
52
+ drive_node_id = f"drive:{_sha256_text(drive_identity)[:24]}"
53
+ root_folder_id = f"folder:{_sha256_text(f'{source_id}:root')[:24]}"
54
+ self._upsert_node(
55
+ conn,
56
+ computer_id,
57
+ "Computer",
58
+ computer_label,
59
+ metadata={"os_type": os_type, "workspace_id": workspace_id},
60
+ owner=user_email,
61
+ workspace_id=workspace_id,
62
+ )
63
+ self._upsert_node(
64
+ conn,
65
+ drive_node_id,
66
+ "Drive",
67
+ drive_id,
68
+ metadata={"os_type": os_type, "drive_id": drive_id, "workspace_id": workspace_id},
69
+ owner=user_email,
70
+ workspace_id=workspace_id,
71
+ )
72
+ stale_parents = conn.execute(
73
+ """
74
+ SELECT e.from_node
75
+ FROM edges e
76
+ JOIN nodes n ON n.id=e.from_node
77
+ WHERE e.to_node=? AND n.type='Drive' AND e.from_node<>?
78
+ """,
79
+ (root_folder_id, drive_node_id),
80
+ ).fetchall()
81
+ for row in stale_parents:
82
+ conn.execute(
83
+ "DELETE FROM edges WHERE from_node=? AND to_node=?",
84
+ (row["from_node"], root_folder_id),
85
+ )
86
+ conn.execute(
87
+ "DELETE FROM edges_v2 WHERE source=? AND target=?",
88
+ (row["from_node"], root_folder_id),
89
+ )
90
+ self._upsert_edge(
91
+ conn,
92
+ computer_id,
93
+ drive_node_id,
94
+ "포함함",
95
+ metadata={"source": "local_scan", "workspace_id": workspace_id},
96
+ )
97
+ self._upsert_node(
98
+ conn,
99
+ root_folder_id,
100
+ "Folder",
101
+ root.name or str(root),
102
+ summary=str(root),
103
+ metadata={"source_id": source_id, "path": str(root), "root": True, "workspace_id": workspace_id},
104
+ owner=user_email,
105
+ workspace_id=workspace_id,
106
+ )
107
+ self._upsert_edge(
108
+ conn,
109
+ drive_node_id,
110
+ root_folder_id,
111
+ "포함함",
112
+ metadata={"source": "local_scan", "workspace_id": workspace_id},
113
+ )
114
+
115
+ try:
116
+ relative_parent = file_path.parent.relative_to(root)
117
+ except ValueError:
118
+ relative_parent = Path()
119
+ parent_id = root_folder_id
120
+ current_path = root
121
+ for part in relative_parent.parts:
122
+ current_path = current_path / part
123
+ folder_id = (
124
+ f"folder:{_sha256_text(f'{source_id}:{current_path.as_posix()}')[:24]}"
125
+ )
126
+ self._upsert_node(
127
+ conn,
128
+ folder_id,
129
+ "Folder",
130
+ part,
131
+ summary=str(current_path),
132
+ metadata={
133
+ "source_id": source_id,
134
+ "path": str(current_path),
135
+ "root": False,
136
+ "workspace_id": workspace_id,
137
+ },
138
+ owner=user_email,
139
+ workspace_id=workspace_id,
140
+ )
141
+ self._upsert_edge(
142
+ conn,
143
+ parent_id,
144
+ folder_id,
145
+ "포함함",
146
+ metadata={"source": "local_scan", "workspace_id": workspace_id},
147
+ )
148
+ parent_id = folder_id
149
+ return parent_id
150
+
151
+ def _upsert_local_file_index(
152
+ self,
153
+ conn: sqlite3.Connection,
154
+ *,
155
+ source_id: str,
156
+ root: Path,
157
+ file_path: Path,
158
+ stat: Optional[os.stat_result],
159
+ os_type: str,
160
+ drive_id: str,
161
+ status: str,
162
+ parser_type: str,
163
+ sha256: Optional[str] = None,
164
+ graph_node_id: Optional[str] = None,
165
+ error_message: Optional[str] = None,
166
+ metadata: Optional[Dict[str, Any]] = None,
167
+ ) -> str:
168
+ try:
169
+ relative_path = file_path.relative_to(root).as_posix()
170
+ except ValueError:
171
+ relative_path = file_path.name
172
+ index_id = f"local-index:{_sha256_text(f'{source_id}:{relative_path}')[:24]}"
173
+ now = _now()
174
+ size = stat.st_size if stat else None
175
+ modified_at = _safe_iso_from_stat_mtime(stat.st_mtime) if stat else ""
176
+ conn.execute(
177
+ """
178
+ INSERT INTO local_file_index(
179
+ id, source_id, os_type, drive_id, root_path, file_path, relative_path,
180
+ file_name, extension, size_bytes, modified_at, sha256, last_scanned_at,
181
+ last_indexed_at, parser_type, status, error_message, graph_node_id,
182
+ deleted, metadata_json
183
+ )
184
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
185
+ ON CONFLICT(source_id, relative_path) DO UPDATE SET
186
+ os_type=excluded.os_type,
187
+ drive_id=excluded.drive_id,
188
+ root_path=excluded.root_path,
189
+ file_path=excluded.file_path,
190
+ file_name=excluded.file_name,
191
+ extension=excluded.extension,
192
+ size_bytes=excluded.size_bytes,
193
+ modified_at=excluded.modified_at,
194
+ sha256=excluded.sha256,
195
+ last_scanned_at=excluded.last_scanned_at,
196
+ last_indexed_at=excluded.last_indexed_at,
197
+ parser_type=excluded.parser_type,
198
+ status=excluded.status,
199
+ error_message=excluded.error_message,
200
+ graph_node_id=excluded.graph_node_id,
201
+ deleted=excluded.deleted,
202
+ metadata_json=excluded.metadata_json
203
+ """,
204
+ (
205
+ index_id,
206
+ source_id,
207
+ os_type,
208
+ drive_id,
209
+ str(root),
210
+ str(file_path),
211
+ relative_path,
212
+ file_path.name,
213
+ file_path.suffix.lower(),
214
+ size,
215
+ modified_at,
216
+ sha256,
217
+ now,
218
+ now if status == "indexed" else None,
219
+ parser_type,
220
+ status,
221
+ error_message,
222
+ graph_node_id,
223
+ 0 if status != "deleted" else 1,
224
+ _json(metadata),
225
+ ),
226
+ )
227
+ return index_id
228
+
229
+ def _upsert_local_file_node(
230
+ self,
231
+ conn: sqlite3.Connection,
232
+ *,
233
+ source_id: str,
234
+ root: Path,
235
+ file_path: Path,
236
+ stat: os.stat_result,
237
+ os_type: str,
238
+ drive_id: str,
239
+ sha256: str,
240
+ category: str,
241
+ parser_type: str,
242
+ text: str,
243
+ parser_meta: Dict[str, Any],
244
+ user_email: Optional[str] = None,
245
+ workspace_id: Optional[str] = None,
246
+ ) -> str:
247
+ text = _clean_text(text)
248
+ if not text:
249
+ raise ValueError("텍스트 추출 결과가 비어 있습니다.")
250
+ try:
251
+ relative_path = file_path.relative_to(root).as_posix()
252
+ except ValueError:
253
+ relative_path = file_path.name
254
+ file_node_id = f"local-file:{_sha256_text(f'{source_id}:{relative_path}')[:24]}"
255
+ parent_folder_id = self._ensure_local_hierarchy(
256
+ conn,
257
+ source_id=source_id,
258
+ root=root,
259
+ file_path=file_path,
260
+ os_type=os_type,
261
+ drive_id=drive_id,
262
+ user_email=user_email,
263
+ workspace_id=workspace_id,
264
+ )
265
+ linked_rows = conn.execute(
266
+ """
267
+ SELECT e.to_node AS id, n.type, n.metadata_json
268
+ FROM edges e
269
+ JOIN nodes n ON n.id=e.to_node
270
+ WHERE e.from_node=?
271
+ """,
272
+ (file_node_id,),
273
+ ).fetchall()
274
+ child_ids = []
275
+ auto_candidate_ids = set()
276
+ for row in linked_rows:
277
+ linked_metadata = _safe_loads(row["metadata_json"])
278
+ if row["type"] in {"Chunk", "ImageText", "Section"} or linked_metadata.get("source_node") == file_node_id:
279
+ child_ids.append(row["id"])
280
+ elif linked_metadata.get("auto_extracted") and linked_metadata.get("source") == "local_folder":
281
+ auto_candidate_ids.add(row["id"])
282
+ conn.execute("DELETE FROM chunks WHERE source_node=?", (file_node_id,))
283
+ if child_ids:
284
+ placeholders = ",".join("?" * len(child_ids))
285
+ conn.execute(f"DELETE FROM nodes WHERE id IN ({placeholders})", child_ids)
286
+ self._v2_delete_nodes(conn, child_ids)
287
+ conn.execute("DELETE FROM edges WHERE from_node=?", (file_node_id,))
288
+ self._v2_delete_edges_from(conn, file_node_id)
289
+ removable_auto_ids = set()
290
+ for node_id in auto_candidate_ids:
291
+ remaining_edges = conn.execute(
292
+ "SELECT from_node, to_node FROM edges WHERE from_node=? OR to_node=?",
293
+ (node_id, node_id),
294
+ ).fetchall()
295
+ if all(
296
+ row["from_node"] in auto_candidate_ids
297
+ and row["to_node"] in auto_candidate_ids
298
+ for row in remaining_edges
299
+ ):
300
+ removable_auto_ids.add(node_id)
301
+ if removable_auto_ids:
302
+ placeholders = ",".join("?" * len(removable_auto_ids))
303
+ params = list(removable_auto_ids)
304
+ conn.execute(
305
+ f"DELETE FROM edges WHERE from_node IN ({placeholders}) OR to_node IN ({placeholders})",
306
+ params * 2,
307
+ )
308
+ conn.execute(f"DELETE FROM nodes WHERE id IN ({placeholders})", params)
309
+ self._v2_delete_nodes(conn, params)
310
+
311
+ metadata = {
312
+ "source": "local_folder",
313
+ "source_id": source_id,
314
+ "root_path": str(root),
315
+ "file_path": str(file_path),
316
+ "relative_path": relative_path,
317
+ "filename": file_path.name,
318
+ "ext": file_path.suffix.lower(),
319
+ "category": category,
320
+ "parser_type": parser_type,
321
+ "bytes": stat.st_size,
322
+ "modified_at": _safe_iso_from_stat_mtime(stat.st_mtime),
323
+ "sha256": sha256,
324
+ "parser": parser_meta,
325
+ "workspace_id": workspace_id,
326
+ }
327
+ self._upsert_node(
328
+ conn,
329
+ file_node_id,
330
+ _node_type_for_category(category),
331
+ file_path.name,
332
+ summary=text[:700],
333
+ metadata=metadata,
334
+ raw=metadata,
335
+ owner=user_email,
336
+ workspace_id=workspace_id,
337
+ )
338
+ self._upsert_edge(
339
+ conn,
340
+ parent_folder_id,
341
+ file_node_id,
342
+ "포함함",
343
+ weight=1.0,
344
+ metadata={"source": "local_scan", "workspace_id": workspace_id},
345
+ )
346
+ self._cleanup_local_graph_orphans(conn, source_id)
347
+
348
+ target_for_concepts = text
349
+ if category == "image" and text:
350
+ image_text_id = f"imagetext:{_sha256_text(f'{file_node_id}:ocr')[:24]}"
351
+ self._upsert_node(
352
+ conn,
353
+ image_text_id,
354
+ "ImageText",
355
+ f"{file_path.name} OCR",
356
+ summary=_clean_text(text)[:700],
357
+ metadata={
358
+ "source_node": file_node_id,
359
+ "source_id": source_id,
360
+ "chars": len(text),
361
+ "workspace_id": workspace_id,
362
+ },
363
+ owner=user_email,
364
+ workspace_id=workspace_id,
365
+ )
366
+ self._upsert_edge(
367
+ conn,
368
+ file_node_id,
369
+ image_text_id,
370
+ "포함함",
371
+ weight=0.8,
372
+ metadata={"source": "ocr", "workspace_id": workspace_id},
373
+ )
374
+
375
+ # Typed chunking by file extension (markdown/code/plain); plain files
376
+ # keep legacy _chunks boundaries and therefore identical chunk ids.
377
+ chunk_strategy = chunk_strategy_for(file_path.name)
378
+ for index, piece in enumerate(typed_chunks(text, strategy=chunk_strategy)):
379
+ chunk = piece["text"]
380
+ chunk_fields = typed_chunk_meta_fields(piece)
381
+ chunk_id = f"chunk:{_sha256_text(f'{file_node_id}:{index}:{chunk}')[:24]}"
382
+ self._upsert_node(
383
+ conn,
384
+ chunk_id,
385
+ "Chunk",
386
+ f"{file_path.name} chunk {index + 1}",
387
+ summary=chunk[:500],
388
+ metadata={
389
+ "index": index,
390
+ "source_node": file_node_id,
391
+ "source_id": source_id,
392
+ "workspace_id": workspace_id,
393
+ **chunk_fields,
394
+ },
395
+ owner=user_email,
396
+ workspace_id=workspace_id,
397
+ )
398
+ self._upsert_chunk(
399
+ conn,
400
+ chunk_id=chunk_id,
401
+ source_node=file_node_id,
402
+ text=chunk,
403
+ metadata={
404
+ "index": index,
405
+ "source_node": file_node_id,
406
+ "source_id": source_id,
407
+ "workspace_id": workspace_id,
408
+ **chunk_fields,
409
+ },
410
+ )
411
+ self._upsert_edge(
412
+ conn,
413
+ file_node_id,
414
+ chunk_id,
415
+ "포함함",
416
+ weight=0.7,
417
+ metadata={"source": "local_scan", "workspace_id": workspace_id},
418
+ )
419
+
420
+ concepts = _extract_concepts(target_for_concepts, limit=18)
421
+ concept_ids: Dict[str, str] = {}
422
+ for concept in concepts:
423
+ node_t = _classify_node_type(concept, target_for_concepts)
424
+ concept_id = _local_scoped_slug(node_t.lower(), concept, workspace_id)
425
+ concept_ids[concept.lower()] = concept_id
426
+ self._upsert_node(
427
+ conn,
428
+ concept_id,
429
+ node_t,
430
+ concept,
431
+ metadata={
432
+ "auto_extracted": True,
433
+ "source": "local_folder",
434
+ "source_id": source_id,
435
+ "workspace_id": workspace_id,
436
+ },
437
+ owner=user_email,
438
+ workspace_id=workspace_id,
439
+ )
440
+ self._upsert_edge(
441
+ conn,
442
+ file_node_id,
443
+ concept_id,
444
+ "언급함",
445
+ weight=0.75,
446
+ metadata={"source": "local_scan", "workspace_id": workspace_id},
447
+ )
448
+
449
+ for triple in _extract_triples(target_for_concepts, concepts, limit=20):
450
+ subj_id = concept_ids.get(triple["subject"].lower())
451
+ obj_id = concept_ids.get(triple["object"].lower())
452
+ if subj_id and obj_id and subj_id != obj_id:
453
+ self._upsert_edge(
454
+ conn,
455
+ subj_id,
456
+ obj_id,
457
+ triple["relation"],
458
+ weight=0.9,
459
+ metadata={
460
+ "context": triple.get("context", "")[:240],
461
+ "source_id": source_id,
462
+ "workspace_id": workspace_id,
463
+ },
464
+ )
465
+
466
+ for item in _semantic_items(target_for_concepts):
467
+ sem_type = item["type"]
468
+ sem_title = item["title"]
469
+ sem_id = f"{sem_type.lower()}:{_sha256_text(f'{file_node_id}:{sem_type}:{sem_title}')[:24]}"
470
+ self._upsert_node(
471
+ conn,
472
+ sem_id,
473
+ sem_type,
474
+ sem_title,
475
+ summary=item["summary"],
476
+ metadata={
477
+ "auto_extracted": True,
478
+ "source_node": file_node_id,
479
+ "filename": file_path.name,
480
+ "workspace_id": workspace_id,
481
+ },
482
+ raw=item,
483
+ owner=user_email,
484
+ workspace_id=workspace_id,
485
+ )
486
+ self._upsert_edge(
487
+ conn,
488
+ file_node_id,
489
+ sem_id,
490
+ "포함함",
491
+ weight=0.9,
492
+ metadata={"source": "local_scan", "workspace_id": workspace_id},
493
+ )
494
+
495
+ return file_node_id
@@ -0,0 +1,42 @@
1
+ """Legacy ↔ v2 projection, the keyword index, and curation over the result.
2
+
3
+ Split into two cohesive submodules in v11.3.0 (no behaviour change):
4
+ ``v2_schema`` (the derived projection, its migration, and the FTS index) and
5
+ ``curation`` (topic promotion, the review queue, noise reduction).
6
+ :class:`KnowledgeGraphProjectionMixin` composes both under its original name, so
7
+ ``KnowledgeGraphStore``'s MRO and every method resolution are what they were,
8
+ and every name the single module exposed is re-exported here.
9
+ """
10
+
11
+ # The single module had no ``__all__``: its surface was "every module global",
12
+ # star-imported ``_kg_common`` vocabulary included. That is reproduced verbatim
13
+ # below, so the waiver the star import needs applies to this file too.
14
+ # ruff: noqa: F403,F405
15
+ from __future__ import annotations
16
+
17
+ from .._kg_common import * # noqa: F401
18
+
19
+ # Module-level names of the single file, re-exported with the redundant-alias
20
+ # form so each is marked deliberate rather than a leftover import.
21
+ #
22
+ # Stubbing note: rebinding one of these *here* changes only this module's name.
23
+ # ``curate`` reads ``_promotion_review_default`` as ``curation``'s own global,
24
+ # so a test standing in for it patches ``…graph.projection.curation``.
25
+ from .curation import _LAST_NOISE_CURATE_KEY as _LAST_NOISE_CURATE_KEY
26
+ from .curation import _PENDING_PROMOTIONS_CAP as _PENDING_PROMOTIONS_CAP
27
+ from .curation import _PENDING_PROMOTIONS_KEY as _PENDING_PROMOTIONS_KEY
28
+ from .curation import _PROMOTION_REVIEW_ENV as _PROMOTION_REVIEW_ENV
29
+ from .curation import KnowledgeGraphCurationMixin
30
+ from .curation import _promotion_review_default as _promotion_review_default
31
+ from .v2_schema import KnowledgeGraphV2SchemaMixin
32
+
33
+
34
+ class KnowledgeGraphProjectionMixin(
35
+ KnowledgeGraphV2SchemaMixin, KnowledgeGraphCurationMixin
36
+ ):
37
+ """Every projection and curation method, under the name the store mixes in.
38
+
39
+ Composition only: the two mixins own disjoint halves of what used to be one
40
+ class body, and this subclass adds nothing. ``KnowledgeGraphStore`` keeps
41
+ listing exactly this name among its bases.
42
+ """