ltcai 11.1.0 → 11.4.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 (289) hide show
  1. package/README.md +47 -54
  2. package/docs/CHANGELOG.md +94 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/FEATURE_AUDIT_v11.2.0.md +393 -0
  6. package/docs/LAYOUT_REBUILD_SPEC.md +9 -1
  7. package/docs/MULTI_AGENT_RUNTIME.md +1 -1
  8. package/docs/ONBOARDING.md +1 -1
  9. package/docs/OPERATIONS.md +6 -2
  10. package/docs/PERMISSION_MODE.md +1 -1
  11. package/docs/TRUST_MODEL.md +1 -1
  12. package/docs/WHY_LATTICE.md +1 -1
  13. package/docs/architecture.md +6 -2
  14. package/docs/kg-schema.md +2 -2
  15. package/docs/v11.3.0_PLAN.md +202 -0
  16. package/docs/v11.4.0_RUST_FOUNDATION_PLAN.md +176 -0
  17. package/lattice_brain/__init__.py +1 -1
  18. package/lattice_brain/gates.py +125 -0
  19. package/lattice_brain/graph/_kg_common/__init__.py +287 -0
  20. package/lattice_brain/graph/_kg_common/extraction.py +516 -0
  21. package/lattice_brain/graph/_kg_common/relations.py +161 -0
  22. package/lattice_brain/graph/_kg_common/text.py +479 -0
  23. package/lattice_brain/graph/discovery_index/__init__.py +35 -0
  24. package/lattice_brain/graph/discovery_index/cleanup.py +182 -0
  25. package/lattice_brain/graph/discovery_index/extract.py +137 -0
  26. package/lattice_brain/graph/discovery_index/scan.py +411 -0
  27. package/lattice_brain/graph/discovery_index/upsert.py +495 -0
  28. package/lattice_brain/graph/fusion.py +35 -4
  29. package/lattice_brain/graph/projection/__init__.py +42 -0
  30. package/lattice_brain/graph/projection/curation.py +500 -0
  31. package/lattice_brain/graph/{projection.py → projection/v2_schema.py} +81 -485
  32. package/lattice_brain/graph/retrieval/__init__.py +54 -0
  33. package/lattice_brain/graph/retrieval/context.py +197 -0
  34. package/lattice_brain/graph/retrieval/graph_view.py +319 -0
  35. package/lattice_brain/graph/retrieval/hybrid.py +488 -0
  36. package/lattice_brain/graph/retrieval/maintenance.py +121 -0
  37. package/lattice_brain/graph/retrieval/signals.py +95 -0
  38. package/lattice_brain/graph/retrieval_vector/__init__.py +42 -0
  39. package/lattice_brain/graph/retrieval_vector/fingerprint.py +97 -0
  40. package/lattice_brain/graph/retrieval_vector/indexing.py +347 -0
  41. package/lattice_brain/graph/retrieval_vector/search.py +560 -0
  42. package/lattice_brain/graph/retrieval_vector/status.py +374 -0
  43. package/lattice_brain/graph/schema.py +9 -0
  44. package/lattice_brain/graph/store.py +9 -0
  45. package/lattice_brain/graph/vector_index/selector.py +32 -2
  46. package/lattice_brain/ingestion/__init__.py +130 -0
  47. package/lattice_brain/ingestion/_contract.py +90 -0
  48. package/lattice_brain/ingestion/constants.py +127 -0
  49. package/lattice_brain/ingestion/folder_scan.py +57 -0
  50. package/lattice_brain/ingestion/folders.py +258 -0
  51. package/lattice_brain/ingestion/hashing.py +26 -0
  52. package/lattice_brain/ingestion/jobs_api.py +107 -0
  53. package/lattice_brain/ingestion/models.py +80 -0
  54. package/lattice_brain/ingestion/pipeline.py +486 -0
  55. package/lattice_brain/ingestion/quality.py +209 -0
  56. package/lattice_brain/ingestion/routing.py +295 -0
  57. package/lattice_brain/multimodal/__init__.py +164 -0
  58. package/lattice_brain/multimodal/audio.py +77 -0
  59. package/lattice_brain/multimodal/common.py +118 -0
  60. package/lattice_brain/{multimodal.py → multimodal/images.py} +22 -262
  61. package/lattice_brain/multimodal/ports.py +169 -0
  62. package/lattice_brain/multimodal/video.py +410 -0
  63. package/lattice_brain/portability/__init__.py +90 -0
  64. package/lattice_brain/portability/_contract.py +42 -0
  65. package/lattice_brain/portability/backups.py +338 -0
  66. package/lattice_brain/portability/bundles.py +136 -0
  67. package/lattice_brain/portability/constants.py +93 -0
  68. package/lattice_brain/portability/fsops.py +138 -0
  69. package/lattice_brain/portability/service.py +41 -0
  70. package/lattice_brain/portability/sharing.py +714 -0
  71. package/lattice_brain/runtime/__init__.py +1 -1
  72. package/lattice_brain/runtime/multi_agent.py +1 -1
  73. package/lattice_brain/sealed_box.py +244 -0
  74. package/lattice_brain/synthesis.py +24 -1
  75. package/latticeai/__init__.py +1 -1
  76. package/latticeai/api/brain_intelligence.py +4 -0
  77. package/latticeai/api/chat.py +11 -0
  78. package/latticeai/api/chat_helpers.py +16 -3
  79. package/latticeai/api/chat_hybrid.py +32 -1
  80. package/latticeai/api/chronicle.py +63 -0
  81. package/latticeai/api/features.py +70 -0
  82. package/latticeai/api/local_files.py +102 -0
  83. package/latticeai/api/portability.py +39 -4
  84. package/latticeai/api/review_queue.py +126 -0
  85. package/latticeai/api/search.py +16 -2
  86. package/latticeai/core/agent/__init__.py +93 -0
  87. package/latticeai/core/agent/_contract.py +79 -0
  88. package/latticeai/core/agent/context.py +57 -0
  89. package/latticeai/core/agent/deps.py +125 -0
  90. package/latticeai/core/agent/execution.py +622 -0
  91. package/latticeai/core/agent/planning.py +145 -0
  92. package/latticeai/core/agent/recovery.py +157 -0
  93. package/latticeai/core/agent/runtime.py +210 -0
  94. package/latticeai/core/agent/verification.py +231 -0
  95. package/latticeai/core/config.py +4 -1
  96. package/latticeai/core/context_builder.py +6 -3
  97. package/latticeai/core/embedding_providers/__init__.py +151 -0
  98. package/latticeai/core/embedding_providers/base.py +199 -0
  99. package/latticeai/core/embedding_providers/captions.py +162 -0
  100. package/latticeai/core/embedding_providers/profiles.py +126 -0
  101. package/latticeai/core/embedding_providers/text.py +350 -0
  102. package/latticeai/core/embedding_providers/vision.py +352 -0
  103. package/latticeai/core/file_generation/__init__.py +115 -0
  104. package/latticeai/core/file_generation/bundles.py +76 -0
  105. package/latticeai/core/file_generation/extraction.py +154 -0
  106. package/latticeai/core/file_generation/inference.py +235 -0
  107. package/latticeai/core/file_generation/orchestration.py +152 -0
  108. package/latticeai/core/file_generation/prompting.py +117 -0
  109. package/latticeai/core/file_generation/repair.py +114 -0
  110. package/latticeai/core/file_generation/sanitize.py +61 -0
  111. package/latticeai/core/file_generation/validation.py +201 -0
  112. package/latticeai/core/legacy_compatibility.py +1 -1
  113. package/latticeai/core/marketplace.py +1 -1
  114. package/latticeai/core/messages.py +152 -0
  115. package/latticeai/core/model_compat.py +73 -2
  116. package/latticeai/core/workspace_os_constants.py +1 -1
  117. package/latticeai/integrations/telegram_bot/__init__.py +123 -0
  118. package/latticeai/integrations/telegram_bot/__main__.py +17 -0
  119. package/latticeai/integrations/telegram_bot/config.py +86 -0
  120. package/latticeai/integrations/telegram_bot/dispatch.py +311 -0
  121. package/latticeai/integrations/telegram_bot/flows.py +478 -0
  122. package/latticeai/integrations/telegram_bot/helpers.py +322 -0
  123. package/latticeai/integrations/telegram_bot/screens.py +394 -0
  124. package/latticeai/models/model_providers.py +12 -4
  125. package/latticeai/models/router/__init__.py +88 -0
  126. package/latticeai/models/router/_contract.py +66 -0
  127. package/latticeai/models/router/branding.py +56 -0
  128. package/latticeai/models/router/catalog.py +69 -0
  129. package/latticeai/models/router/documents.py +199 -0
  130. package/latticeai/models/router/errors.py +37 -0
  131. package/latticeai/models/router/generation.py +258 -0
  132. package/latticeai/models/router/loading.py +291 -0
  133. package/latticeai/models/router/local_models.py +85 -0
  134. package/latticeai/models/router/registry.py +147 -0
  135. package/latticeai/runtime/build_phases/__init__.py +82 -0
  136. package/latticeai/runtime/build_phases/features.py +407 -0
  137. package/latticeai/runtime/build_phases/foundation.py +555 -0
  138. package/latticeai/runtime/build_phases/web.py +492 -0
  139. package/latticeai/runtime/chat_wiring.py +4 -0
  140. package/latticeai/runtime/feature_toggle_wiring.py +163 -0
  141. package/latticeai/runtime/router_registration.py +11 -0
  142. package/latticeai/runtime/runtime_context.py +1 -0
  143. package/latticeai/services/app_context.py +8 -0
  144. package/latticeai/services/architecture_readiness.py +48 -19
  145. package/latticeai/services/automation_intelligence.py +22 -2
  146. package/latticeai/services/brain_intelligence/__init__.py +58 -0
  147. package/latticeai/services/brain_intelligence/_contract.py +71 -0
  148. package/latticeai/services/brain_intelligence/consistency.py +193 -0
  149. package/latticeai/services/brain_intelligence/constants.py +47 -0
  150. package/latticeai/services/brain_intelligence/digest.py +258 -0
  151. package/latticeai/services/brain_intelligence/health.py +331 -0
  152. package/latticeai/services/brain_intelligence/proposals.py +264 -0
  153. package/latticeai/services/brain_intelligence/sampling.py +84 -0
  154. package/latticeai/services/brain_intelligence/service.py +48 -0
  155. package/latticeai/services/chronicle.py +557 -0
  156. package/latticeai/services/command_center.py +10 -4
  157. package/latticeai/services/feature_toggles.py +502 -0
  158. package/latticeai/services/folder_watch.py +122 -1
  159. package/latticeai/services/hybrid_chat.py +56 -5
  160. package/latticeai/services/interop_bridges.py +978 -0
  161. package/latticeai/services/memory_service/__init__.py +52 -0
  162. package/latticeai/services/memory_service/_contract.py +100 -0
  163. package/latticeai/services/memory_service/brief.py +431 -0
  164. package/latticeai/services/memory_service/constants.py +57 -0
  165. package/latticeai/services/memory_service/maintenance.py +138 -0
  166. package/latticeai/services/memory_service/manager.py +186 -0
  167. package/latticeai/services/memory_service/proof.py +136 -0
  168. package/latticeai/services/memory_service/recall.py +225 -0
  169. package/latticeai/services/memory_service/service.py +48 -0
  170. package/latticeai/services/memory_service/stores.py +110 -0
  171. package/latticeai/services/model_capability_registry.py +434 -261
  172. package/latticeai/services/model_catalog.py +95 -61
  173. package/latticeai/services/model_recommendation.py +18 -11
  174. package/latticeai/services/model_runtime/__init__.py +322 -0
  175. package/latticeai/services/model_runtime/cloud.py +87 -0
  176. package/latticeai/services/model_runtime/download.py +282 -0
  177. package/latticeai/services/model_runtime/engines.py +341 -0
  178. package/latticeai/services/model_runtime/loading.py +178 -0
  179. package/latticeai/services/model_runtime/service.py +129 -0
  180. package/latticeai/services/model_runtime/state.py +131 -0
  181. package/latticeai/services/model_runtime/status.py +255 -0
  182. package/latticeai/services/multimodal_ports.py +26 -1
  183. package/latticeai/services/obsidian_bridge.py +16 -25
  184. package/latticeai/services/product_readiness.py +15 -7
  185. package/latticeai/services/search_service.py +149 -2
  186. package/latticeai/services/tool_dispatch.py +4 -0
  187. package/latticeai/setup/auto_setup.py +27 -30
  188. package/latticeai/setup/wizard/__init__.py +126 -0
  189. package/latticeai/setup/wizard/catalog.py +172 -0
  190. package/latticeai/setup/wizard/detect.py +323 -0
  191. package/latticeai/setup/wizard/install.py +348 -0
  192. package/latticeai/setup/wizard/paths.py +168 -0
  193. package/latticeai/setup/wizard/plans.py +74 -0
  194. package/latticeai/setup/wizard/recommend.py +320 -0
  195. package/package.json +6 -2
  196. package/scripts/bump_version.py +14 -0
  197. package/scripts/capture_release_evidence.mjs +33 -21
  198. package/scripts/check_current_release_docs.mjs +1 -1
  199. package/scripts/check_i18n_namespace_coverage.mjs +41 -4
  200. package/scripts/check_max_file_lines.mjs +102 -0
  201. package/scripts/check_release_evidence_bound.mjs +30 -15
  202. package/scripts/check_screenshot_pixel_delta.py +34 -4
  203. package/scripts/check_server_i18n.mjs +2 -0
  204. package/scripts/generate_rust_parity_fixtures.py +562 -0
  205. package/scripts/lib/mock_server_fingerprint.mjs +94 -0
  206. package/scripts/release_screen_claims.json +44 -2
  207. package/scripts/verify_hf_model_registry.py +253 -218
  208. package/src-tauri/Cargo.lock +361 -3
  209. package/src-tauri/Cargo.toml +6 -1
  210. package/src-tauri/src/backend.rs +349 -0
  211. package/src-tauri/src/folder.rs +33 -0
  212. package/src-tauri/src/main.rs +97 -399
  213. package/src-tauri/tauri.conf.json +1 -1
  214. package/static/app/asset-manifest.json +41 -37
  215. package/static/app/assets/Act-yYpYnn0v.js +1 -0
  216. package/static/app/assets/AdminConsole-DL3Cr5pL.js +1 -0
  217. package/static/app/assets/{Brain-CzCsI1mi.js → Brain-C1HBN0Wf.js} +2 -2
  218. package/static/app/assets/BrainHome-DoXRhUUC.js +2 -0
  219. package/static/app/assets/BrainSignals-6yR6ir5t.js +1 -0
  220. package/static/app/assets/Capture-CFIRsFNE.js +1 -0
  221. package/static/app/assets/Chronicle-BZbEgiwN.js +1 -0
  222. package/static/app/assets/CommandPalette-D2pMxC2I.js +1 -0
  223. package/static/app/assets/Library-DwO3yZST.js +1 -0
  224. package/static/app/assets/{LivingBrain-BXMWIK_2.js → LivingBrain-Jn1GK0-S.js} +1 -1
  225. package/static/app/assets/ProductFlow-B-w1R4Oo.js +1 -0
  226. package/static/app/assets/ReviewCard-6B27X8Vg.js +3 -0
  227. package/static/app/assets/System-DW8F-2xL.js +1 -0
  228. package/static/app/assets/arrow-left-DXvKg9U6.js +1 -0
  229. package/static/app/assets/{bot-4BvN07ux.js → bot-IM_E_Y12.js} +1 -1
  230. package/static/app/assets/brain-Ci1CkWjM.js +1 -0
  231. package/static/app/assets/{button-CDjtnAoU.js → button-COwyqfHM.js} +1 -1
  232. package/static/app/assets/circle-check-DfInj-qD.js +1 -0
  233. package/static/app/assets/{circle-pause-D_RMn7tp.js → circle-pause-DEM4A1Y5.js} +1 -1
  234. package/static/app/assets/{circle-play-B5OpB8ae.js → circle-play-C9djDuLd.js} +1 -1
  235. package/static/app/assets/{cpu-BIlWInHf.js → cpu-DFdo1gw-.js} +1 -1
  236. package/static/app/assets/{download-BtjXfL3z.js → download-SnJL6oqk.js} +1 -1
  237. package/static/app/assets/{folder-open-DefMpxI2.js → folder-open-CqZeDkjE.js} +1 -1
  238. package/static/app/assets/{hard-drive-BQ8NZVkw.js → hard-drive-j1jJXYYf.js} +1 -1
  239. package/static/app/assets/{index-vtEfYvQY.css → index-BLPb5lmE.css} +1 -1
  240. package/static/app/assets/index-_u5iUHDr.js +10 -0
  241. package/static/app/assets/input-B0lPdRQZ.js +1 -0
  242. package/static/app/assets/link-2-CoFbooHS.js +1 -0
  243. package/static/app/assets/{permissionCopy-BqZ5tsgL.js → permissionCopy-BsyLxtao.js} +1 -1
  244. package/static/app/assets/primitives-DEbN-d6p.js +1 -0
  245. package/static/app/assets/search-BybIWPNd.js +1 -0
  246. package/static/app/assets/{share-2-D5zg_0fY.js → share-2-CVtZ_ewX.js} +1 -1
  247. package/static/app/assets/{shield-alert-B5pZzkUb.js → shield-alert-CBi2GNWM.js} +1 -1
  248. package/static/app/assets/{textarea-nEVIweKY.js → textarea-DNMpB5ih.js} +1 -1
  249. package/static/app/assets/{useFocusTrap-Cm99AHlz.js → useFocusTrap-C83t3GXF.js} +1 -1
  250. package/static/app/assets/useMutation-DtbJDoyz.js +1 -0
  251. package/static/app/assets/{useQuery-Dm__N6bL.js → useQuery-Dcp1OChy.js} +1 -1
  252. package/static/app/assets/utils-BlZr7Pd4.js +4 -0
  253. package/static/app/assets/workspace-jJY4RuAV.js +1 -0
  254. package/static/app/index.html +4 -4
  255. package/static/sw.js +1 -1
  256. package/lattice_brain/graph/_kg_common.py +0 -1331
  257. package/lattice_brain/graph/discovery_index.py +0 -1141
  258. package/lattice_brain/graph/retrieval.py +0 -1120
  259. package/lattice_brain/graph/retrieval_vector.py +0 -1293
  260. package/lattice_brain/ingestion.py +0 -1377
  261. package/lattice_brain/portability.py +0 -1210
  262. package/latticeai/core/agent.py +0 -1412
  263. package/latticeai/core/embedding_providers.py +0 -1196
  264. package/latticeai/core/file_generation.py +0 -1047
  265. package/latticeai/integrations/telegram_bot.py +0 -1390
  266. package/latticeai/models/router.py +0 -1007
  267. package/latticeai/runtime/build_phases.py +0 -1422
  268. package/latticeai/services/brain_intelligence.py +0 -967
  269. package/latticeai/services/memory_service.py +0 -1177
  270. package/latticeai/services/model_runtime.py +0 -1281
  271. package/latticeai/setup/wizard.py +0 -1277
  272. package/static/app/assets/Act-D0HWqtn0.js +0 -1
  273. package/static/app/assets/AdminConsole-D-QDW-A4.js +0 -1
  274. package/static/app/assets/BrainHome-Btns-_TA.js +0 -2
  275. package/static/app/assets/BrainSignals-2dHQNkns.js +0 -1
  276. package/static/app/assets/Capture-CT8v1StE.js +0 -1
  277. package/static/app/assets/CommandPalette-DoLXC2KH.js +0 -1
  278. package/static/app/assets/Library-DDoxFE5c.js +0 -1
  279. package/static/app/assets/ProductFlow-DOYf7JIs.js +0 -1
  280. package/static/app/assets/ReviewCard-COQsqidK.js +0 -3
  281. package/static/app/assets/System-BRllvYXd.js +0 -1
  282. package/static/app/assets/arrow-left-DnyMzss-.js +0 -1
  283. package/static/app/assets/brain-uMb_5hnO.js +0 -1
  284. package/static/app/assets/index-0AvoEBzJ.js +0 -10
  285. package/static/app/assets/input-B_5ZJ9oy.js +0 -1
  286. package/static/app/assets/primitives-CVwew78r.js +0 -1
  287. package/static/app/assets/search-DkhnOKZt.js +0 -1
  288. package/static/app/assets/utils-DcDMoZIe.js +0 -4
  289. package/static/app/assets/workspace-LtRRSKTf.js +0 -1
