ltcai 6.0.0 → 6.2.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 +33 -35
- package/docs/CHANGELOG.md +68 -0
- package/docs/V4_1_FRONTEND_MIGRATION_REPORT.md +1 -1
- package/docs/V4_6_1_RELEASE_REFRESH_REPORT.md +1 -1
- package/docs/V4_7_0_ADMIN_SEPARATION_REPORT.md +1 -1
- package/docs/V4_7_1_ADMIN_OPERATIONS_REPORT.md +1 -1
- package/frontend/src/App.tsx +3 -1281
- package/frontend/src/components/LanguageSwitcher.tsx +23 -0
- package/frontend/src/components/ProductFlow.tsx +32 -662
- package/frontend/src/components/onboarding/ProductFlowScreens.tsx +688 -0
- package/frontend/src/features/admin/AdminConsole.tsx +294 -0
- package/frontend/src/features/brain/BrainHome.tsx +999 -0
- package/frontend/src/features/brain/brainData.ts +98 -0
- package/frontend/src/features/brain/graphLayout.ts +26 -0
- package/frontend/src/features/brain/types.ts +44 -0
- package/frontend/src/features/review/ReviewCard.tsx +15 -10
- package/frontend/src/i18n.ts +208 -0
- package/frontend/src/styles.css +240 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/chat.py +52 -33
- package/latticeai/api/tools.py +50 -23
- package/latticeai/app_factory.py +65 -47
- package/latticeai/cli/__init__.py +1 -0
- package/latticeai/cli/entrypoint.py +283 -0
- package/latticeai/cli/runtime.py +37 -0
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/integrations/__init__.py +0 -0
- package/latticeai/integrations/telegram_bot.py +1009 -0
- package/latticeai/runtime/lifespan_runtime.py +1 -1
- package/latticeai/runtime/platform_runtime_wiring.py +87 -0
- package/latticeai/runtime/router_registration.py +49 -30
- package/latticeai/services/app_context.py +1 -0
- package/latticeai/services/p_reinforce.py +258 -0
- package/latticeai/services/router_context.py +52 -0
- package/latticeai/services/tool_dispatch.py +82 -25
- package/ltcai_cli.py +7 -305
- package/p_reinforce.py +4 -255
- package/package.json +2 -1
- package/scripts/release_smoke.py +133 -0
- package/scripts/wheel_smoke.py +4 -0
- 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 +5 -5
- package/static/app/assets/{index-xRn29gI8.css → index-B2-1Gm0q.css} +1 -1
- package/static/app/assets/index-D91Rz5--.js +16 -0
- package/static/app/assets/index-D91Rz5--.js.map +1 -0
- package/static/app/index.html +2 -2
- package/telegram_bot.py +9 -1008
- package/static/app/assets/index-D2zafMYb.js +0 -16
- package/static/app/assets/index-D2zafMYb.js.map +0 -1
package/p_reinforce.py
CHANGED
|
@@ -1,258 +1,7 @@
|
|
|
1
|
-
"""
|
|
2
|
-
P-Reinforce Knowledge Gardener — notes capture with a brain-backed memory.
|
|
1
|
+
"""Compatibility shim for the historical root P-Reinforce module."""
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
The vault stays as the user-owned, Obsidian-compatible *mirror* (capability
|
|
6
|
-
preserved), but the Knowledge Graph is authoritative: notes created through
|
|
7
|
-
the API are ingested through the unified pipeline (provenance + hooks), the
|
|
8
|
-
existing vault is imported idempotently, and chat context comes from brain
|
|
9
|
-
queries instead of an O(n) vault scan per message.
|
|
10
|
-
"""
|
|
3
|
+
import sys
|
|
11
4
|
|
|
12
|
-
import
|
|
13
|
-
import os
|
|
14
|
-
import re
|
|
15
|
-
import shutil
|
|
16
|
-
from datetime import datetime
|
|
17
|
-
from pathlib import Path
|
|
18
|
-
from typing import Any, Optional
|
|
5
|
+
from latticeai.services import p_reinforce as _impl
|
|
19
6
|
|
|
20
|
-
|
|
21
|
-
os.getenv("LATTICEAI_OBSIDIAN_VAULT_DIR")
|
|
22
|
-
or os.getenv("LATTICEAI_BRAIN_DIR")
|
|
23
|
-
or Path.home() / ".ltcai-brain"
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
STRUCTURE = {
|
|
27
|
-
"10_Wiki": "검증된 지식, 개념 설명, 레퍼런스",
|
|
28
|
-
"00_Raw": "정제되지 않은 원시 데이터, 아이디어 메모",
|
|
29
|
-
"20_Skills": "재사용 가능한 코드 스니펫, 프롬프트, 워크플로",
|
|
30
|
-
"30_Projects": "프로젝트별 컨텍스트, 진행 상황",
|
|
31
|
-
"40_Log": "날짜별 작업 로그",
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class PReinforceGardener:
|
|
36
|
-
def __init__(self, ingestion_pipeline: Any = None, knowledge_graph: Any = None):
|
|
37
|
-
self._pipeline = ingestion_pipeline
|
|
38
|
-
self._kg = knowledge_graph
|
|
39
|
-
self._ensure_structure()
|
|
40
|
-
|
|
41
|
-
def _ensure_structure(self):
|
|
42
|
-
for folder in STRUCTURE:
|
|
43
|
-
(BRAIN_DIR / folder).mkdir(parents=True, exist_ok=True)
|
|
44
|
-
# 인덱스 파일
|
|
45
|
-
index_path = BRAIN_DIR / "INDEX.md"
|
|
46
|
-
if not index_path.exists():
|
|
47
|
-
index_path.write_text(self._render_index())
|
|
48
|
-
|
|
49
|
-
def _render_index(self) -> str:
|
|
50
|
-
lines = ["# 🧠 Lattice AI Brain — P-Reinforce Index\n"]
|
|
51
|
-
lines.append(f"*Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}*\n")
|
|
52
|
-
lines.append("\nThis folder is an Obsidian-compatible Markdown vault.\n")
|
|
53
|
-
lines.append("\nThe Knowledge Graph is the authoritative store; this vault is the\nuser-owned markdown mirror of garden notes.\n")
|
|
54
|
-
for folder, desc in STRUCTURE.items():
|
|
55
|
-
lines.append(f"## [{folder}](./{folder}/)\n_{desc}_\n")
|
|
56
|
-
lines.append("## Connector Status\n")
|
|
57
|
-
lines.append(f"- OCR engine: `{'tesseract' if shutil.which('tesseract') else 'not installed'}`\n")
|
|
58
|
-
return "\n".join(lines)
|
|
59
|
-
|
|
60
|
-
# ── Classify ──────────────────────────────────────────────────────────────
|
|
61
|
-
|
|
62
|
-
def _classify(self, text: str) -> str:
|
|
63
|
-
"""간단한 규칙 기반 분류 (LLM 없이도 동작)"""
|
|
64
|
-
text_lower = text.lower()
|
|
65
|
-
|
|
66
|
-
code_signals = ["def ", "class ", "import ", "```", "function ", "const ", "let ", "var "]
|
|
67
|
-
if any(s in text for s in code_signals):
|
|
68
|
-
return "20_Skills"
|
|
69
|
-
|
|
70
|
-
wiki_signals = ["개념", "원리", "이란", "what is", "how does", "definition", "explanation"]
|
|
71
|
-
if any(s in text_lower for s in wiki_signals):
|
|
72
|
-
return "10_Wiki"
|
|
73
|
-
|
|
74
|
-
project_signals = ["project", "프로젝트", "todo", "task", "작업", "기능", "feature"]
|
|
75
|
-
if any(s in text_lower for s in project_signals):
|
|
76
|
-
return "30_Projects"
|
|
77
|
-
|
|
78
|
-
return "00_Raw"
|
|
79
|
-
|
|
80
|
-
# ── File Naming ───────────────────────────────────────────────────────────
|
|
81
|
-
|
|
82
|
-
def _make_filename(self, text: str, folder: str) -> str:
|
|
83
|
-
# 첫 줄을 제목으로
|
|
84
|
-
first_line = text.strip().split("\n")[0][:60]
|
|
85
|
-
# 파일명 안전하게
|
|
86
|
-
safe = re.sub(r"[^\w\s-]", "", first_line).strip()
|
|
87
|
-
safe = re.sub(r"\s+", "_", safe)
|
|
88
|
-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
89
|
-
return f"{timestamp}_{safe or 'note'}.md"
|
|
90
|
-
|
|
91
|
-
# ── Process ───────────────────────────────────────────────────────────────
|
|
92
|
-
|
|
93
|
-
async def process(self, raw_data: str, category: Optional[str] = None) -> dict:
|
|
94
|
-
folder = category if category in STRUCTURE else self._classify(raw_data)
|
|
95
|
-
filename = self._make_filename(raw_data, folder)
|
|
96
|
-
filepath = BRAIN_DIR / folder / filename
|
|
97
|
-
|
|
98
|
-
# 마크다운 미러 (사용자 소유 Obsidian 호환 아티팩트)
|
|
99
|
-
content = self._wrap_markdown(raw_data, folder)
|
|
100
|
-
filepath.write_text(content, encoding="utf-8")
|
|
101
|
-
|
|
102
|
-
# 오늘 로그에도 기록
|
|
103
|
-
self._append_log(raw_data[:200], folder, filename)
|
|
104
|
-
|
|
105
|
-
result = {
|
|
106
|
-
"status": "saved",
|
|
107
|
-
"folder": folder,
|
|
108
|
-
"filename": filename,
|
|
109
|
-
"path": str(filepath),
|
|
110
|
-
"classified_as": folder,
|
|
111
|
-
"description": STRUCTURE[folder],
|
|
112
|
-
}
|
|
113
|
-
# 두뇌(Knowledge Graph)가 정식 저장소: 통합 수집 파이프라인으로 ingest.
|
|
114
|
-
result.update(self._ingest_note(raw_data, source_uri=str(filepath), folder=folder))
|
|
115
|
-
return result
|
|
116
|
-
|
|
117
|
-
def _ingest_note(self, text: str, *, source_uri: str, folder: str, title: Optional[str] = None) -> dict:
|
|
118
|
-
if self._pipeline is None:
|
|
119
|
-
return {"graph": "unavailable", "graph_detail": "ingestion pipeline not wired"}
|
|
120
|
-
try:
|
|
121
|
-
from lattice_brain.ingestion import IngestionItem
|
|
122
|
-
|
|
123
|
-
ingest = self._pipeline.ingest(
|
|
124
|
-
IngestionItem(
|
|
125
|
-
source_type="note",
|
|
126
|
-
title=title or text.strip().split("\n")[0][:80],
|
|
127
|
-
text=text,
|
|
128
|
-
source_uri=source_uri,
|
|
129
|
-
metadata={"garden_folder": folder, "pipeline": "p-reinforce"},
|
|
130
|
-
)
|
|
131
|
-
)
|
|
132
|
-
if ingest.status != "ok":
|
|
133
|
-
return {"graph": ingest.status, "graph_detail": ingest.detail}
|
|
134
|
-
return {
|
|
135
|
-
"graph": "ok",
|
|
136
|
-
"graph_node_id": ingest.node_id,
|
|
137
|
-
"provenance_id": ingest.provenance_id,
|
|
138
|
-
"duplicate": ingest.duplicate,
|
|
139
|
-
}
|
|
140
|
-
except Exception as exc:
|
|
141
|
-
logging.warning("garden note ingest failed: %s", exc)
|
|
142
|
-
return {"graph": "failed", "graph_detail": str(exc)}
|
|
143
|
-
|
|
144
|
-
def import_vault(self) -> dict:
|
|
145
|
-
"""Idempotent import of every existing vault note into the brain.
|
|
146
|
-
|
|
147
|
-
Content-hash dedup in the store makes re-runs safe; vault files are
|
|
148
|
-
never modified or deleted. INDEX.md and the daily logs are skipped.
|
|
149
|
-
"""
|
|
150
|
-
if self._pipeline is None:
|
|
151
|
-
return {"status": "unavailable", "imported": 0}
|
|
152
|
-
imported = duplicates = failed = 0
|
|
153
|
-
for file_path in sorted(BRAIN_DIR.rglob("*.md")):
|
|
154
|
-
if file_path.name == "INDEX.md" or "40_Log" in file_path.parts:
|
|
155
|
-
continue
|
|
156
|
-
try:
|
|
157
|
-
text = file_path.read_text(encoding="utf-8")
|
|
158
|
-
except Exception:
|
|
159
|
-
failed += 1
|
|
160
|
-
continue
|
|
161
|
-
folder = file_path.parent.name if file_path.parent != BRAIN_DIR else "00_Raw"
|
|
162
|
-
outcome = self._ingest_note(
|
|
163
|
-
text, source_uri=str(file_path), folder=folder, title=file_path.stem
|
|
164
|
-
)
|
|
165
|
-
if outcome.get("graph") == "ok":
|
|
166
|
-
if outcome.get("duplicate"):
|
|
167
|
-
duplicates += 1
|
|
168
|
-
else:
|
|
169
|
-
imported += 1
|
|
170
|
-
else:
|
|
171
|
-
failed += 1
|
|
172
|
-
if imported:
|
|
173
|
-
logging.info("garden: imported %d vault notes into the brain (%d already known)", imported, duplicates)
|
|
174
|
-
return {"status": "ok", "imported": imported, "duplicates": duplicates, "failed": failed}
|
|
175
|
-
|
|
176
|
-
def _wrap_markdown(self, raw: str, folder: str) -> str:
|
|
177
|
-
now = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
178
|
-
first_line = raw.strip().split("\n")[0][:80]
|
|
179
|
-
lines = [
|
|
180
|
-
f"# {first_line}",
|
|
181
|
-
f"\n> 📁 `{folder}` | 🕐 {now} | Lattice AI MLX\n",
|
|
182
|
-
"---\n",
|
|
183
|
-
raw,
|
|
184
|
-
"\n\n---",
|
|
185
|
-
"*Auto-organized by P-Reinforce Gardener*",
|
|
186
|
-
]
|
|
187
|
-
return "\n".join(lines)
|
|
188
|
-
|
|
189
|
-
def _append_log(self, preview: str, folder: str, filename: str):
|
|
190
|
-
today = datetime.now().strftime("%Y-%m-%d")
|
|
191
|
-
log_path = BRAIN_DIR / "40_Log" / f"{today}.md"
|
|
192
|
-
entry = f"\n- [{datetime.now().strftime('%H:%M')}] → `{folder}/{filename}`\n > {preview[:100]}\n"
|
|
193
|
-
with open(log_path, "a", encoding="utf-8") as f:
|
|
194
|
-
if log_path.stat().st_size == 0 if log_path.exists() else True:
|
|
195
|
-
f.write(f"# 📅 Log — {today}\n")
|
|
196
|
-
f.write(entry)
|
|
197
|
-
|
|
198
|
-
# ── Tree ──────────────────────────────────────────────────────────────────
|
|
199
|
-
|
|
200
|
-
def get_tree(self) -> dict:
|
|
201
|
-
"""지식 정원 파일트리 (마크다운 미러 기준)."""
|
|
202
|
-
folders = []
|
|
203
|
-
for folder, desc in STRUCTURE.items():
|
|
204
|
-
folder_path = BRAIN_DIR / folder
|
|
205
|
-
files = []
|
|
206
|
-
if folder_path.exists():
|
|
207
|
-
for file_path in sorted(folder_path.glob("*.md")):
|
|
208
|
-
try:
|
|
209
|
-
stat = file_path.stat()
|
|
210
|
-
files.append({
|
|
211
|
-
"name": file_path.name,
|
|
212
|
-
"size_bytes": stat.st_size,
|
|
213
|
-
"modified_at": datetime.fromtimestamp(stat.st_mtime).isoformat(timespec="seconds"),
|
|
214
|
-
})
|
|
215
|
-
except OSError:
|
|
216
|
-
continue
|
|
217
|
-
folders.append({"name": folder, "description": desc, "files": files, "count": len(files)})
|
|
218
|
-
return {"root": str(BRAIN_DIR), "folders": folders}
|
|
219
|
-
|
|
220
|
-
def get_relevant_context(self, query: str, limit: int = 3) -> str:
|
|
221
|
-
"""질문과 관련된 정원 노트를 두뇌에서 검색해 컨텍스트로 반환.
|
|
222
|
-
|
|
223
|
-
v4: 채팅마다 vault 전체를 rglob 하던 O(n) 스캔을 브레인 검색으로
|
|
224
|
-
대체. 그래프가 없으면(비활성) 기존 파일 스캔으로 정직하게 폴백.
|
|
225
|
-
"""
|
|
226
|
-
if self._kg is not None:
|
|
227
|
-
try:
|
|
228
|
-
matches = self._kg.search(query, max(limit * 4, 8)).get("matches", [])
|
|
229
|
-
results = []
|
|
230
|
-
for match in matches:
|
|
231
|
-
meta = match.get("metadata") or {}
|
|
232
|
-
if not (meta.get("garden_folder") or meta.get("pipeline") == "p-reinforce"):
|
|
233
|
-
continue
|
|
234
|
-
title = match.get("title") or "note"
|
|
235
|
-
body = match.get("summary") or ""
|
|
236
|
-
results.append(f"--- Document: {title} ---\n{body[:800]}")
|
|
237
|
-
if len(results) >= limit:
|
|
238
|
-
break
|
|
239
|
-
return "\n\n".join(results)
|
|
240
|
-
except Exception as exc:
|
|
241
|
-
logging.debug("garden brain context failed, falling back to vault scan: %s", exc)
|
|
242
|
-
return self._scan_vault_context(query, limit)
|
|
243
|
-
|
|
244
|
-
def _scan_vault_context(self, query: str, limit: int = 3) -> str:
|
|
245
|
-
results = []
|
|
246
|
-
for file_path in BRAIN_DIR.rglob("*.md"):
|
|
247
|
-
if file_path.name == "INDEX.md" or "40_Log" in str(file_path):
|
|
248
|
-
continue
|
|
249
|
-
try:
|
|
250
|
-
content = file_path.read_text(encoding="utf-8")
|
|
251
|
-
keywords = [k for k in re.split(r"\s+", query) if len(k) > 1]
|
|
252
|
-
if any(k.lower() in content.lower() for k in keywords):
|
|
253
|
-
results.append(f"--- Document: {file_path.name} ---\n{content[:800]}")
|
|
254
|
-
if len(results) >= limit:
|
|
255
|
-
break
|
|
256
|
-
except Exception:
|
|
257
|
-
continue
|
|
258
|
-
return "\n\n".join(results)
|
|
7
|
+
sys.modules[__name__] = _impl
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ltcai",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.2.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": {
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"package:vsix": "node scripts/build_vsix.mjs",
|
|
41
41
|
"release:artifacts": "node scripts/clean_release_artifacts.mjs $npm_package_version && npm run build:assets && npm run build:python && npm pack && npm run package:vsix && npm run desktop:tauri:build",
|
|
42
42
|
"release:validate": "node scripts/run_python.mjs scripts/validate_release_artifacts.py $npm_package_version --require-vsix --require-tgz --require-dmg",
|
|
43
|
+
"release:smoke": "node scripts/run_python.mjs scripts/release_smoke.py $npm_package_version",
|
|
43
44
|
"publish:npm": "npm pack && npm publish ltcai-$npm_package_version.tgz --access public",
|
|
44
45
|
"publish:pypi": "npm run build:python && node scripts/run_python.mjs -m twine upload --skip-existing dist/ltcai-$npm_package_version.tar.gz dist/ltcai-$npm_package_version-py3-none-any.whl",
|
|
45
46
|
"publish:vscode": "cd vscode-extension && npm run package:vsix && npm run publish:vscode",
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Release smoke checks for exact-version release artifacts.
|
|
3
|
+
|
|
4
|
+
This complements ``validate_release_artifacts.py`` by opening the generated
|
|
5
|
+
artifacts and proving the installable surfaces are coherent:
|
|
6
|
+
|
|
7
|
+
- wheel installs/imports in a fresh environment via ``wheel_smoke.py``
|
|
8
|
+
- npm tgz contains the package metadata, CLI bin, and static assets
|
|
9
|
+
- static asset manifest points at files that exist on disk
|
|
10
|
+
- Tauri DMG/app bundle exists, and can optionally be launched briefly
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
import tarfile
|
|
21
|
+
import time
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def run(cmd: list[str], **kwargs) -> None:
|
|
28
|
+
print("+", " ".join(str(item) for item in cmd), flush=True)
|
|
29
|
+
subprocess.run(cmd, check=True, **kwargs)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def smoke_wheel(version: str, skip_health: bool) -> None:
|
|
33
|
+
wheel = REPO_ROOT / "dist" / f"ltcai-{version}-py3-none-any.whl"
|
|
34
|
+
if not wheel.is_file():
|
|
35
|
+
raise SystemExit(f"missing wheel: {wheel}")
|
|
36
|
+
cmd = [sys.executable, str(REPO_ROOT / "scripts" / "wheel_smoke.py"), "--wheel", str(wheel)]
|
|
37
|
+
if skip_health:
|
|
38
|
+
cmd.append("--skip-health")
|
|
39
|
+
run(cmd, cwd=REPO_ROOT)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def smoke_npm_tgz(version: str) -> None:
|
|
43
|
+
tgz = REPO_ROOT / f"ltcai-{version}.tgz"
|
|
44
|
+
if not tgz.is_file():
|
|
45
|
+
raise SystemExit(f"missing npm tgz: {tgz}")
|
|
46
|
+
required = {
|
|
47
|
+
"package/package.json",
|
|
48
|
+
"package/bin/ltcai.js",
|
|
49
|
+
"package/static/app/index.html",
|
|
50
|
+
"package/static/app/asset-manifest.json",
|
|
51
|
+
}
|
|
52
|
+
with tarfile.open(tgz, "r:gz") as archive:
|
|
53
|
+
names = set(archive.getnames())
|
|
54
|
+
missing = sorted(required - names)
|
|
55
|
+
if missing:
|
|
56
|
+
raise SystemExit(f"{tgz.name} missing entries: {missing}")
|
|
57
|
+
package_data = json.loads(archive.extractfile("package/package.json").read().decode("utf-8")) # type: ignore[union-attr]
|
|
58
|
+
if package_data.get("version") != version:
|
|
59
|
+
raise SystemExit(f"{tgz.name} package.json version {package_data.get('version')!r} != {version!r}")
|
|
60
|
+
print(f"npm tgz smoke ok: {tgz.name}")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def smoke_static_assets(version: str) -> None:
|
|
64
|
+
manifest_path = REPO_ROOT / "static" / "app" / "asset-manifest.json"
|
|
65
|
+
if not manifest_path.is_file():
|
|
66
|
+
raise SystemExit(f"missing static manifest: {manifest_path}")
|
|
67
|
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
68
|
+
if manifest.get("version") != version:
|
|
69
|
+
raise SystemExit(f"static manifest version {manifest.get('version')!r} != {version!r}")
|
|
70
|
+
required_paths = {"/static/app/index.html"}
|
|
71
|
+
required_paths.update(str(path) for path in manifest.get("assets", {}).values())
|
|
72
|
+
missing = []
|
|
73
|
+
for static_path in required_paths:
|
|
74
|
+
rel = static_path.removeprefix("/static/")
|
|
75
|
+
path = REPO_ROOT / "static" / rel
|
|
76
|
+
if not path.is_file():
|
|
77
|
+
missing.append(static_path)
|
|
78
|
+
if missing:
|
|
79
|
+
raise SystemExit(f"static manifest references missing files: {missing}")
|
|
80
|
+
print(f"static asset smoke ok: {len(required_paths)} files")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def smoke_tauri(version: str, launch: bool) -> None:
|
|
84
|
+
dmg = REPO_ROOT / "src-tauri" / "target" / "release" / "bundle" / "dmg" / f"Lattice AI_{version}_aarch64.dmg"
|
|
85
|
+
app = REPO_ROOT / "src-tauri" / "target" / "release" / "bundle" / "macos" / "Lattice AI.app"
|
|
86
|
+
macos_dir = app / "Contents" / "MacOS"
|
|
87
|
+
executable = app / "Contents" / "MacOS" / "Lattice AI"
|
|
88
|
+
if not executable.exists() and macos_dir.is_dir():
|
|
89
|
+
candidates = [path for path in macos_dir.iterdir() if path.is_file() and os.access(path, os.X_OK)]
|
|
90
|
+
if candidates:
|
|
91
|
+
executable = candidates[0]
|
|
92
|
+
missing = [path for path in (dmg, app, executable) if not path.exists()]
|
|
93
|
+
if missing:
|
|
94
|
+
raise SystemExit("missing Tauri artifact(s): " + ", ".join(str(path) for path in missing))
|
|
95
|
+
if launch:
|
|
96
|
+
env = {
|
|
97
|
+
**os.environ,
|
|
98
|
+
"LATTICEAI_ENABLE_TELEGRAM": "false",
|
|
99
|
+
"LATTICEAI_AUTOLOAD_MODELS": "false",
|
|
100
|
+
}
|
|
101
|
+
proc = subprocess.Popen([str(executable)], cwd=REPO_ROOT, env=env)
|
|
102
|
+
time.sleep(4)
|
|
103
|
+
if proc.poll() not in (None, 0):
|
|
104
|
+
raise SystemExit(f"Tauri executable exited early with {proc.returncode}")
|
|
105
|
+
if proc.poll() is None:
|
|
106
|
+
proc.terminate()
|
|
107
|
+
try:
|
|
108
|
+
proc.wait(timeout=5)
|
|
109
|
+
except subprocess.TimeoutExpired:
|
|
110
|
+
proc.kill()
|
|
111
|
+
proc.wait(timeout=5)
|
|
112
|
+
print("Tauri launch smoke ok")
|
|
113
|
+
else:
|
|
114
|
+
print(f"Tauri artifact smoke ok: {dmg.name}")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def main() -> int:
|
|
118
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
119
|
+
parser.add_argument("version", help="exact version to smoke, e.g. 6.2.0")
|
|
120
|
+
parser.add_argument("--skip-wheel-health", action="store_true", help="skip FastAPI /health check inside wheel smoke")
|
|
121
|
+
parser.add_argument("--launch-tauri", action="store_true", help="briefly launch the built Tauri executable")
|
|
122
|
+
args = parser.parse_args()
|
|
123
|
+
|
|
124
|
+
smoke_wheel(args.version, args.skip_wheel_health)
|
|
125
|
+
smoke_npm_tgz(args.version)
|
|
126
|
+
smoke_static_assets(args.version)
|
|
127
|
+
smoke_tauri(args.version, args.launch_tauri)
|
|
128
|
+
print(f"release smoke passed for v{args.version}")
|
|
129
|
+
return 0
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if __name__ == "__main__":
|
|
133
|
+
raise SystemExit(main())
|
package/scripts/wheel_smoke.py
CHANGED
|
@@ -46,9 +46,13 @@ WHEEL_MODULES = [
|
|
|
46
46
|
"latticeai",
|
|
47
47
|
"latticeai.server_app",
|
|
48
48
|
"latticeai.app_factory",
|
|
49
|
+
"latticeai.cli.entrypoint",
|
|
50
|
+
"latticeai.integrations",
|
|
51
|
+
"latticeai.integrations.telegram_bot",
|
|
49
52
|
"latticeai.models.router",
|
|
50
53
|
"latticeai.core.mcp_registry",
|
|
51
54
|
"latticeai.api.knowledge_graph",
|
|
55
|
+
"latticeai.services.p_reinforce",
|
|
52
56
|
"ltcai_cli",
|
|
53
57
|
"auto_setup",
|
|
54
58
|
"server",
|
package/src-tauri/Cargo.lock
CHANGED
package/src-tauri/Cargo.toml
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "6.
|
|
2
|
+
"version": "6.2.0",
|
|
3
3
|
"generated_at": "vite",
|
|
4
4
|
"entrypoints": {
|
|
5
5
|
"app": "/static/app/index.html"
|
|
6
6
|
},
|
|
7
7
|
"assets": {
|
|
8
8
|
"../node_modules/@tauri-apps/api/core.js": "/static/app/assets/core-CwxXejkd.js",
|
|
9
|
-
"index.html": "/static/app/assets/index-
|
|
10
|
-
"assets/index-
|
|
9
|
+
"index.html": "/static/app/assets/index-D91Rz5--.js",
|
|
10
|
+
"assets/index-B2-1Gm0q.css": "/static/app/assets/index-B2-1Gm0q.css"
|
|
11
11
|
},
|
|
12
12
|
"vite": {
|
|
13
13
|
"../node_modules/@tauri-apps/api/core.js": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"isDynamicEntry": true
|
|
18
18
|
},
|
|
19
19
|
"index.html": {
|
|
20
|
-
"file": "assets/index-
|
|
20
|
+
"file": "assets/index-D91Rz5--.js",
|
|
21
21
|
"name": "index",
|
|
22
22
|
"src": "index.html",
|
|
23
23
|
"isEntry": true,
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"../node_modules/@tauri-apps/api/core.js"
|
|
26
26
|
],
|
|
27
27
|
"css": [
|
|
28
|
-
"assets/index-
|
|
28
|
+
"assets/index-B2-1Gm0q.css"
|
|
29
29
|
]
|
|
30
30
|
}
|
|
31
31
|
}
|