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.
- package/README.md +47 -54
- package/docs/CHANGELOG.md +94 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/FEATURE_AUDIT_v11.2.0.md +393 -0
- package/docs/LAYOUT_REBUILD_SPEC.md +9 -1
- package/docs/MULTI_AGENT_RUNTIME.md +1 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +6 -2
- package/docs/PERMISSION_MODE.md +1 -1
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/architecture.md +6 -2
- package/docs/kg-schema.md +2 -2
- package/docs/v11.3.0_PLAN.md +202 -0
- package/docs/v11.4.0_RUST_FOUNDATION_PLAN.md +176 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/gates.py +125 -0
- package/lattice_brain/graph/_kg_common/__init__.py +287 -0
- package/lattice_brain/graph/_kg_common/extraction.py +516 -0
- package/lattice_brain/graph/_kg_common/relations.py +161 -0
- package/lattice_brain/graph/_kg_common/text.py +479 -0
- package/lattice_brain/graph/discovery_index/__init__.py +35 -0
- package/lattice_brain/graph/discovery_index/cleanup.py +182 -0
- package/lattice_brain/graph/discovery_index/extract.py +137 -0
- package/lattice_brain/graph/discovery_index/scan.py +411 -0
- package/lattice_brain/graph/discovery_index/upsert.py +495 -0
- package/lattice_brain/graph/fusion.py +35 -4
- package/lattice_brain/graph/projection/__init__.py +42 -0
- package/lattice_brain/graph/projection/curation.py +500 -0
- package/lattice_brain/graph/{projection.py → projection/v2_schema.py} +81 -485
- package/lattice_brain/graph/retrieval/__init__.py +54 -0
- package/lattice_brain/graph/retrieval/context.py +197 -0
- package/lattice_brain/graph/retrieval/graph_view.py +319 -0
- package/lattice_brain/graph/retrieval/hybrid.py +488 -0
- package/lattice_brain/graph/retrieval/maintenance.py +121 -0
- package/lattice_brain/graph/retrieval/signals.py +95 -0
- package/lattice_brain/graph/retrieval_vector/__init__.py +42 -0
- package/lattice_brain/graph/retrieval_vector/fingerprint.py +97 -0
- package/lattice_brain/graph/retrieval_vector/indexing.py +347 -0
- package/lattice_brain/graph/retrieval_vector/search.py +560 -0
- package/lattice_brain/graph/retrieval_vector/status.py +374 -0
- package/lattice_brain/graph/schema.py +9 -0
- package/lattice_brain/graph/store.py +9 -0
- package/lattice_brain/graph/vector_index/selector.py +32 -2
- package/lattice_brain/ingestion/__init__.py +130 -0
- package/lattice_brain/ingestion/_contract.py +90 -0
- package/lattice_brain/ingestion/constants.py +127 -0
- package/lattice_brain/ingestion/folder_scan.py +57 -0
- package/lattice_brain/ingestion/folders.py +258 -0
- package/lattice_brain/ingestion/hashing.py +26 -0
- package/lattice_brain/ingestion/jobs_api.py +107 -0
- package/lattice_brain/ingestion/models.py +80 -0
- package/lattice_brain/ingestion/pipeline.py +486 -0
- package/lattice_brain/ingestion/quality.py +209 -0
- package/lattice_brain/ingestion/routing.py +295 -0
- package/lattice_brain/multimodal/__init__.py +164 -0
- package/lattice_brain/multimodal/audio.py +77 -0
- package/lattice_brain/multimodal/common.py +118 -0
- package/lattice_brain/{multimodal.py → multimodal/images.py} +22 -262
- package/lattice_brain/multimodal/ports.py +169 -0
- package/lattice_brain/multimodal/video.py +410 -0
- package/lattice_brain/portability/__init__.py +90 -0
- package/lattice_brain/portability/_contract.py +42 -0
- package/lattice_brain/portability/backups.py +338 -0
- package/lattice_brain/portability/bundles.py +136 -0
- package/lattice_brain/portability/constants.py +93 -0
- package/lattice_brain/portability/fsops.py +138 -0
- package/lattice_brain/portability/service.py +41 -0
- package/lattice_brain/portability/sharing.py +714 -0
- package/lattice_brain/runtime/__init__.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/lattice_brain/sealed_box.py +244 -0
- package/lattice_brain/synthesis.py +24 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/brain_intelligence.py +4 -0
- package/latticeai/api/chat.py +11 -0
- package/latticeai/api/chat_helpers.py +16 -3
- package/latticeai/api/chat_hybrid.py +32 -1
- package/latticeai/api/chronicle.py +63 -0
- package/latticeai/api/features.py +70 -0
- package/latticeai/api/local_files.py +102 -0
- package/latticeai/api/portability.py +39 -4
- package/latticeai/api/review_queue.py +126 -0
- package/latticeai/api/search.py +16 -2
- package/latticeai/core/agent/__init__.py +93 -0
- package/latticeai/core/agent/_contract.py +79 -0
- package/latticeai/core/agent/context.py +57 -0
- package/latticeai/core/agent/deps.py +125 -0
- package/latticeai/core/agent/execution.py +622 -0
- package/latticeai/core/agent/planning.py +145 -0
- package/latticeai/core/agent/recovery.py +157 -0
- package/latticeai/core/agent/runtime.py +210 -0
- package/latticeai/core/agent/verification.py +231 -0
- package/latticeai/core/config.py +4 -1
- package/latticeai/core/context_builder.py +6 -3
- package/latticeai/core/embedding_providers/__init__.py +151 -0
- package/latticeai/core/embedding_providers/base.py +199 -0
- package/latticeai/core/embedding_providers/captions.py +162 -0
- package/latticeai/core/embedding_providers/profiles.py +126 -0
- package/latticeai/core/embedding_providers/text.py +350 -0
- package/latticeai/core/embedding_providers/vision.py +352 -0
- package/latticeai/core/file_generation/__init__.py +115 -0
- package/latticeai/core/file_generation/bundles.py +76 -0
- package/latticeai/core/file_generation/extraction.py +154 -0
- package/latticeai/core/file_generation/inference.py +235 -0
- package/latticeai/core/file_generation/orchestration.py +152 -0
- package/latticeai/core/file_generation/prompting.py +117 -0
- package/latticeai/core/file_generation/repair.py +114 -0
- package/latticeai/core/file_generation/sanitize.py +61 -0
- package/latticeai/core/file_generation/validation.py +201 -0
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/messages.py +152 -0
- package/latticeai/core/model_compat.py +73 -2
- package/latticeai/core/workspace_os_constants.py +1 -1
- package/latticeai/integrations/telegram_bot/__init__.py +123 -0
- package/latticeai/integrations/telegram_bot/__main__.py +17 -0
- package/latticeai/integrations/telegram_bot/config.py +86 -0
- package/latticeai/integrations/telegram_bot/dispatch.py +311 -0
- package/latticeai/integrations/telegram_bot/flows.py +478 -0
- package/latticeai/integrations/telegram_bot/helpers.py +322 -0
- package/latticeai/integrations/telegram_bot/screens.py +394 -0
- package/latticeai/models/model_providers.py +12 -4
- package/latticeai/models/router/__init__.py +88 -0
- package/latticeai/models/router/_contract.py +66 -0
- package/latticeai/models/router/branding.py +56 -0
- package/latticeai/models/router/catalog.py +69 -0
- package/latticeai/models/router/documents.py +199 -0
- package/latticeai/models/router/errors.py +37 -0
- package/latticeai/models/router/generation.py +258 -0
- package/latticeai/models/router/loading.py +291 -0
- package/latticeai/models/router/local_models.py +85 -0
- package/latticeai/models/router/registry.py +147 -0
- package/latticeai/runtime/build_phases/__init__.py +82 -0
- package/latticeai/runtime/build_phases/features.py +407 -0
- package/latticeai/runtime/build_phases/foundation.py +555 -0
- package/latticeai/runtime/build_phases/web.py +492 -0
- package/latticeai/runtime/chat_wiring.py +4 -0
- package/latticeai/runtime/feature_toggle_wiring.py +163 -0
- package/latticeai/runtime/router_registration.py +11 -0
- package/latticeai/runtime/runtime_context.py +1 -0
- package/latticeai/services/app_context.py +8 -0
- package/latticeai/services/architecture_readiness.py +48 -19
- package/latticeai/services/automation_intelligence.py +22 -2
- package/latticeai/services/brain_intelligence/__init__.py +58 -0
- package/latticeai/services/brain_intelligence/_contract.py +71 -0
- package/latticeai/services/brain_intelligence/consistency.py +193 -0
- package/latticeai/services/brain_intelligence/constants.py +47 -0
- package/latticeai/services/brain_intelligence/digest.py +258 -0
- package/latticeai/services/brain_intelligence/health.py +331 -0
- package/latticeai/services/brain_intelligence/proposals.py +264 -0
- package/latticeai/services/brain_intelligence/sampling.py +84 -0
- package/latticeai/services/brain_intelligence/service.py +48 -0
- package/latticeai/services/chronicle.py +557 -0
- package/latticeai/services/command_center.py +10 -4
- package/latticeai/services/feature_toggles.py +502 -0
- package/latticeai/services/folder_watch.py +122 -1
- package/latticeai/services/hybrid_chat.py +56 -5
- package/latticeai/services/interop_bridges.py +978 -0
- package/latticeai/services/memory_service/__init__.py +52 -0
- package/latticeai/services/memory_service/_contract.py +100 -0
- package/latticeai/services/memory_service/brief.py +431 -0
- package/latticeai/services/memory_service/constants.py +57 -0
- package/latticeai/services/memory_service/maintenance.py +138 -0
- package/latticeai/services/memory_service/manager.py +186 -0
- package/latticeai/services/memory_service/proof.py +136 -0
- package/latticeai/services/memory_service/recall.py +225 -0
- package/latticeai/services/memory_service/service.py +48 -0
- package/latticeai/services/memory_service/stores.py +110 -0
- package/latticeai/services/model_capability_registry.py +434 -261
- package/latticeai/services/model_catalog.py +95 -61
- package/latticeai/services/model_recommendation.py +18 -11
- package/latticeai/services/model_runtime/__init__.py +322 -0
- package/latticeai/services/model_runtime/cloud.py +87 -0
- package/latticeai/services/model_runtime/download.py +282 -0
- package/latticeai/services/model_runtime/engines.py +341 -0
- package/latticeai/services/model_runtime/loading.py +178 -0
- package/latticeai/services/model_runtime/service.py +129 -0
- package/latticeai/services/model_runtime/state.py +131 -0
- package/latticeai/services/model_runtime/status.py +255 -0
- package/latticeai/services/multimodal_ports.py +26 -1
- package/latticeai/services/obsidian_bridge.py +16 -25
- package/latticeai/services/product_readiness.py +15 -7
- package/latticeai/services/search_service.py +149 -2
- package/latticeai/services/tool_dispatch.py +4 -0
- package/latticeai/setup/auto_setup.py +27 -30
- package/latticeai/setup/wizard/__init__.py +126 -0
- package/latticeai/setup/wizard/catalog.py +172 -0
- package/latticeai/setup/wizard/detect.py +323 -0
- package/latticeai/setup/wizard/install.py +348 -0
- package/latticeai/setup/wizard/paths.py +168 -0
- package/latticeai/setup/wizard/plans.py +74 -0
- package/latticeai/setup/wizard/recommend.py +320 -0
- package/package.json +6 -2
- package/scripts/bump_version.py +14 -0
- package/scripts/capture_release_evidence.mjs +33 -21
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/check_i18n_namespace_coverage.mjs +41 -4
- package/scripts/check_max_file_lines.mjs +102 -0
- package/scripts/check_release_evidence_bound.mjs +30 -15
- package/scripts/check_screenshot_pixel_delta.py +34 -4
- package/scripts/check_server_i18n.mjs +2 -0
- package/scripts/generate_rust_parity_fixtures.py +562 -0
- package/scripts/lib/mock_server_fingerprint.mjs +94 -0
- package/scripts/release_screen_claims.json +44 -2
- package/scripts/verify_hf_model_registry.py +253 -218
- package/src-tauri/Cargo.lock +361 -3
- package/src-tauri/Cargo.toml +6 -1
- package/src-tauri/src/backend.rs +349 -0
- package/src-tauri/src/folder.rs +33 -0
- package/src-tauri/src/main.rs +97 -399
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +41 -37
- package/static/app/assets/Act-yYpYnn0v.js +1 -0
- package/static/app/assets/AdminConsole-DL3Cr5pL.js +1 -0
- package/static/app/assets/{Brain-CzCsI1mi.js → Brain-C1HBN0Wf.js} +2 -2
- package/static/app/assets/BrainHome-DoXRhUUC.js +2 -0
- package/static/app/assets/BrainSignals-6yR6ir5t.js +1 -0
- package/static/app/assets/Capture-CFIRsFNE.js +1 -0
- package/static/app/assets/Chronicle-BZbEgiwN.js +1 -0
- package/static/app/assets/CommandPalette-D2pMxC2I.js +1 -0
- package/static/app/assets/Library-DwO3yZST.js +1 -0
- package/static/app/assets/{LivingBrain-BXMWIK_2.js → LivingBrain-Jn1GK0-S.js} +1 -1
- package/static/app/assets/ProductFlow-B-w1R4Oo.js +1 -0
- package/static/app/assets/ReviewCard-6B27X8Vg.js +3 -0
- package/static/app/assets/System-DW8F-2xL.js +1 -0
- package/static/app/assets/arrow-left-DXvKg9U6.js +1 -0
- package/static/app/assets/{bot-4BvN07ux.js → bot-IM_E_Y12.js} +1 -1
- package/static/app/assets/brain-Ci1CkWjM.js +1 -0
- package/static/app/assets/{button-CDjtnAoU.js → button-COwyqfHM.js} +1 -1
- package/static/app/assets/circle-check-DfInj-qD.js +1 -0
- package/static/app/assets/{circle-pause-D_RMn7tp.js → circle-pause-DEM4A1Y5.js} +1 -1
- package/static/app/assets/{circle-play-B5OpB8ae.js → circle-play-C9djDuLd.js} +1 -1
- package/static/app/assets/{cpu-BIlWInHf.js → cpu-DFdo1gw-.js} +1 -1
- package/static/app/assets/{download-BtjXfL3z.js → download-SnJL6oqk.js} +1 -1
- package/static/app/assets/{folder-open-DefMpxI2.js → folder-open-CqZeDkjE.js} +1 -1
- package/static/app/assets/{hard-drive-BQ8NZVkw.js → hard-drive-j1jJXYYf.js} +1 -1
- package/static/app/assets/{index-vtEfYvQY.css → index-BLPb5lmE.css} +1 -1
- package/static/app/assets/index-_u5iUHDr.js +10 -0
- package/static/app/assets/input-B0lPdRQZ.js +1 -0
- package/static/app/assets/link-2-CoFbooHS.js +1 -0
- package/static/app/assets/{permissionCopy-BqZ5tsgL.js → permissionCopy-BsyLxtao.js} +1 -1
- package/static/app/assets/primitives-DEbN-d6p.js +1 -0
- package/static/app/assets/search-BybIWPNd.js +1 -0
- package/static/app/assets/{share-2-D5zg_0fY.js → share-2-CVtZ_ewX.js} +1 -1
- package/static/app/assets/{shield-alert-B5pZzkUb.js → shield-alert-CBi2GNWM.js} +1 -1
- package/static/app/assets/{textarea-nEVIweKY.js → textarea-DNMpB5ih.js} +1 -1
- package/static/app/assets/{useFocusTrap-Cm99AHlz.js → useFocusTrap-C83t3GXF.js} +1 -1
- package/static/app/assets/useMutation-DtbJDoyz.js +1 -0
- package/static/app/assets/{useQuery-Dm__N6bL.js → useQuery-Dcp1OChy.js} +1 -1
- package/static/app/assets/utils-BlZr7Pd4.js +4 -0
- package/static/app/assets/workspace-jJY4RuAV.js +1 -0
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/lattice_brain/graph/_kg_common.py +0 -1331
- package/lattice_brain/graph/discovery_index.py +0 -1141
- package/lattice_brain/graph/retrieval.py +0 -1120
- package/lattice_brain/graph/retrieval_vector.py +0 -1293
- package/lattice_brain/ingestion.py +0 -1377
- package/lattice_brain/portability.py +0 -1210
- package/latticeai/core/agent.py +0 -1412
- package/latticeai/core/embedding_providers.py +0 -1196
- package/latticeai/core/file_generation.py +0 -1047
- package/latticeai/integrations/telegram_bot.py +0 -1390
- package/latticeai/models/router.py +0 -1007
- package/latticeai/runtime/build_phases.py +0 -1422
- package/latticeai/services/brain_intelligence.py +0 -967
- package/latticeai/services/memory_service.py +0 -1177
- package/latticeai/services/model_runtime.py +0 -1281
- package/latticeai/setup/wizard.py +0 -1277
- package/static/app/assets/Act-D0HWqtn0.js +0 -1
- package/static/app/assets/AdminConsole-D-QDW-A4.js +0 -1
- package/static/app/assets/BrainHome-Btns-_TA.js +0 -2
- package/static/app/assets/BrainSignals-2dHQNkns.js +0 -1
- package/static/app/assets/Capture-CT8v1StE.js +0 -1
- package/static/app/assets/CommandPalette-DoLXC2KH.js +0 -1
- package/static/app/assets/Library-DDoxFE5c.js +0 -1
- package/static/app/assets/ProductFlow-DOYf7JIs.js +0 -1
- package/static/app/assets/ReviewCard-COQsqidK.js +0 -3
- package/static/app/assets/System-BRllvYXd.js +0 -1
- package/static/app/assets/arrow-left-DnyMzss-.js +0 -1
- package/static/app/assets/brain-uMb_5hnO.js +0 -1
- package/static/app/assets/index-0AvoEBzJ.js +0 -10
- package/static/app/assets/input-B_5ZJ9oy.js +0 -1
- package/static/app/assets/primitives-CVwew78r.js +0 -1
- package/static/app/assets/search-DkhnOKZt.js +0 -1
- package/static/app/assets/utils-DcDMoZIe.js +0 -4
- 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.
|
|
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 &&
|
|
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",
|
package/scripts/bump_version.py
CHANGED
|
@@ -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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
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, [
|
|
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(
|
|
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
|
|
482
|
-
// will refuse to trust these screenshots
|
|
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**
|
|
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 = "
|
|
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
|
|
35
|
-
|
|
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 (
|
|
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
|
);
|