@@ -0,0 +1,320 @@
1
+ """The wizard's recommendation: environment scan in, tailored checklist out.
2
+
3
+ Pure function over the :func:`~latticeai.setup.wizard.detect.scan_environment`
4
+ payload — no probing, no installing. It picks the preferred engine, filters the
5
+ catalogue to what fits, and returns the component / engine / model / MCP groups
6
+ the setup screen renders, each already carrying a confirmation-token plan.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import sys
12
+ from typing import Any, Dict, List
13
+
14
+ from latticeai.setup.wizard.catalog import (
15
+ _CROSS_PLATFORM_MODEL_CATALOG,
16
+ _MODEL_CATALOG,
17
+ _best_model_for_engine,
18
+ _filter_lower_family_versions,
19
+ )
20
+ from latticeai.setup.wizard.paths import OFFICIAL_DOWNLOADS
21
+ from latticeai.setup.wizard.plans import _hydrate_install_actions
22
+
23
+ # ── Recommendation Logic ──────────────────────────────────────────────────────
24
+
25
+ def get_recommendations(env: Dict[str, Any]) -> Dict[str, Any]:
26
+ ram = env["ram_gb"]
27
+ chip = env["chip"]
28
+ mlx = env["mlx"]
29
+ tools = env["tools"]
30
+ api_keys = env["api_keys"]
31
+ disk_free = env["disk_free_gb"]
32
+ is_apple = chip["is_apple_silicon"]
33
+ gpu = env.get("gpu", {})
34
+ cuda = env.get("cuda", {})
35
+ wsl = env.get("wsl", {})
36
+ cpu = env.get("cpu", {})
37
+ os_name = env.get("os", "")
38
+
39
+ max_model_gb = ram * 0.72 # ~28% headroom for OS + apps
40
+
41
+ if is_apple:
42
+ preferred_engine = "local_mlx"
43
+ elif gpu.get("vendor") == "nvidia" and cuda.get("available") and (os_name == "Linux" or wsl.get("is_wsl")):
44
+ preferred_engine = "vllm"
45
+ elif tools.get("lms"):
46
+ preferred_engine = "lmstudio"
47
+ elif tools.get("ollama"):
48
+ preferred_engine = "ollama"
49
+ else:
50
+ preferred_engine = "llamacpp"
51
+
52
+ apple_catalog = _filter_lower_family_versions(_MODEL_CATALOG)
53
+ engine_catalog = (
54
+ []
55
+ if is_apple
56
+ else _filter_lower_family_versions(_CROSS_PLATFORM_MODEL_CATALOG[preferred_engine])
57
+ )
58
+ best_id = _best_model_for_engine(
59
+ "local_mlx" if is_apple else preferred_engine,
60
+ ram,
61
+ apple_catalog if is_apple else engine_catalog,
62
+ )
63
+
64
+ # ── Engines ──────────────────────────────────────────────────────────────
65
+ engines: List[Dict] = []
66
+
67
+ if is_apple:
68
+ if mlx["available"] and mlx["mlx_vlm"]:
69
+ engines.append({
70
+ "id": "engine_mlx", "name": "MLX",
71
+ "subtitle": f"{chip['name']} GPU 가속 · MLX-VLM 멀티모달 실행",
72
+ "status": "installed", "priority": "recommended",
73
+ "checked": True, "action": None, "badge": "설치됨",
74
+ })
75
+ else:
76
+ engines.append({
77
+ "id": "engine_mlx", "name": "MLX",
78
+ "subtitle": f"{chip['name']} 전용 MLX-VLM 멀티모달 실행",
79
+ "status": "available", "priority": "recommended",
80
+ "checked": True,
81
+ "action": {"type": "pip", "packages": ["mlx-vlm"], "verify_modules": ["mlx", "mlx_vlm"]},
82
+ "badge": "설치 필요",
83
+ })
84
+
85
+ if tools.get("ollama"):
86
+ engines.append({
87
+ "id": "engine_ollama", "name": "Ollama",
88
+ "subtitle": "범용 로컬 LLM 서버 · 크로스 플랫폼",
89
+ "status": "installed", "priority": "recommended" if preferred_engine == "ollama" else "optional",
90
+ "checked": preferred_engine == "ollama", "action": None, "badge": "설치됨",
91
+ })
92
+ else:
93
+ hint = "brew install 가능" if (tools.get("brew") or env["os"] == "Darwin") else "수동 설치 필요"
94
+ engines.append({
95
+ "id": "engine_ollama", "name": "Ollama",
96
+ "subtitle": "범용 로컬 LLM 서버 · 크로스 플랫폼",
97
+ "status": "available", "priority": "recommended" if preferred_engine == "ollama" else "optional",
98
+ "checked": preferred_engine == "ollama",
99
+ "action": (
100
+ {"type": "brew", "package": "ollama", "binary": "ollama", "official_url": OFFICIAL_DOWNLOADS["ollama"]}
101
+ if tools.get("brew")
102
+ else {"type": "url", "url": OFFICIAL_DOWNLOADS["ollama"], "binary": "ollama"}
103
+ ),
104
+ "badge": hint,
105
+ })
106
+
107
+ if not is_apple:
108
+ lmstudio_installed = bool(tools.get("lms"))
109
+ engines.append({
110
+ "id": "engine_lmstudio", "name": "LM Studio",
111
+ "subtitle": "Windows/macOS/Linux 데스크톱 GPU 서버 · 모델 다운로드 UI 포함",
112
+ "status": "installed" if lmstudio_installed else "available",
113
+ "priority": "recommended" if preferred_engine == "lmstudio" else "optional",
114
+ "checked": preferred_engine == "lmstudio",
115
+ "action": None if lmstudio_installed else {"type": "url", "url": OFFICIAL_DOWNLOADS["lmstudio"], "binary": "lms"},
116
+ "badge": "설치됨" if lmstudio_installed else "설치 필요",
117
+ })
118
+ if gpu.get("vendor") == "nvidia":
119
+ engines.append({
120
+ "id": "engine_cuda", "name": "CUDA",
121
+ "subtitle": f"NVIDIA {gpu.get('name') or 'GPU'} · VRAM {gpu.get('vram_gb') or 0} GB",
122
+ "status": "installed" if cuda.get("available") else "available",
123
+ "priority": "recommended",
124
+ "checked": False,
125
+ "action": None if cuda.get("available") else {"type": "url", "url": OFFICIAL_DOWNLOADS["cuda"], "binary": "nvcc"},
126
+ "badge": cuda.get("version") or ("감지됨" if cuda.get("available") else "설치 필요"),
127
+ })
128
+ engines.append({
129
+ "id": "engine_vllm", "name": "vLLM",
130
+ "subtitle": "NVIDIA 서버형 추론 · Windows는 WSL/Linux 권장",
131
+ "status": "available",
132
+ "priority": "recommended" if preferred_engine == "vllm" else "optional",
133
+ "checked": preferred_engine == "vllm",
134
+ "action": {"type": "pip", "packages": ["vllm", "huggingface_hub[cli]"], "verify_modules": ["vllm", "huggingface_hub"]},
135
+ "badge": "WSL/Linux 권장" if os_name == "Windows" and not wsl.get("is_wsl") else "설치 가능",
136
+ })
137
+ elif gpu.get("vendor") in {"amd", "intel"}:
138
+ engines.append({
139
+ "id": "engine_vulkan_directml", "name": "Vulkan/DirectML",
140
+ "subtitle": f"{gpu.get('vendor', '').upper()} GPU 감지 · LM Studio 또는 llama.cpp 백엔드 권장",
141
+ "status": "available",
142
+ "priority": "recommended" if preferred_engine in {"lmstudio", "llamacpp"} else "optional",
143
+ "checked": False,
144
+ "action": None,
145
+ "badge": gpu.get("backend") or "GPU",
146
+ })
147
+
148
+ components: List[Dict] = []
149
+ component_specs = [
150
+ ("homebrew", "Homebrew", "macOS 패키지 관리자 · 자동 설치 기반", "brew", None, "recommended"),
151
+ ("git", "Git", "저장소 · 확장 · MCP 도구 연동에 필요", "git", "git", "recommended"),
152
+ ("node", "Node.js", "npm 패키지와 VS Code 확장 개발에 필요", "node", "node", "optional"),
153
+ ("tesseract", "Tesseract OCR", "이미지/PDF OCR 기능에 필요", "tesseract", "tesseract", "optional"),
154
+ ]
155
+ for cid, name, subtitle, binary, brew_pkg, priority in component_specs:
156
+ installed = bool(tools.get(binary))
157
+ if cid == "homebrew" and env["os"] != "Darwin":
158
+ continue
159
+ if installed:
160
+ components.append({
161
+ "id": f"component_{cid}", "name": name,
162
+ "subtitle": subtitle, "status": "installed",
163
+ "priority": priority, "checked": False, "action": None,
164
+ "badge": "설치됨",
165
+ })
166
+ continue
167
+ if cid == "homebrew":
168
+ action = {"type": "url", "url": OFFICIAL_DOWNLOADS["homebrew"], "binary": "brew"}
169
+ elif tools.get("brew") and brew_pkg:
170
+ action = {
171
+ "type": "brew",
172
+ "package": brew_pkg,
173
+ "binary": binary or "",
174
+ "official_url": OFFICIAL_DOWNLOADS.get(cid, ""),
175
+ }
176
+ else:
177
+ action = {
178
+ "type": "url",
179
+ "url": OFFICIAL_DOWNLOADS.get(cid, ""),
180
+ "binary": binary or "",
181
+ }
182
+ components.append({
183
+ "id": f"component_{cid}", "name": name,
184
+ "subtitle": subtitle, "status": "available",
185
+ "priority": priority, "checked": priority == "recommended",
186
+ "action": action, "badge": "설치 필요",
187
+ })
188
+
189
+ python_ok = sys.version_info >= (3, 11)
190
+ if not python_ok:
191
+ components.insert(0, {
192
+ "id": "component_python", "name": "Python 3.11+",
193
+ "subtitle": "Lattice AI 서버 실행에 필요한 Python 런타임",
194
+ "status": "available", "priority": "recommended", "checked": True,
195
+ "action": {"type": "url", "url": OFFICIAL_DOWNLOADS["python"], "binary": "python3"},
196
+ "badge": "업데이트 필요",
197
+ })
198
+
199
+ for provider, has_key in api_keys.items():
200
+ if has_key:
201
+ engines.append({
202
+ "id": f"engine_{provider}", "name": provider.title(),
203
+ "subtitle": f"{provider.upper()}_API_KEY 감지됨 · 클라우드 API",
204
+ "status": "ready", "priority": "optional",
205
+ "checked": False, "action": None, "badge": "준비됨",
206
+ })
207
+
208
+ # ── Models ───────────────────────────────────────────────────────────────
209
+ models: List[Dict] = []
210
+
211
+ if is_apple:
212
+ for mid, mname, size_gb, tag, desc, min_ram in apple_catalog:
213
+ fits = ram >= min_ram and size_gb <= max_model_gb and disk_free >= size_gb + 2
214
+ is_best = mid == best_id
215
+ models.append({
216
+ "id": f"model_{mid.replace('/', '__').replace('-', '_')}",
217
+ "model_id": mid,
218
+ "name": mname,
219
+ "subtitle": desc,
220
+ "size_gb": size_gb,
221
+ "tag": tag,
222
+ "fits": fits,
223
+ "priority": "recommended" if is_best else "optional",
224
+ "checked": is_best and fits,
225
+ "disabled": not fits,
226
+ "badge": f"{size_gb} GB",
227
+ "action": {"type": "load_model", "model_id": mid} if fits else None,
228
+ })
229
+ else:
230
+ vram_gb = float(gpu.get("vram_gb") or 0)
231
+ gpu_budget_gb = vram_gb * 1.15 if gpu.get("vendor") in {"nvidia", "amd", "intel"} and vram_gb else max_model_gb
232
+ model_budget_gb = min(max_model_gb, gpu_budget_gb)
233
+ for mid, mname, size_gb, tag, desc, min_ram in engine_catalog:
234
+ fits = ram >= min_ram and size_gb <= model_budget_gb and disk_free >= size_gb + 2
235
+ is_best = mid == best_id
236
+ models.append({
237
+ "id": f"model_{mid.replace('/', '__').replace(':', '__').replace('-', '_')}",
238
+ "model_id": mid,
239
+ "name": mname,
240
+ "subtitle": desc,
241
+ "size_gb": size_gb,
242
+ "tag": tag,
243
+ "fits": fits,
244
+ "priority": "recommended" if is_best else "optional",
245
+ "checked": is_best and fits,
246
+ "disabled": not fits,
247
+ "badge": f"{size_gb} GB · {preferred_engine}",
248
+ "action": {"type": "load_model", "model_id": mid} if fits else None,
249
+ })
250
+ if models and not any(item.get("checked") for item in models):
251
+ for item in models:
252
+ if not item.get("disabled"):
253
+ item["priority"] = "recommended"
254
+ item["checked"] = True
255
+ break
256
+
257
+ # ── MCPs ─────────────────────────────────────────────────────────────────
258
+ mcps: List[Dict] = [
259
+ {
260
+ "id": "mcp_files", "name": "Workspace Files",
261
+ "subtitle": "파일 읽기/쓰기 · 코드 생성 · 미리보기",
262
+ "status": "active", "priority": "recommended",
263
+ "checked": True, "action": None,
264
+ "badge": "기본 탑재", "needs_auth": False,
265
+ },
266
+ {
267
+ "id": "mcp_presentations", "name": "Presentations",
268
+ "subtitle": "PPTX · 슬라이드 자동 생성",
269
+ "status": "active", "priority": "optional",
270
+ "checked": False, "action": None,
271
+ "badge": "기본 탑재", "needs_auth": False,
272
+ },
273
+ {
274
+ "id": "mcp_github", "name": "GitHub",
275
+ "subtitle": "저장소 · PR · 이슈 · CI 연동",
276
+ "status": "available", "priority": "optional",
277
+ "checked": False,
278
+ "action": {"type": "auth", "url": "https://github.com/apps", "mcp_id": "github"},
279
+ "badge": "인증 필요", "needs_auth": True,
280
+ },
281
+ {
282
+ "id": "mcp_googledrive", "name": "Google Drive",
283
+ "subtitle": "Docs · Sheets · Drive 파일 연동",
284
+ "status": "available", "priority": "optional",
285
+ "checked": False,
286
+ "action": {"type": "auth", "url": "https://chatgpt.com/connectors", "mcp_id": "google-drive"},
287
+ "badge": "인증 필요", "needs_auth": True,
288
+ },
289
+ {
290
+ "id": "mcp_slack", "name": "Slack",
291
+ "subtitle": "팀 채널 공유 · 알림 워크플로",
292
+ "status": "available", "priority": "optional",
293
+ "checked": False,
294
+ "action": {"type": "auth", "url": "https://chatgpt.com/connectors", "mcp_id": "slack"},
295
+ "badge": "인증 필요", "needs_auth": True,
296
+ },
297
+ ]
298
+
299
+ return _hydrate_install_actions({
300
+ "components": components,
301
+ "engines": engines,
302
+ "models": models,
303
+ "mcps": mcps,
304
+ "summary": {
305
+ "chip": chip["name"],
306
+ "cpu_cores": cpu.get("logical_cores"),
307
+ "cpu_instructions": cpu.get("instructions", []),
308
+ "gpu": gpu.get("name") or gpu.get("vendor"),
309
+ "gpu_vendor": gpu.get("vendor"),
310
+ "vram_gb": gpu.get("vram_gb"),
311
+ "cuda": cuda.get("available"),
312
+ "cuda_version": cuda.get("version"),
313
+ "wsl": wsl,
314
+ "preferred_engine": preferred_engine,
315
+ "ram_gb": ram,
316
+ "disk_free_gb": disk_free,
317
+ "is_apple_silicon": is_apple,
318
+ "max_model_gb": round(max_model_gb, 1),
319
+ },
320
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "11.1.0",
3
+ "version": "11.4.0",
4
4
  "description": "Lattice AI — local-first Digital Brain that keeps your knowledge durable across any AI model.",
5
5
  "homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
6
6
  "repository": {
@@ -24,7 +24,9 @@
24
24
  "build:assets": "vite build && node scripts/build_frontend_assets.mjs",
25
25
  "build:python": "node scripts/run_python.mjs -m build",
26
26
  "check:python": "node scripts/run_python.mjs scripts/check_python.py",
27
- "lint": "npm run lint:python && node --check tests/visual/mock_server.cjs && node --check tests/visual/v3.spec.js && npm run lint:frontend && npm run frontend:openapi:check && node scripts/check_i18n_literals.mjs && npm run check:i18n-namespaces && npm run check:bundle && npm run check:legacy-debt && npm run check:server-i18n && npm run test:browser-extension && npm run test:vscode-extension && node scripts/check_release_evidence_bound.mjs",
27
+ "lint": "npm run lint:python && npm run lint:visual && npm run lint:frontend && npm run frontend:openapi:check && node scripts/check_i18n_literals.mjs && npm run check:i18n-namespaces && npm run check:bundle && npm run check:legacy-debt && npm run check:server-i18n && npm run test:browser-extension && npm run test:vscode-extension && node scripts/check_release_evidence_bound.mjs && npm run check:max-file-lines",
28
+ "lint:visual": "for f in tests/visual/*.js tests/visual/*.cjs tests/visual/mock_server/*.cjs; do node --check \"$f\" || exit 1; done && echo 'visual syntax ok'",
29
+ "check:max-file-lines": "node scripts/check_max_file_lines.mjs",
28
30
  "check:release-evidence-bound": "node scripts/check_release_evidence_bound.mjs",
29
31
  "check:legacy-debt": "node scripts/check_legacy_debt.mjs",
30
32
  "check:server-i18n": "node scripts/check_server_i18n.mjs",
@@ -48,6 +50,8 @@
48
50
  "desktop:tauri": "tauri dev",
49
51
  "desktop:tauri:build": "tauri build",
50
52
  "desktop:tauri:check": "cd src-tauri && cargo check",
53
+ "rust:test": "cd rust && cargo test --workspace",
54
+ "rust:lint": "cd rust && cargo fmt --all -- --check && cargo clippy --workspace --all-targets -- -D warnings",
51
55
  "desktop:electron": "electron desktop/electron/main.cjs",
52
56
  "package:vsix": "node scripts/build_vsix.mjs",
53
57
  "check:screenshot-delta": "node scripts/run_python.mjs scripts/check_screenshot_pixel_delta.py",
@@ -20,6 +20,14 @@ from pathlib import Path
20
20
 
21
21
  REPO = Path(__file__).resolve().parents[1]
22
22
 
23
+ # The rust/ workspace crates carry `version.workspace = true`, so the workspace
24
+ # root is the only hand-edited copy of the number. Both lockfiles still record
25
+ # it per crate — src-tauri's too, since 11.4.0, because the desktop shell now
26
+ # depends on lattice-host by path — and a lockfile left behind is a file the
27
+ # next `cargo build` silently rewrites underneath a tagged release.
28
+ RUST_CRATES = ("lattice-core", "lattice-host", "lattice-retrieval")
29
+ RUST_LOCKFILES = ("rust/Cargo.lock", "src-tauri/Cargo.lock")
30
+
23
31
  # (path, kind, pattern) — pattern groups: (prefix, version)
24
32
  TARGETS = [
25
33
  ("latticeai/__init__.py", "regex", r'(__version__ = ")([^"]+)(")'),
@@ -40,6 +48,12 @@ TARGETS = [
40
48
  ("src-tauri/Cargo.toml", "regex", r'(^version = ")([^"]+)(")'),
41
49
  ("src-tauri/Cargo.lock", "regex", r'(name = "lattice-ai-desktop"\nversion = ")([^"]+)(")'),
42
50
  ("src-tauri/tauri.conf.json", "json", "version"),
51
+ ("rust/Cargo.toml", "regex", r'(^version = ")([^"]+)(")'),
52
+ *[
53
+ (lock, "regex", rf'(name = "{crate}"\nversion = ")([^"]+)(")')
54
+ for lock in RUST_LOCKFILES
55
+ for crate in RUST_CRATES
56
+ ],
43
57
  ("static/app/asset-manifest.json", "json", "version"),
44
58
  ]
45
59
 
@@ -4,6 +4,12 @@ import { execFileSync, spawn } from "node:child_process";
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
6
 
7
+ import {
8
+ MOCK_SERVER_ENTRY,
9
+ MOCK_SERVER_LABEL,
10
+ mockServerFingerprint,
11
+ } from "./lib/mock_server_fingerprint.mjs";
12
+
7
13
  const repoRoot = process.cwd();
8
14
  const version = JSON.parse(fs.readFileSync(path.join(repoRoot, "package.json"), "utf8")).version;
9
15
  const port = Number(process.env.LTCAI_RELEASE_EVIDENCE_PORT || 4936);
@@ -34,20 +40,10 @@ function assetManifestFingerprint() {
34
40
  };
35
41
  }
36
42
 
37
- const mockServerPath = path.join(repoRoot, "tests", "visual", "mock_server.cjs");
38
-
39
- function mockServerFingerprint() {
40
- if (!fs.existsSync(mockServerPath)) {
41
- return null;
42
- }
43
- const body = fs.readFileSync(mockServerPath);
44
- const stat = fs.statSync(mockServerPath);
45
- return {
46
- sha256: createHash("sha256").update(body).digest("hex"),
47
- mtime: stat.mtime.toISOString(),
48
- bytes: body.length,
49
- };
50
- }
43
+ // The mock API surface is the entry file plus the route modules it composes;
44
+ // the fingerprint covers all of them. Shared with
45
+ // scripts/check_release_evidence_bound.mjs so the digest recorded here and the
46
+ // digest verified there are computed by exactly one piece of code.
51
47
 
52
48
  /**
53
49
  * Refuse to overwrite published release screenshots for an already-tagged version.
@@ -99,7 +95,7 @@ function assertNotOverwritingTaggedRelease() {
99
95
  * Refuse to capture when the release lint gate is red.
100
96
  *
101
97
  * Failure mode this catches: lint is failing (e.g. ruff F841) but
102
- * capture still runs and writes 12 screenshots + SCREENSHOT_INDEX.md
98
+ * capture still runs and writes 13 screenshots + SCREENSHOT_INDEX.md
103
99
  * bound to the asset manifest — evidence that looks green while the
104
100
  * first CI stage would already have stopped. Capture is not proof that
105
101
  * the tree is releasable; lint must pass first.
@@ -253,7 +249,7 @@ fs.mkdirSync(screenshots, { recursive: true });
253
249
  fs.mkdirSync(videos, { recursive: true });
254
250
  fs.mkdirSync(gifs, { recursive: true });
255
251
 
256
- const server = spawn(process.execPath, ["tests/visual/mock_server.cjs"], {
252
+ const server = spawn(process.execPath, [MOCK_SERVER_ENTRY], {
257
253
  cwd: repoRoot,
258
254
  env: { ...process.env, LTCAI_VISUAL_PORT: String(port) },
259
255
  stdio: ["ignore", "pipe", "pipe"],
@@ -444,6 +440,18 @@ async function main() {
444
440
  }
445
441
  await shot(page, "12-review-center.png");
446
442
 
443
+ // 연대기 (11.3.0). Waiting on the title alone would publish the loading or
444
+ // empty frame, which is precisely what this screen must never be caught in:
445
+ // "오늘부터 기록이 쌓입니다" is a true state and a useless screenshot. Wait for
446
+ // the drawn curve and a populated grid, both of which only exist once the
447
+ // overview read has landed.
448
+ await page.goto(`${baseURL}/app#/chronicle`, { waitUntil: "networkidle" });
449
+ await page.locator("h1.page-title", { hasText: "두뇌가 자라온 시간" }).waitFor();
450
+ await page.locator(".chronicle-growth-area[d^='M']").waitFor();
451
+ await page.getByTestId("chronicle-heatmap-cell").first().waitFor();
452
+ await page.getByTestId("chronicle-day").waitFor();
453
+ await shot(page, "13-chronicle.png");
454
+
447
455
  const video = page.video();
448
456
  await context.close();
449
457
  await browser.close();
@@ -473,13 +481,14 @@ async function main() {
473
481
  if (!fingerprint) {
474
482
  throw new Error("asset-manifest.json missing after capture — cannot bind evidence");
475
483
  }
476
- const mockFp = mockServerFingerprint();
484
+ const mockFp = mockServerFingerprint(repoRoot);
477
485
  if (!mockFp) {
478
- throw new Error("tests/visual/mock_server.cjs missing after capture — cannot bind evidence");
486
+ throw new Error(`${MOCK_SERVER_ENTRY} missing after capture — cannot bind evidence`);
479
487
  }
480
488
  // Post-capture binding: if a later build:assets changes the manifest hash,
481
- // or mock_server.cjs changes the capture payloads, check_release_evidence_bound.mjs
482
- // will refuse to trust these screenshots without a recapture.
489
+ // or any mock-server file changes the capture payloads,
490
+ // check_release_evidence_bound.mjs will refuse to trust these screenshots
491
+ // without a recapture.
483
492
  const index = `# v${version} Release Evidence
484
493
 
485
494
  Captured from the built React/Vite app served by the release visual API on ${new Date().toISOString()}.
@@ -487,7 +496,8 @@ Captured from the built React/Vite app served by the release visual API on ${new
487
496
  ## Build Binding
488
497
 
489
498
  Evidence is only trustworthy while this fingerprint matches
490
- \`static/app/asset-manifest.json\` **and** \`tests/visual/mock_server.cjs\`.
499
+ \`static/app/asset-manifest.json\` **and** the mock API surface
500
+ (\`${MOCK_SERVER_LABEL}\`).
491
501
  A later \`build:assets\` or mock-server edit without recapture invalidates the
492
502
  screenshots even when their mtimes look fresh.
493
503
 
@@ -497,6 +507,7 @@ screenshots even when their mtimes look fresh.
497
507
  - mock-server.sha256: \`${mockFp.sha256}\`
498
508
  - mock-server.mtime: \`${mockFp.mtime}\`
499
509
  - mock-server.bytes: ${mockFp.bytes}
510
+ - mock-server.files: ${mockFp.files}
500
511
 
501
512
  ## Screenshots
502
513
 
@@ -514,6 +525,7 @@ screenshots even when their mtimes look fresh.
514
525
  | [10-admin-console.png](screenshots/10-admin-console.png) | Separate Admin Console |
515
526
  | [11-knowledge-journey.png](screenshots/11-knowledge-journey.png) | Material-to-memory steps |
516
527
  | [12-review-center.png](screenshots/12-review-center.png) | Automation Review Center |
528
+ | [13-chronicle.png](screenshots/13-chronicle.png) | Brain Chronicle — growth, activity, one day's story |
517
529
 
518
530
  ## Motion Evidence
519
531
 
@@ -6,7 +6,7 @@ const root = process.cwd();
6
6
  const pkg = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"));
7
7
  const version = pkg.version;
8
8
  const releaseDir = `output/release/v${version}`;
9
- const releaseTheme = "Product Intelligence";
9
+ const releaseTheme = "Rust Foundation";
10
10
  const title = `${version} — ${releaseTheme}`;
11
11
  const escapedVersion = version.replaceAll(".", "\\.");
12
12
 
@@ -28,11 +28,48 @@ const NAMESPACES = readdirSync(i18nDir)
28
28
  .map((name) => name.replace(/\.ts$/, ""))
29
29
  .filter((name) => !["types", "registry"].includes(name));
30
30
 
31
+ /**
32
+ * Every file that holds copy for a namespace.
33
+ *
34
+ * A namespace is one module — `brain.ts` — and that module is still the only
35
+ * thing that calls `registerCopy`. But a dictionary that outgrew one file keeps
36
+ * its domain parts in a directory beside it (`brain/home.ts`, `brain/chat.ts`,
37
+ * …), which `brain.ts` imports and merges. Those parts own keys; they are not
38
+ * namespaces of their own, and importing one does NOT make the namespace
39
+ * available (nothing registers). So they feed `keyOwner` below, and the
40
+ * availability check further down still only accepts the namespace module.
41
+ */
42
+ function namespaceSources(ns) {
43
+ const files = [join(i18nDir, `${ns}.ts`)];
44
+ const partsDir = join(i18nDir, ns);
45
+ if (existsSync(partsDir) && statSync(partsDir).isDirectory()) {
46
+ const walk = (dir) => {
47
+ for (const name of readdirSync(dir)) {
48
+ const path = join(dir, name);
49
+ if (statSync(path).isDirectory()) walk(path);
50
+ else if (name.endsWith(".ts")) files.push(path);
51
+ }
52
+ };
53
+ walk(partsDir);
54
+ }
55
+ return files;
56
+ }
57
+
58
+ /** A file inside frontend/src/i18n holds copy, never key uses to check. */
59
+ function insideI18n(file) {
60
+ const rel = relative(i18nDir, file);
61
+ return rel !== "" && !rel.startsWith("..");
62
+ }
63
+
31
64
  // key -> namespace that defines it
32
65
  const keyOwner = new Map();
66
+ let copyFiles = 0;
33
67
  for (const ns of NAMESPACES) {
34
- const text = readFileSync(join(i18nDir, `${ns}.ts`), "utf8");
35
- for (const match of text.matchAll(/^\s+"([^"]+)":/gm)) keyOwner.set(match[1], ns);
68
+ for (const file of namespaceSources(ns)) {
69
+ copyFiles += 1;
70
+ const text = readFileSync(file, "utf8");
71
+ for (const match of text.matchAll(/^\s+"([^"]+)":/gm)) keyOwner.set(match[1], ns);
72
+ }
36
73
  }
37
74
 
38
75
  // `shell` is registered by i18n.ts itself, so it is always available.
@@ -121,7 +158,7 @@ for (const [label, entry] of entries) {
121
158
  }
122
159
  }
123
160
  for (const file of modules) {
124
- if (dirname(file) === i18nDir) continue;
161
+ if (insideI18n(file)) continue;
125
162
  for (const key of moduleInfo(file).keys) {
126
163
  const owner = keyOwner.get(key);
127
164
  if (!owner) {
@@ -144,5 +181,5 @@ if (errors.length) {
144
181
 
145
182
  console.log(
146
183
  `i18n namespace coverage: ${entries.size} entry chunk(s) verified against `
147
- + `${NAMESPACES.length} namespaces (${keyOwner.size} keys).`,
184
+ + `${NAMESPACES.length} namespaces (${keyOwner.size} keys across ${copyFiles} copy file(s)).`,
148
185
  );