ltcai 8.9.0 → 9.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.
- package/README.md +28 -39
- package/auto_setup.py +11 -62
- package/docs/CHANGELOG.md +76 -237
- package/docs/COMMUNITY_AND_PLUGINS.md +2 -2
- package/docs/DEVELOPMENT.md +8 -8
- package/docs/LEGACY_COMPATIBILITY.md +1 -1
- package/docs/ONBOARDING.md +2 -2
- package/docs/ROADMAP_RECOMMENDATIONS.md +61 -36
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +2 -2
- package/docs/kg-schema.md +1 -1
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/archive.py +4 -9
- package/lattice_brain/embeddings.py +38 -2
- package/lattice_brain/graph/_kg_common.py +27 -462
- package/lattice_brain/graph/_kg_constants.py +243 -0
- package/lattice_brain/graph/_kg_fsutil.py +297 -0
- package/lattice_brain/graph/discovery.py +0 -948
- package/lattice_brain/graph/discovery_index.py +972 -0
- package/lattice_brain/graph/retrieval.py +0 -570
- package/lattice_brain/graph/retrieval_docgen.py +210 -0
- package/lattice_brain/graph/retrieval_vector.py +460 -0
- package/lattice_brain/graph/store.py +6 -0
- package/lattice_brain/ingestion.py +68 -0
- package/lattice_brain/portability.py +1 -9
- package/lattice_brain/quality.py +98 -4
- package/lattice_brain/runtime/agent_runtime.py +166 -0
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/lattice_brain/utils.py +28 -0
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/chat.py +340 -407
- package/latticeai/api/chat_helpers.py +227 -0
- package/latticeai/api/computer_use.py +149 -31
- package/latticeai/api/marketplace.py +11 -0
- package/latticeai/api/permissions.py +3 -3
- package/latticeai/api/tools.py +1 -0
- package/latticeai/app_factory.py +82 -360
- package/latticeai/core/io_utils.py +37 -0
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/local_embeddings.py +2 -4
- package/latticeai/core/marketplace.py +33 -2
- package/latticeai/core/mcp_catalog.py +450 -0
- package/latticeai/core/mcp_registry.py +2 -441
- package/latticeai/core/users.py +4 -9
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/core/workspace_os_utils.py +3 -17
- package/latticeai/integrations/telegram_bot.py +7 -2
- package/latticeai/models/model_providers.py +111 -0
- package/latticeai/models/router.py +58 -136
- package/latticeai/runtime/audit_runtime.py +27 -16
- package/latticeai/runtime/automation_runtime.py +9 -0
- package/latticeai/runtime/history_runtime.py +163 -0
- package/latticeai/runtime/namespace_runtime.py +173 -0
- package/latticeai/runtime/network_config_runtime.py +56 -0
- package/latticeai/runtime/sso_config_runtime.py +128 -0
- package/latticeai/runtime/user_key_runtime.py +106 -0
- package/latticeai/services/architecture_readiness.py +2 -2
- package/latticeai/services/memory_service.py +213 -0
- package/latticeai/services/platform_runtime.py +9 -1
- package/latticeai/services/product_readiness.py +11 -11
- package/latticeai/services/review_queue.py +64 -11
- package/latticeai/services/run_executor.py +21 -0
- package/latticeai/services/setup_detection.py +80 -0
- package/latticeai/services/tool_dispatch.py +1 -1
- package/package.json +1 -1
- package/setup_wizard.py +11 -55
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +11 -11
- package/static/app/assets/{Act-fZokUnC0.js → Act-21lIXx2E.js} +1 -1
- package/static/app/assets/Brain-BqUd5UJJ.js +321 -0
- package/static/app/assets/{Capture-D5KV3Cu7.js → Capture-BA7Z2Q1u.js} +1 -1
- package/static/app/assets/{Library-C9kyFkSt.js → Library-bFMtyni3.js} +1 -1
- package/static/app/assets/System-K6krGCqn.js +1 -0
- package/static/app/assets/index-C4R3ws30.js +17 -0
- package/static/app/assets/index-ChSeOB02.css +2 -0
- package/static/app/assets/primitives-sQU3it5I.js +1 -0
- package/static/app/assets/{textarea-CD8UNKIy.js → textarea-DK3Fd_lR.js} +1 -1
- package/static/app/index.html +2 -2
- package/static/css/tokens.css +4 -2
- package/static/sw.js +1 -1
- package/tools/local_files.py +6 -0
- package/latticeai/runtime/sso_runtime.py +0 -52
- package/static/app/assets/Brain-DtyuWubr.js +0 -321
- package/static/app/assets/System-VbChmX7r.js +0 -1
- package/static/app/assets/index-DCh5AoXt.css +0 -2
- package/static/app/assets/index-DPdcPoF0.js +0 -17
- package/static/app/assets/primitives-DFeanEV6.js +0 -1
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Static constants for the SQLite knowledge graph.
|
|
2
|
+
|
|
3
|
+
Extracted verbatim from ``_kg_common`` so the grab-bag module keeps only logic.
|
|
4
|
+
These are pure literals (projection/format versions, the local-ingestion file
|
|
5
|
+
classification tables, and the OS exclusion lists). ``_kg_common`` re-exports
|
|
6
|
+
every name here, so existing ``from ._kg_common import <CONST>`` sites are
|
|
7
|
+
unaffected.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
# Bump when the v2 projection layout changes (columns, normalization rules).
|
|
13
|
+
# On init, a stale projection is dropped and rebuilt from the authoritative
|
|
14
|
+
# legacy tables — safe because nodes_v2/edges_v2 only ever hold a derived view.
|
|
15
|
+
# v4: summary nullable + verbatim (byte-faithful) projection of legacy values.
|
|
16
|
+
_PROJECTION_VERSION = 4
|
|
17
|
+
_KG_DB_FORMAT_VERSION = 4
|
|
18
|
+
_KG_DB_FORMAT_KEY = "db_format_version"
|
|
19
|
+
_V2_WRITE_MASTER_KEY = "v2_write_mastered_at"
|
|
20
|
+
|
|
21
|
+
GRAPH_SCHEMA_VERSION = 1
|
|
22
|
+
|
|
23
|
+
LOCAL_TEXT_EXTENSIONS = {".txt", ".md"}
|
|
24
|
+
LOCAL_CODE_EXTENSIONS = {
|
|
25
|
+
".py",
|
|
26
|
+
".js",
|
|
27
|
+
".ts",
|
|
28
|
+
".tsx",
|
|
29
|
+
".jsx",
|
|
30
|
+
".html",
|
|
31
|
+
".css",
|
|
32
|
+
".json",
|
|
33
|
+
".yaml",
|
|
34
|
+
".yml",
|
|
35
|
+
".xml",
|
|
36
|
+
".sql",
|
|
37
|
+
".sh",
|
|
38
|
+
".zsh",
|
|
39
|
+
".toml",
|
|
40
|
+
".ini",
|
|
41
|
+
}
|
|
42
|
+
LOCAL_DOCUMENT_EXTENSIONS = {".pdf", ".docx"}
|
|
43
|
+
LOCAL_SPREADSHEET_EXTENSIONS = {".xlsx", ".csv"}
|
|
44
|
+
LOCAL_SLIDE_EXTENSIONS = {".pptx"}
|
|
45
|
+
LOCAL_IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp"}
|
|
46
|
+
LOCAL_SUPPORTED_EXTENSIONS = (
|
|
47
|
+
LOCAL_TEXT_EXTENSIONS
|
|
48
|
+
| LOCAL_CODE_EXTENSIONS
|
|
49
|
+
| LOCAL_DOCUMENT_EXTENSIONS
|
|
50
|
+
| LOCAL_SPREADSHEET_EXTENSIONS
|
|
51
|
+
| LOCAL_SLIDE_EXTENSIONS
|
|
52
|
+
| LOCAL_IMAGE_EXTENSIONS
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
LOCAL_SIZE_LIMITS = {
|
|
56
|
+
"text": 4_000_000,
|
|
57
|
+
"code": 4_000_000,
|
|
58
|
+
"pdf": 50_000_000,
|
|
59
|
+
"document": 50_000_000,
|
|
60
|
+
"spreadsheet": 50_000_000,
|
|
61
|
+
"slide_deck": 50_000_000,
|
|
62
|
+
"image": 100_000_000,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
COMMON_EXCLUDED_DIRS = {
|
|
66
|
+
".git",
|
|
67
|
+
"node_modules",
|
|
68
|
+
".venv",
|
|
69
|
+
"venv",
|
|
70
|
+
"env",
|
|
71
|
+
"__pycache__",
|
|
72
|
+
".pytest_cache",
|
|
73
|
+
".mypy_cache",
|
|
74
|
+
".ruff_cache",
|
|
75
|
+
".next",
|
|
76
|
+
".nuxt",
|
|
77
|
+
".turbo",
|
|
78
|
+
"dist",
|
|
79
|
+
"build",
|
|
80
|
+
"target",
|
|
81
|
+
"out",
|
|
82
|
+
"coverage",
|
|
83
|
+
".cache",
|
|
84
|
+
".config",
|
|
85
|
+
".ssh",
|
|
86
|
+
".gnupg",
|
|
87
|
+
".docker",
|
|
88
|
+
".kube",
|
|
89
|
+
".aws",
|
|
90
|
+
".azure",
|
|
91
|
+
".npm",
|
|
92
|
+
".pnpm-store",
|
|
93
|
+
".yarn",
|
|
94
|
+
".bun",
|
|
95
|
+
".cargo",
|
|
96
|
+
".rustup",
|
|
97
|
+
".pyenv",
|
|
98
|
+
".conda",
|
|
99
|
+
".local",
|
|
100
|
+
".claude",
|
|
101
|
+
".codex",
|
|
102
|
+
".cursor",
|
|
103
|
+
".copilot",
|
|
104
|
+
".antigravity",
|
|
105
|
+
".antigravity-ide",
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
COMMON_EXCLUDED_FILE_NAMES = {
|
|
109
|
+
".env",
|
|
110
|
+
".env.local",
|
|
111
|
+
".env.production",
|
|
112
|
+
".env.development",
|
|
113
|
+
"id_rsa",
|
|
114
|
+
"id_ed25519",
|
|
115
|
+
"authorized_keys",
|
|
116
|
+
"known_hosts",
|
|
117
|
+
"credentials.json",
|
|
118
|
+
"service-account.json",
|
|
119
|
+
"token.json",
|
|
120
|
+
"secrets.json",
|
|
121
|
+
"cookies",
|
|
122
|
+
"login data",
|
|
123
|
+
"history",
|
|
124
|
+
"web data",
|
|
125
|
+
".ds_store",
|
|
126
|
+
"thumbs.db",
|
|
127
|
+
}
|
|
128
|
+
COMMON_EXCLUDED_FILE_SUFFIXES = {
|
|
129
|
+
".pem",
|
|
130
|
+
".key",
|
|
131
|
+
".p12",
|
|
132
|
+
".pfx",
|
|
133
|
+
".kdbx",
|
|
134
|
+
".wallet",
|
|
135
|
+
".sqlite",
|
|
136
|
+
".db",
|
|
137
|
+
".exe",
|
|
138
|
+
".dll",
|
|
139
|
+
".sys",
|
|
140
|
+
".msi",
|
|
141
|
+
".dmg",
|
|
142
|
+
".pkg",
|
|
143
|
+
".app",
|
|
144
|
+
".zip",
|
|
145
|
+
".tar",
|
|
146
|
+
".gz",
|
|
147
|
+
".7z",
|
|
148
|
+
".rar",
|
|
149
|
+
".mp4",
|
|
150
|
+
".mov",
|
|
151
|
+
".mp3",
|
|
152
|
+
".wav",
|
|
153
|
+
".tmp",
|
|
154
|
+
".bak",
|
|
155
|
+
".lock",
|
|
156
|
+
}
|
|
157
|
+
SENSITIVE_PATH_KEYWORDS = {
|
|
158
|
+
"secret",
|
|
159
|
+
"secrets",
|
|
160
|
+
"token",
|
|
161
|
+
"password",
|
|
162
|
+
"passwd",
|
|
163
|
+
"credential",
|
|
164
|
+
"credentials",
|
|
165
|
+
"private",
|
|
166
|
+
"key",
|
|
167
|
+
"wallet",
|
|
168
|
+
"recovery",
|
|
169
|
+
"seed",
|
|
170
|
+
"mnemonic",
|
|
171
|
+
"cookie",
|
|
172
|
+
"session",
|
|
173
|
+
"auth",
|
|
174
|
+
"oauth",
|
|
175
|
+
"certificate",
|
|
176
|
+
"cert",
|
|
177
|
+
"api_key",
|
|
178
|
+
"apikey",
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
MACOS_EXCLUDED_PREFIXES = (
|
|
182
|
+
"/System",
|
|
183
|
+
"/Library",
|
|
184
|
+
"/Applications",
|
|
185
|
+
"/private",
|
|
186
|
+
"/tmp",
|
|
187
|
+
"/var",
|
|
188
|
+
)
|
|
189
|
+
WINDOWS_EXCLUDED_NAMES = {
|
|
190
|
+
"windows",
|
|
191
|
+
"program files",
|
|
192
|
+
"program files (x86)",
|
|
193
|
+
"programdata",
|
|
194
|
+
"appdata",
|
|
195
|
+
"$recycle.bin",
|
|
196
|
+
"system volume information",
|
|
197
|
+
"recovery",
|
|
198
|
+
"perflogs",
|
|
199
|
+
"intel",
|
|
200
|
+
"amd",
|
|
201
|
+
"nvidia",
|
|
202
|
+
}
|
|
203
|
+
LINUX_EXCLUDED_PREFIXES = (
|
|
204
|
+
"/bin",
|
|
205
|
+
"/boot",
|
|
206
|
+
"/dev",
|
|
207
|
+
"/etc",
|
|
208
|
+
"/lib",
|
|
209
|
+
"/lib64",
|
|
210
|
+
"/proc",
|
|
211
|
+
"/root",
|
|
212
|
+
"/run",
|
|
213
|
+
"/sbin",
|
|
214
|
+
"/sys",
|
|
215
|
+
"/tmp",
|
|
216
|
+
"/usr",
|
|
217
|
+
"/var",
|
|
218
|
+
"/snap",
|
|
219
|
+
"/lost+found",
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
__all__ = [
|
|
223
|
+
"_PROJECTION_VERSION",
|
|
224
|
+
"_KG_DB_FORMAT_VERSION",
|
|
225
|
+
"_KG_DB_FORMAT_KEY",
|
|
226
|
+
"_V2_WRITE_MASTER_KEY",
|
|
227
|
+
"GRAPH_SCHEMA_VERSION",
|
|
228
|
+
"LOCAL_TEXT_EXTENSIONS",
|
|
229
|
+
"LOCAL_CODE_EXTENSIONS",
|
|
230
|
+
"LOCAL_DOCUMENT_EXTENSIONS",
|
|
231
|
+
"LOCAL_SPREADSHEET_EXTENSIONS",
|
|
232
|
+
"LOCAL_SLIDE_EXTENSIONS",
|
|
233
|
+
"LOCAL_IMAGE_EXTENSIONS",
|
|
234
|
+
"LOCAL_SUPPORTED_EXTENSIONS",
|
|
235
|
+
"LOCAL_SIZE_LIMITS",
|
|
236
|
+
"COMMON_EXCLUDED_DIRS",
|
|
237
|
+
"COMMON_EXCLUDED_FILE_NAMES",
|
|
238
|
+
"COMMON_EXCLUDED_FILE_SUFFIXES",
|
|
239
|
+
"SENSITIVE_PATH_KEYWORDS",
|
|
240
|
+
"MACOS_EXCLUDED_PREFIXES",
|
|
241
|
+
"WINDOWS_EXCLUDED_NAMES",
|
|
242
|
+
"LINUX_EXCLUDED_PREFIXES",
|
|
243
|
+
]
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""Pure filesystem / path / hash / file-classification helpers.
|
|
2
|
+
|
|
3
|
+
Split out of _kg_common so that module keeps only the text/NLP extraction
|
|
4
|
+
logic. These depend only on stdlib and the static tables in _kg_constants —
|
|
5
|
+
no dependency back on _kg_common — so the import graph stays linear
|
|
6
|
+
(_kg_constants ← _kg_fsutil ← _kg_common). _kg_common re-exports every name
|
|
7
|
+
here via ``from ._kg_fsutil import *`` (its computed __all__ then forwards
|
|
8
|
+
them to the graph mixins), so existing call sites are unaffected.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
# ruff: noqa: F403,F405
|
|
14
|
+
|
|
15
|
+
import hashlib
|
|
16
|
+
import math
|
|
17
|
+
import os
|
|
18
|
+
import platform
|
|
19
|
+
import re
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
23
|
+
|
|
24
|
+
from ._kg_constants import * # noqa: F401,F403
|
|
25
|
+
from ..utils import parse_iso as _parse_iso
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _now() -> str:
|
|
29
|
+
return datetime.now().isoformat()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _recency_score(
|
|
33
|
+
updated_at: Optional[str],
|
|
34
|
+
*,
|
|
35
|
+
now: Optional[datetime] = None,
|
|
36
|
+
half_life_days: float = 14.0,
|
|
37
|
+
) -> float:
|
|
38
|
+
stamp = _parse_iso(updated_at)
|
|
39
|
+
if not stamp:
|
|
40
|
+
return 0.0
|
|
41
|
+
now = now or datetime.now()
|
|
42
|
+
age_days = max(0.0, (now - stamp).total_seconds() / 86400.0)
|
|
43
|
+
decay = math.log(2) / max(0.1, half_life_days)
|
|
44
|
+
return math.exp(-decay * age_days)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _slug(text: str, max_len: int = 96) -> str:
|
|
48
|
+
value = re.sub(r"\s+", " ", str(text or "")).strip().lower()
|
|
49
|
+
value = re.sub(r"[^0-9a-zA-Z가-힣._:@/-]+", "-", value).strip("-")
|
|
50
|
+
return (value or "untitled")[:max_len]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _sha256_bytes(data: bytes) -> str:
|
|
54
|
+
return hashlib.sha256(data).hexdigest()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _sha256_text(text: str) -> str:
|
|
58
|
+
return hashlib.sha256(text.encode("utf-8", errors="replace")).hexdigest()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _safe_iso_from_stat_mtime(mtime: float) -> str:
|
|
62
|
+
try:
|
|
63
|
+
return datetime.fromtimestamp(float(mtime)).isoformat()
|
|
64
|
+
except (TypeError, ValueError, OSError):
|
|
65
|
+
return ""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _path_fingerprint(path: Path) -> str:
|
|
69
|
+
return _sha256_text(str(path.expanduser().resolve()))[:24]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _is_relative_to(path: Path, base: Path) -> bool:
|
|
73
|
+
try:
|
|
74
|
+
path.relative_to(base)
|
|
75
|
+
return True
|
|
76
|
+
except ValueError:
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _path_parts_lower(path: Path) -> List[str]:
|
|
81
|
+
return [
|
|
82
|
+
part.lower()
|
|
83
|
+
for part in path.parts
|
|
84
|
+
if part and part not in {os.sep, path.anchor}
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _current_os_type() -> str:
|
|
89
|
+
system = platform.system().lower()
|
|
90
|
+
if system.startswith("darwin"):
|
|
91
|
+
return "macos"
|
|
92
|
+
if system.startswith("windows"):
|
|
93
|
+
return "windows"
|
|
94
|
+
if system.startswith("linux"):
|
|
95
|
+
return "linux"
|
|
96
|
+
return system or "unknown"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _drive_id_for_path(path: Path) -> str:
|
|
100
|
+
resolved = path.expanduser().resolve()
|
|
101
|
+
if resolved.drive:
|
|
102
|
+
return resolved.drive.upper()
|
|
103
|
+
parts = resolved.parts
|
|
104
|
+
if len(parts) >= 3 and parts[1] == "Volumes":
|
|
105
|
+
return f"/Volumes/{parts[2]}"
|
|
106
|
+
if len(parts) >= 3 and parts[1] == "media":
|
|
107
|
+
return f"/media/{parts[2]}"
|
|
108
|
+
if len(parts) >= 3 and parts[1] == "mnt":
|
|
109
|
+
return f"/mnt/{parts[2]}"
|
|
110
|
+
return resolved.anchor or "/"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _file_category(ext: str) -> str:
|
|
114
|
+
ext = (ext or "").lower()
|
|
115
|
+
if ext in LOCAL_CODE_EXTENSIONS:
|
|
116
|
+
return "code"
|
|
117
|
+
if ext in LOCAL_TEXT_EXTENSIONS:
|
|
118
|
+
return "text"
|
|
119
|
+
if ext == ".pdf":
|
|
120
|
+
return "pdf"
|
|
121
|
+
if ext in LOCAL_DOCUMENT_EXTENSIONS:
|
|
122
|
+
return "document"
|
|
123
|
+
if ext in LOCAL_SPREADSHEET_EXTENSIONS:
|
|
124
|
+
return "spreadsheet"
|
|
125
|
+
if ext in LOCAL_SLIDE_EXTENSIONS:
|
|
126
|
+
return "slide_deck"
|
|
127
|
+
if ext in LOCAL_IMAGE_EXTENSIONS:
|
|
128
|
+
return "image"
|
|
129
|
+
return "unsupported"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _node_type_for_category(category: str) -> str:
|
|
133
|
+
return {
|
|
134
|
+
"code": "CodeFile",
|
|
135
|
+
"spreadsheet": "Spreadsheet",
|
|
136
|
+
"slide_deck": "SlideDeck",
|
|
137
|
+
"image": "Image",
|
|
138
|
+
"unsupported": "File",
|
|
139
|
+
}.get(category, "Document")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _parser_type_for_category(category: str, ext: str) -> str:
|
|
143
|
+
if category in {"text", "code"}:
|
|
144
|
+
return "plain_text"
|
|
145
|
+
if category == "spreadsheet" and ext == ".csv":
|
|
146
|
+
return "csv_text"
|
|
147
|
+
if category == "image":
|
|
148
|
+
return "image_ocr"
|
|
149
|
+
return ext.lstrip(".") or category
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _size_limit_for_category(category: str) -> int:
|
|
153
|
+
return LOCAL_SIZE_LIMITS.get(category, LOCAL_SIZE_LIMITS["document"])
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _is_hidden_path(path: Path, root: Optional[Path] = None) -> bool:
|
|
157
|
+
parts: Iterable[str]
|
|
158
|
+
if root is not None:
|
|
159
|
+
try:
|
|
160
|
+
parts = path.relative_to(root).parts
|
|
161
|
+
except ValueError:
|
|
162
|
+
parts = path.parts
|
|
163
|
+
else:
|
|
164
|
+
parts = path.parts
|
|
165
|
+
return any(part.startswith(".") and part not in {".", ".."} for part in parts)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _excluded_directory_reason(
|
|
169
|
+
path: Path, *, root: Optional[Path] = None, os_type: Optional[str] = None
|
|
170
|
+
) -> Optional[str]:
|
|
171
|
+
os_type = os_type or _current_os_type()
|
|
172
|
+
name = path.name.lower()
|
|
173
|
+
if name in COMMON_EXCLUDED_DIRS:
|
|
174
|
+
return "excluded_folder"
|
|
175
|
+
if _is_hidden_path(path, root):
|
|
176
|
+
return "hidden_folder"
|
|
177
|
+
parts = _path_parts_lower(path)
|
|
178
|
+
if os_type == "windows" and any(part in WINDOWS_EXCLUDED_NAMES for part in parts):
|
|
179
|
+
return "system_folder"
|
|
180
|
+
normalized = path.as_posix()
|
|
181
|
+
root_normalized = root.as_posix() if root else ""
|
|
182
|
+
|
|
183
|
+
def _prefix_blocks(prefixes: Tuple[str, ...]) -> bool:
|
|
184
|
+
for prefix in prefixes:
|
|
185
|
+
path_under_prefix = normalized == prefix or normalized.startswith(
|
|
186
|
+
f"{prefix}/"
|
|
187
|
+
)
|
|
188
|
+
root_under_prefix = bool(root_normalized) and (
|
|
189
|
+
root_normalized == prefix or root_normalized.startswith(f"{prefix}/")
|
|
190
|
+
)
|
|
191
|
+
if path_under_prefix and not root_under_prefix:
|
|
192
|
+
return True
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
if os_type == "macos":
|
|
196
|
+
home_library = Path.home() / "Library"
|
|
197
|
+
try:
|
|
198
|
+
root_is_library = bool(root) and _is_relative_to(
|
|
199
|
+
root.expanduser().resolve(), home_library.expanduser().resolve()
|
|
200
|
+
)
|
|
201
|
+
if (
|
|
202
|
+
_is_relative_to(
|
|
203
|
+
path.expanduser().resolve(), home_library.expanduser().resolve()
|
|
204
|
+
)
|
|
205
|
+
and not root_is_library
|
|
206
|
+
):
|
|
207
|
+
return "user_library"
|
|
208
|
+
except OSError:
|
|
209
|
+
pass
|
|
210
|
+
if _prefix_blocks(MACOS_EXCLUDED_PREFIXES):
|
|
211
|
+
return "system_folder"
|
|
212
|
+
if os_type == "linux":
|
|
213
|
+
if _prefix_blocks(LINUX_EXCLUDED_PREFIXES):
|
|
214
|
+
return "system_folder"
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _sensitive_file_reason(path: Path, *, root: Optional[Path] = None) -> Optional[str]:
|
|
219
|
+
name = path.name.lower()
|
|
220
|
+
suffix = path.suffix.lower()
|
|
221
|
+
if name in COMMON_EXCLUDED_FILE_NAMES or suffix in COMMON_EXCLUDED_FILE_SUFFIXES:
|
|
222
|
+
return "sensitive_or_excluded_file"
|
|
223
|
+
try:
|
|
224
|
+
rel_text = (
|
|
225
|
+
path.relative_to(root).as_posix().lower()
|
|
226
|
+
if root
|
|
227
|
+
else path.as_posix().lower()
|
|
228
|
+
)
|
|
229
|
+
except ValueError:
|
|
230
|
+
rel_text = path.as_posix().lower()
|
|
231
|
+
tokens = re.split(r"[^0-9a-zA-Z_가-힣]+", rel_text)
|
|
232
|
+
if any(token in SENSITIVE_PATH_KEYWORDS for token in tokens):
|
|
233
|
+
return "sensitive_name"
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _root_warning(path: Path, os_type: str) -> Optional[str]:
|
|
238
|
+
resolved = path.expanduser().resolve()
|
|
239
|
+
home = Path.home().expanduser().resolve()
|
|
240
|
+
if os_type == "macos" and resolved == home:
|
|
241
|
+
return "홈 전체에는 설정/숨김 폴더가 포함될 수 있습니다. 문서, 데스크탑, 다운로드, 프로젝트 폴더부터 추가하는 것을 권장합니다."
|
|
242
|
+
if os_type == "linux" and resolved.as_posix() == "/":
|
|
243
|
+
return "루트 디렉터리에는 시스템 파일이 포함되어 있습니다. 일반 사용자 폴더나 마운트된 데이터 폴더를 권장합니다."
|
|
244
|
+
if os_type == "windows" and str(resolved).rstrip("\\/").upper() in {"C:", "C:\\"}:
|
|
245
|
+
return "C드라이브에는 Windows 시스템 파일과 앱 설정 파일이 포함되어 있습니다. 하위 폴더를 선택하는 것을 권장합니다."
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _sample_file(
|
|
250
|
+
path: Path, root: Path, status: str, reason: str = ""
|
|
251
|
+
) -> Dict[str, Any]:
|
|
252
|
+
try:
|
|
253
|
+
rel = path.relative_to(root).as_posix()
|
|
254
|
+
except ValueError:
|
|
255
|
+
rel = path.name
|
|
256
|
+
try:
|
|
257
|
+
stat = path.stat()
|
|
258
|
+
size = stat.st_size if path.is_file() else None
|
|
259
|
+
modified_at = _safe_iso_from_stat_mtime(stat.st_mtime)
|
|
260
|
+
except OSError:
|
|
261
|
+
size = None
|
|
262
|
+
modified_at = ""
|
|
263
|
+
return {
|
|
264
|
+
"path": str(path),
|
|
265
|
+
"relative_path": rel,
|
|
266
|
+
"name": path.name,
|
|
267
|
+
"extension": path.suffix.lower(),
|
|
268
|
+
"status": status,
|
|
269
|
+
"reason": reason,
|
|
270
|
+
"size_bytes": size,
|
|
271
|
+
"modified_at": modified_at,
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
__all__ = [
|
|
276
|
+
"_now",
|
|
277
|
+
"_parse_iso",
|
|
278
|
+
"_recency_score",
|
|
279
|
+
"_slug",
|
|
280
|
+
"_sha256_bytes",
|
|
281
|
+
"_sha256_text",
|
|
282
|
+
"_safe_iso_from_stat_mtime",
|
|
283
|
+
"_path_fingerprint",
|
|
284
|
+
"_is_relative_to",
|
|
285
|
+
"_path_parts_lower",
|
|
286
|
+
"_current_os_type",
|
|
287
|
+
"_drive_id_for_path",
|
|
288
|
+
"_file_category",
|
|
289
|
+
"_node_type_for_category",
|
|
290
|
+
"_parser_type_for_category",
|
|
291
|
+
"_size_limit_for_category",
|
|
292
|
+
"_is_hidden_path",
|
|
293
|
+
"_excluded_directory_reason",
|
|
294
|
+
"_sensitive_file_reason",
|
|
295
|
+
"_root_warning",
|
|
296
|
+
"_sample_file",
|
|
297
|
+
]
|