ltcai 3.5.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -35
- package/docs/CARRYOVER_AUDIT_v3.6.0.md +61 -0
- package/docs/CHANGELOG.md +32 -0
- package/docs/HANDOVER_v3.6.0.md +46 -0
- package/docs/RUNTIME_HOOK_COVERAGE_v3.6.0.md +49 -0
- package/docs/V4_BRAIN_ARCHITECTURE.md +322 -0
- package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +509 -0
- package/docs/V4_IMPLEMENTATION_PLAN.md +470 -0
- package/docs/architecture.md +13 -12
- package/docs/kg-schema.md +102 -53
- package/docs/privacy.md +18 -2
- package/docs/security-model.md +17 -0
- package/kg_schema.py +139 -10
- package/knowledge_graph.py +874 -26
- package/knowledge_graph_api.py +11 -127
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +1 -1
- package/latticeai/api/agents.py +7 -1
- package/latticeai/api/auth.py +27 -4
- package/latticeai/api/browser.py +217 -0
- package/latticeai/api/chat.py +112 -76
- package/latticeai/api/health.py +1 -1
- package/latticeai/api/hooks.py +1 -1
- package/latticeai/api/knowledge_graph.py +146 -0
- package/latticeai/api/local_files.py +1 -1
- package/latticeai/api/mcp.py +23 -11
- package/latticeai/api/memory.py +1 -1
- package/latticeai/api/models.py +1 -1
- package/latticeai/api/network.py +81 -0
- package/latticeai/api/portability.py +93 -0
- package/latticeai/api/realtime.py +1 -1
- package/latticeai/api/search.py +26 -2
- package/latticeai/api/security_dashboard.py +2 -3
- package/latticeai/api/setup.py +2 -2
- package/latticeai/api/static_routes.py +2 -4
- package/latticeai/api/tools.py +3 -0
- package/latticeai/api/workflow_designer.py +46 -0
- package/latticeai/api/workspace.py +71 -49
- package/latticeai/app_factory.py +1710 -0
- package/latticeai/brain/__init__.py +18 -0
- package/latticeai/brain/context.py +213 -0
- package/latticeai/brain/conversations.py +236 -0
- package/latticeai/brain/identity.py +175 -0
- package/latticeai/brain/memory.py +102 -0
- package/latticeai/brain/network.py +205 -0
- package/latticeai/core/agent.py +31 -7
- package/latticeai/core/audit.py +0 -7
- package/latticeai/core/config.py +1 -1
- package/latticeai/core/context_builder.py +1 -2
- package/latticeai/core/enterprise.py +1 -1
- package/latticeai/core/graph_curator.py +2 -2
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/mcp_registry.py +791 -0
- package/latticeai/core/model_compat.py +1 -1
- package/latticeai/core/model_resolution.py +0 -1
- package/latticeai/core/multi_agent.py +238 -4
- package/latticeai/core/security.py +1 -1
- package/latticeai/core/sessions.py +37 -7
- package/latticeai/core/workflow_engine.py +114 -2
- package/latticeai/core/workspace_os.py +58 -10
- package/latticeai/models/__init__.py +7 -0
- package/latticeai/models/router.py +779 -0
- package/latticeai/server_app.py +29 -1504
- package/latticeai/services/agent_runtime.py +1 -0
- package/latticeai/services/app_context.py +75 -14
- package/latticeai/services/ingestion.py +318 -0
- package/latticeai/services/kg_portability.py +207 -0
- package/latticeai/services/memory_service.py +39 -11
- package/latticeai/services/model_runtime.py +2 -5
- package/latticeai/services/platform_runtime.py +100 -23
- package/latticeai/services/search_service.py +17 -8
- package/latticeai/services/tool_dispatch.py +12 -2
- package/latticeai/services/triggers.py +241 -0
- package/latticeai/services/upload_service.py +37 -12
- package/latticeai/services/workspace_service.py +31 -0
- package/llm_router.py +29 -772
- package/ltcai_cli.py +1 -2
- package/mcp_registry.py +25 -788
- package/p_reinforce.py +124 -14
- package/package.json +11 -8
- package/scripts/build_vsix.mjs +72 -0
- package/scripts/bump_version.py +99 -0
- package/scripts/generate_diagrams.py +0 -1
- package/scripts/lint_v3.mjs +82 -18
- package/scripts/validate_release_artifacts.py +0 -1
- package/scripts/wheel_smoke.py +142 -0
- package/server.py +11 -7
- package/setup_wizard.py +1142 -0
- package/static/account.html +2 -4
- package/static/admin.html +3 -5
- package/static/chat.html +3 -6
- package/static/graph.html +2 -4
- package/static/sw.js +81 -52
- package/static/v3/asset-manifest.json +20 -19
- package/static/v3/css/{lattice.base.e4cdd05d.css → lattice.base.49deefb5.css} +1 -1
- package/static/v3/css/lattice.base.css +1 -1
- package/static/v3/css/{lattice.components.9b49d614.css → lattice.components.cde18231.css} +1 -1
- package/static/v3/css/lattice.components.css +1 -1
- package/static/v3/css/{lattice.shell.8fcc9d33.css → lattice.shell.29d36d85.css} +1 -1
- package/static/v3/css/lattice.shell.css +1 -1
- package/static/v3/css/{lattice.tokens.e7018963.css → lattice.tokens.304cbc40.css} +3 -0
- package/static/v3/css/lattice.tokens.css +3 -0
- package/static/v3/css/{lattice.views.22f69117.css → lattice.views.0a18b6c5.css} +2 -2
- package/static/v3/css/lattice.views.css +2 -2
- package/static/v3/index.html +3 -4
- package/static/v3/js/{app.d086489d.js → app.356e6452.js} +1 -1
- package/static/v3/js/core/{api.12b568ad.js → api.7a308b89.js} +39 -1
- package/static/v3/js/core/api.js +38 -0
- package/static/v3/js/core/{routes.d214b399.js → routes.7222343d.js} +22 -22
- package/static/v3/js/core/routes.js +22 -22
- package/static/v3/js/core/{shell.d05266f5.js → shell.a1657f20.js} +4 -4
- package/static/v3/js/core/shell.js +1 -1
- package/static/v3/js/core/{store.34ebd5e6.js → store.204a08b2.js} +1 -1
- package/static/v3/js/core/store.js +1 -1
- package/static/v3/js/views/graph-canvas.17c15d65.js +509 -0
- package/static/v3/js/views/graph-canvas.js +509 -0
- package/static/v3/js/views/{hybrid-search.b22b97e0.js → hybrid-search.2fb63ed9.js} +1 -2
- package/static/v3/js/views/hybrid-search.js +1 -2
- package/static/v3/js/views/knowledge-graph.5e40cbeb.js +509 -0
- package/static/v3/js/views/knowledge-graph.js +326 -54
- package/static/vendor/chart.umd.min.js +20 -0
- package/static/vendor/fonts/inter-latin-300-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-400-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-500-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-600-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-700-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-800-normal.woff2 +0 -0
- package/static/vendor/fonts/inter.css +44 -0
- package/static/vendor/icons/tabler-icons.min.css +4 -0
- package/static/vendor/icons/tabler-icons.woff2 +0 -0
- package/static/vendor/marked.min.js +69 -0
- package/static/workspace.html +2 -2
- package/telegram_bot.py +1 -2
- package/tools/commands.py +4 -2
- package/tools/computer.py +1 -1
- package/tools/documents.py +1 -3
- package/tools/filesystem.py +0 -4
- package/tools/knowledge.py +1 -3
- package/tools/network.py +1 -3
- package/codex_telegram_bot.py +0 -195
- package/docs/assets/v3.4.0/agent-run.png +0 -0
- package/docs/assets/v3.4.0/agents.png +0 -0
- package/docs/assets/v3.4.0/before/chat-before.png +0 -0
- package/docs/assets/v3.4.0/before/files-before.png +0 -0
- package/docs/assets/v3.4.0/chat.png +0 -0
- package/docs/assets/v3.4.0/connect-folder.png +0 -0
- package/docs/assets/v3.4.0/files.png +0 -0
- package/docs/assets/v3.4.0/home.png +0 -0
- package/docs/assets/v3.4.0/hooks-dispatch.png +0 -0
- package/docs/assets/v3.4.0/knowledge-graph.png +0 -0
- package/docs/assets/v3.4.0/local-agent.png +0 -0
- package/docs/assets/v3.4.0/memory.png +0 -0
- package/docs/assets/v3.4.0/settings.png +0 -0
- package/docs/assets/v3.4.0/vision-input.png +0 -0
- package/docs/assets/v3.4.0/workflows.png +0 -0
- package/docs/assets/v3.4.1/e2e_runtime_log.txt +0 -42
- package/docs/assets/v3.4.1/hooks-dispatch.png +0 -0
- package/docs/assets/v3.4.1/local-agent.png +0 -0
- package/docs/images/admin-dashboard.png +0 -0
- package/docs/images/architecture.png +0 -0
- package/docs/images/enterprise.png +0 -0
- package/docs/images/graph.png +0 -0
- package/docs/images/hero.gif +0 -0
- package/docs/images/knowledge-graph.png +0 -0
- package/docs/images/lattice-ai-demo.gif +0 -0
- package/docs/images/lattice-ai-hero.png +0 -0
- package/docs/images/logo.svg +0 -33
- package/docs/images/mobile-responsive.png +0 -0
- package/docs/images/model-recommendation.png +0 -0
- package/docs/images/onboarding.png +0 -0
- package/docs/images/organization.png +0 -0
- package/docs/images/pipeline.png +0 -0
- package/docs/images/screenshot-admin.png +0 -0
- package/docs/images/screenshot-chat.png +0 -0
- package/docs/images/screenshot-graph.png +0 -0
- package/docs/images/skills.png +0 -0
- package/docs/images/workspace-dark.png +0 -0
- package/docs/images/workspace-light.png +0 -0
- package/docs/images/workspace.png +0 -0
- package/requirements.txt +0 -16
- package/static/v3/js/views/knowledge-graph.a14ea7e7.js +0 -237
package/p_reinforce.py
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
"""
|
|
2
|
-
P-Reinforce Knowledge Gardener
|
|
3
|
-
|
|
2
|
+
P-Reinforce Knowledge Gardener — notes capture with a brain-backed memory.
|
|
3
|
+
|
|
4
|
+
v4 (T4.3 garden absorption): the markdown vault is no longer a second brain.
|
|
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.
|
|
4
10
|
"""
|
|
5
11
|
|
|
6
|
-
import
|
|
12
|
+
import logging
|
|
7
13
|
import os
|
|
8
14
|
import re
|
|
9
|
-
import time
|
|
10
15
|
import shutil
|
|
11
16
|
from datetime import datetime
|
|
12
17
|
from pathlib import Path
|
|
13
|
-
from typing import Optional
|
|
18
|
+
from typing import Any, Optional
|
|
14
19
|
|
|
15
20
|
BRAIN_DIR = Path(
|
|
16
21
|
os.getenv("LATTICEAI_OBSIDIAN_VAULT_DIR")
|
|
@@ -28,7 +33,9 @@ STRUCTURE = {
|
|
|
28
33
|
|
|
29
34
|
|
|
30
35
|
class PReinforceGardener:
|
|
31
|
-
def __init__(self):
|
|
36
|
+
def __init__(self, ingestion_pipeline: Any = None, knowledge_graph: Any = None):
|
|
37
|
+
self._pipeline = ingestion_pipeline
|
|
38
|
+
self._kg = knowledge_graph
|
|
32
39
|
self._ensure_structure()
|
|
33
40
|
|
|
34
41
|
def _ensure_structure(self):
|
|
@@ -43,6 +50,7 @@ class PReinforceGardener:
|
|
|
43
50
|
lines = ["# 🧠 Lattice AI Brain — P-Reinforce Index\n"]
|
|
44
51
|
lines.append(f"*Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}*\n")
|
|
45
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")
|
|
46
54
|
for folder, desc in STRUCTURE.items():
|
|
47
55
|
lines.append(f"## [{folder}](./{folder}/)\n_{desc}_\n")
|
|
48
56
|
lines.append("## Connector Status\n")
|
|
@@ -87,14 +95,14 @@ class PReinforceGardener:
|
|
|
87
95
|
filename = self._make_filename(raw_data, folder)
|
|
88
96
|
filepath = BRAIN_DIR / folder / filename
|
|
89
97
|
|
|
90
|
-
# 마크다운
|
|
98
|
+
# 마크다운 미러 (사용자 소유 Obsidian 호환 아티팩트)
|
|
91
99
|
content = self._wrap_markdown(raw_data, folder)
|
|
92
100
|
filepath.write_text(content, encoding="utf-8")
|
|
93
101
|
|
|
94
102
|
# 오늘 로그에도 기록
|
|
95
103
|
self._append_log(raw_data[:200], folder, filename)
|
|
96
104
|
|
|
97
|
-
|
|
105
|
+
result = {
|
|
98
106
|
"status": "saved",
|
|
99
107
|
"folder": folder,
|
|
100
108
|
"filename": filename,
|
|
@@ -102,6 +110,68 @@ class PReinforceGardener:
|
|
|
102
110
|
"classified_as": folder,
|
|
103
111
|
"description": STRUCTURE[folder],
|
|
104
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 latticeai.services.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}
|
|
105
175
|
|
|
106
176
|
def _wrap_markdown(self, raw: str, folder: str) -> str:
|
|
107
177
|
now = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
@@ -112,7 +182,7 @@ class PReinforceGardener:
|
|
|
112
182
|
"---\n",
|
|
113
183
|
raw,
|
|
114
184
|
"\n\n---",
|
|
115
|
-
|
|
185
|
+
"*Auto-organized by P-Reinforce Gardener*",
|
|
116
186
|
]
|
|
117
187
|
return "\n".join(lines)
|
|
118
188
|
|
|
@@ -127,22 +197,62 @@ class PReinforceGardener:
|
|
|
127
197
|
|
|
128
198
|
# ── Tree ──────────────────────────────────────────────────────────────────
|
|
129
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
|
+
|
|
130
220
|
def get_relevant_context(self, query: str, limit: int = 3) -> str:
|
|
131
|
-
"""질문과 관련된
|
|
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:
|
|
132
245
|
results = []
|
|
133
|
-
# 모든 마크다운 파일 탐색 (INDEX.md 및 Log 제외)
|
|
134
246
|
for file_path in BRAIN_DIR.rglob("*.md"):
|
|
135
247
|
if file_path.name == "INDEX.md" or "40_Log" in str(file_path):
|
|
136
248
|
continue
|
|
137
|
-
|
|
138
249
|
try:
|
|
139
250
|
content = file_path.read_text(encoding="utf-8")
|
|
140
|
-
keywords = [k for k in re.split(r
|
|
251
|
+
keywords = [k for k in re.split(r"\s+", query) if len(k) > 1]
|
|
141
252
|
if any(k.lower() in content.lower() for k in keywords):
|
|
142
253
|
results.append(f"--- Document: {file_path.name} ---\n{content[:800]}")
|
|
143
254
|
if len(results) >= limit:
|
|
144
255
|
break
|
|
145
256
|
except Exception:
|
|
146
257
|
continue
|
|
147
|
-
|
|
148
258
|
return "\n\n".join(results)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ltcai",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Lattice AI
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "Lattice AI — local-first Digital Brain Platform (knowledge graph, durable memory, hybrid search, agents, signed brain exchange)",
|
|
5
5
|
"homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
"capture:skills": "node scripts/capture/capture_skills.js",
|
|
34
34
|
"capture:enterprise": "node scripts/capture/capture_enterprise.js",
|
|
35
35
|
"capture:onboarding": "node scripts/capture/capture_onboarding.js",
|
|
36
|
-
"
|
|
36
|
+
"package:vsix": "node scripts/build_vsix.mjs",
|
|
37
|
+
"release:artifacts": "npm run build:assets && npm run build:python && npm pack && npm run package:vsix",
|
|
37
38
|
"release:validate": "python3 scripts/validate_release_artifacts.py $npm_package_version --require-vsix --require-tgz",
|
|
38
39
|
"publish:npm": "npm pack && npm publish ltcai-$npm_package_version.tgz --access public",
|
|
39
40
|
"publish:pypi": "npm run build:python && python3 -m twine upload --skip-existing dist/ltcai-$npm_package_version.tar.gz dist/ltcai-$npm_package_version-py3-none-any.whl",
|
|
@@ -65,13 +66,11 @@
|
|
|
65
66
|
"server.py",
|
|
66
67
|
"kg_schema.py",
|
|
67
68
|
"knowledge_graph.py",
|
|
68
|
-
"knowledge_graph_api.py",
|
|
69
69
|
"local_knowledge_api.py",
|
|
70
70
|
"llm_router.py",
|
|
71
71
|
"p_reinforce.py",
|
|
72
72
|
"telegram_bot.py",
|
|
73
73
|
"tools/",
|
|
74
|
-
"codex_telegram_bot.py",
|
|
75
74
|
"mcp_registry.py",
|
|
76
75
|
"latticeai/**/*.py",
|
|
77
76
|
"skills/",
|
|
@@ -95,12 +94,16 @@
|
|
|
95
94
|
"static/icons/",
|
|
96
95
|
"plugins/",
|
|
97
96
|
"scripts/",
|
|
98
|
-
"docs/",
|
|
99
97
|
"!docs/images/tmp_frames/",
|
|
100
98
|
"!**/__pycache__/",
|
|
101
99
|
"!**/*.pyc",
|
|
102
|
-
"
|
|
103
|
-
"
|
|
100
|
+
"README.md",
|
|
101
|
+
"setup_wizard.py",
|
|
102
|
+
"knowledge_graph_api.py",
|
|
103
|
+
"static/vendor/",
|
|
104
|
+
"docs/*.md",
|
|
105
|
+
"!docs/assets/",
|
|
106
|
+
"!docs/images/"
|
|
104
107
|
],
|
|
105
108
|
"publishConfig": {
|
|
106
109
|
"access": "public"
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Build the VS Code extension VSIX into `dist/ltcai-<version>.vsix`.
|
|
4
|
+
*
|
|
5
|
+
* Why a script instead of an inline `cd vscode-extension && npm run package:vsix`:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Single version source.** The output name is derived from the ROOT
|
|
8
|
+
* `package.json` version — the same value `release:validate` checks. The old
|
|
9
|
+
* inline form used the extension's own `$npm_package_version`, so a version
|
|
10
|
+
* drift between root and extension produced a `dist/ltcai-<ext>.vsix` that the
|
|
11
|
+
* validator (run from root) reported as a *missing* `dist/ltcai-<root>.vsix`.
|
|
12
|
+
* 2. **Fresh-checkout / CI safe.** It installs the extension's toolchain
|
|
13
|
+
* (`tsc`, `vsce`) when `node_modules` is absent, so the artifact builds on a
|
|
14
|
+
* clean clone — not only on a warmed-up dev tree.
|
|
15
|
+
* 3. **Fails loudly.** It verifies the compiled entrypoint and the final VSIX
|
|
16
|
+
* exist, exiting non-zero otherwise, so a skipped compile can't yield a
|
|
17
|
+
* silently-empty or missing artifact.
|
|
18
|
+
*
|
|
19
|
+
* Mirrors the tag-driven `.github/workflows/release.yml` VSIX step.
|
|
20
|
+
*/
|
|
21
|
+
import { execFileSync } from "node:child_process";
|
|
22
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
23
|
+
import { dirname, join, resolve } from "node:path";
|
|
24
|
+
import { fileURLToPath } from "node:url";
|
|
25
|
+
|
|
26
|
+
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
27
|
+
const extDir = join(repoRoot, "vscode-extension");
|
|
28
|
+
const distDir = join(repoRoot, "dist");
|
|
29
|
+
|
|
30
|
+
const version = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8")).version;
|
|
31
|
+
const extVersion = JSON.parse(readFileSync(join(extDir, "package.json"), "utf8")).version;
|
|
32
|
+
if (extVersion !== version) {
|
|
33
|
+
console.error(
|
|
34
|
+
`build_vsix: version drift — root package.json is ${version} but ` +
|
|
35
|
+
`vscode-extension/package.json is ${extVersion}. Bump both to the same value.`
|
|
36
|
+
);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const outFile = join(distDir, `ltcai-${version}.vsix`);
|
|
41
|
+
const binExt = process.platform === "win32" ? ".cmd" : "";
|
|
42
|
+
|
|
43
|
+
mkdirSync(distDir, { recursive: true });
|
|
44
|
+
|
|
45
|
+
function run(cmd, args, cwd) {
|
|
46
|
+
console.log(`$ (cd ${cwd && cwd.replace(repoRoot, ".")}) ${cmd} ${args.join(" ")}`);
|
|
47
|
+
execFileSync(cmd, args, { cwd, stdio: "inherit" });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 1) Ensure the extension toolchain (tsc + vsce) is installed.
|
|
51
|
+
if (!existsSync(join(extDir, "node_modules", ".bin", `vsce${binExt}`))) {
|
|
52
|
+
const installCmd = existsSync(join(extDir, "package-lock.json")) ? "ci" : "install";
|
|
53
|
+
run(`npm${binExt}`, [installCmd, "--no-audit", "--no-fund"], extDir);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 2) Compile and assert the entrypoint exists (vsce also runs vscode:prepublish,
|
|
57
|
+
// but we fail fast and explicitly here for a clearer error).
|
|
58
|
+
run(`npm${binExt}`, ["run", "compile"], extDir);
|
|
59
|
+
if (!existsSync(join(extDir, "out", "extension.js"))) {
|
|
60
|
+
console.error("build_vsix: vscode-extension/out/extension.js missing after compile");
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 3) Package to the root-version-scoped path. `--no-yarn` matches release.yml.
|
|
65
|
+
run(join(extDir, "node_modules", ".bin", `vsce${binExt}`), ["package", "--no-yarn", "-o", outFile], extDir);
|
|
66
|
+
|
|
67
|
+
// 4) Verify the artifact landed where release:validate expects it.
|
|
68
|
+
if (!existsSync(outFile)) {
|
|
69
|
+
console.error(`build_vsix: expected artifact not found: ${outFile}`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
console.log(`build_vsix: wrote ${outFile.replace(repoRoot, ".")}`);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Single-source version bump — rewrites every synchronized version copy.
|
|
3
|
+
|
|
4
|
+
The canonical version has always been nine hand-edited copies guarded by
|
|
5
|
+
tests/unit/test_version_consistency.py. This script makes the bump one
|
|
6
|
+
command; the consistency test keeps guarding the result.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python scripts/bump_version.py 4.0.0
|
|
10
|
+
python scripts/bump_version.py 4.0.0 --check # verify only, no writes
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import re
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
REPO = Path(__file__).resolve().parents[1]
|
|
22
|
+
|
|
23
|
+
# (path, kind, pattern) — pattern groups: (prefix, version)
|
|
24
|
+
TARGETS = [
|
|
25
|
+
("latticeai/__init__.py", "regex", r'(__version__ = ")([^"]+)(")'),
|
|
26
|
+
("latticeai/core/workspace_os.py", "regex", r'(WORKSPACE_OS_VERSION = ")([^"]+)(")'),
|
|
27
|
+
("latticeai/core/marketplace.py", "regex", r'(MARKETPLACE_VERSION = ")([^"]+)(")'),
|
|
28
|
+
("latticeai/core/multi_agent.py", "regex", r'(MULTI_AGENT_VERSION = ")([^"]+)(")'),
|
|
29
|
+
("pyproject.toml", "regex", r'(^version = ")([^"]+)(")'),
|
|
30
|
+
("package.json", "json", "version"),
|
|
31
|
+
("package-lock.json", "package-lock", None),
|
|
32
|
+
("vscode-extension/package.json", "json", "version"),
|
|
33
|
+
("vscode-extension/package-lock.json", "package-lock", None),
|
|
34
|
+
("static/v3/asset-manifest.json", "json", "version"),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def bump(version: str, *, check: bool = False) -> int:
|
|
39
|
+
if not re.fullmatch(r"\d+\.\d+\.\d+([-.][0-9A-Za-z.]+)?", version):
|
|
40
|
+
print(f"error: '{version}' is not a sane semantic version", file=sys.stderr)
|
|
41
|
+
return 2
|
|
42
|
+
failures = 0
|
|
43
|
+
for rel, kind, spec in TARGETS:
|
|
44
|
+
path = REPO / rel
|
|
45
|
+
if not path.exists():
|
|
46
|
+
print(f" skip {rel} (missing)")
|
|
47
|
+
continue
|
|
48
|
+
text = path.read_text(encoding="utf-8")
|
|
49
|
+
if kind == "regex":
|
|
50
|
+
new, n = re.subn(spec, lambda m: m.group(1) + version + m.group(3), text, count=1, flags=re.MULTILINE)
|
|
51
|
+
changed = n == 1 and new != text
|
|
52
|
+
ok = n == 1
|
|
53
|
+
elif kind == "json":
|
|
54
|
+
data = json.loads(text)
|
|
55
|
+
ok = spec in data
|
|
56
|
+
changed = ok and data.get(spec) != version
|
|
57
|
+
if ok:
|
|
58
|
+
data[spec] = version
|
|
59
|
+
new = json.dumps(data, ensure_ascii=False, indent=2) + "\n"
|
|
60
|
+
elif kind == "package-lock":
|
|
61
|
+
data = json.loads(text)
|
|
62
|
+
ok = "version" in data
|
|
63
|
+
changed = ok and data.get("version") != version
|
|
64
|
+
if ok:
|
|
65
|
+
data["version"] = version
|
|
66
|
+
if "packages" in data and "" in data["packages"]:
|
|
67
|
+
data["packages"][""]["version"] = version
|
|
68
|
+
new = json.dumps(data, ensure_ascii=False, indent=2) + "\n"
|
|
69
|
+
else: # pragma: no cover
|
|
70
|
+
raise AssertionError(kind)
|
|
71
|
+
if not ok:
|
|
72
|
+
print(f" FAIL {rel}: version field not found")
|
|
73
|
+
failures += 1
|
|
74
|
+
continue
|
|
75
|
+
if check:
|
|
76
|
+
current = re.search(spec, text, flags=re.MULTILINE).group(2) if kind == "regex" else json.loads(text)["version"]
|
|
77
|
+
status = "ok" if current == version else f"MISMATCH ({current})"
|
|
78
|
+
if current != version:
|
|
79
|
+
failures += 1
|
|
80
|
+
print(f" {status:>9} {rel}")
|
|
81
|
+
continue
|
|
82
|
+
if changed:
|
|
83
|
+
path.write_text(new, encoding="utf-8")
|
|
84
|
+
print(f" bumped {rel}")
|
|
85
|
+
else:
|
|
86
|
+
print(f" ok {rel}")
|
|
87
|
+
return 1 if failures else 0
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def main() -> int:
|
|
91
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
92
|
+
parser.add_argument("version")
|
|
93
|
+
parser.add_argument("--check", action="store_true", help="verify only; write nothing")
|
|
94
|
+
args = parser.parse_args()
|
|
95
|
+
return bump(args.version, check=args.check)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
if __name__ == "__main__":
|
|
99
|
+
raise SystemExit(main())
|
package/scripts/lint_v3.mjs
CHANGED
|
@@ -1,33 +1,97 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/*
|
|
3
|
-
*
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
/* Lattice v3 frontend lint. Gates `npm run lint`:
|
|
3
|
+
* 1. Syntax-check every v3 ES module (node --check).
|
|
4
|
+
* 2. Design tokens: no raw hex/rgb colors in static/v3/css outside the two
|
|
5
|
+
* token files (lattice.tokens.css, tokens.css) — themed surfaces must use
|
|
6
|
+
* var(--…) tokens.
|
|
7
|
+
* 3. No inline style colors in view JS (style="…color: #…" or
|
|
8
|
+
* style.color = "#…" literals).
|
|
9
|
+
* 4. Privacy: zero CDN/external URLs in shipped static HTML/CSS/JS —
|
|
10
|
+
* fonts/icons/libs are vendored under static/vendor.
|
|
11
|
+
* Exits non-zero on any failure. */
|
|
12
|
+
import { readdirSync, statSync, readFileSync } from "node:fs";
|
|
13
|
+
import { join, dirname, relative } from "node:path";
|
|
6
14
|
import { fileURLToPath } from "node:url";
|
|
7
15
|
import { spawnSync } from "node:child_process";
|
|
8
16
|
|
|
9
|
-
const
|
|
17
|
+
const repo = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
18
|
+
const v3js = join(repo, "static", "v3", "js");
|
|
19
|
+
const v3css = join(repo, "static", "v3", "css");
|
|
20
|
+
const staticRoot = join(repo, "static");
|
|
10
21
|
|
|
11
|
-
function walk(dir) {
|
|
22
|
+
function walk(dir, ext) {
|
|
12
23
|
const out = [];
|
|
13
24
|
for (const name of readdirSync(dir)) {
|
|
14
25
|
const p = join(dir, name);
|
|
15
|
-
if (statSync(p).isDirectory()) out.push(...walk(p));
|
|
16
|
-
else if (name.endsWith(
|
|
26
|
+
if (statSync(p).isDirectory()) out.push(...walk(p, ext));
|
|
27
|
+
else if (ext.some((e) => name.endsWith(e))) out.push(p);
|
|
17
28
|
}
|
|
18
29
|
return out;
|
|
19
30
|
}
|
|
20
31
|
|
|
21
|
-
const files = walk(root).sort();
|
|
22
32
|
let failed = 0;
|
|
23
|
-
|
|
33
|
+
const fail = (msg) => { failed++; console.error(`FAIL ${msg}`); };
|
|
34
|
+
|
|
35
|
+
// ── 1. syntax ────────────────────────────────────────────────────────────
|
|
36
|
+
const modules = walk(v3js, [".js"]).sort();
|
|
37
|
+
let syntaxOk = 0;
|
|
38
|
+
for (const file of modules) {
|
|
24
39
|
const r = spawnSync(process.execPath, ["--check", file], { encoding: "utf8" });
|
|
25
|
-
if (r.status === 0)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
40
|
+
if (r.status === 0) syntaxOk++;
|
|
41
|
+
else fail(`${relative(repo, file)}\n${r.stderr || r.stdout}`);
|
|
42
|
+
}
|
|
43
|
+
console.log(`syntax: ${syntaxOk}/${modules.length} v3 modules pass`);
|
|
44
|
+
|
|
45
|
+
// ── 2. raw colors in v3 css (outside token files) ────────────────────────
|
|
46
|
+
const TOKEN_FILES = new Set(["lattice.tokens.css", "tokens.css"]);
|
|
47
|
+
const colorRe = /#[0-9a-fA-F]{3,8}\b|rgba?\(/;
|
|
48
|
+
let cssChecked = 0;
|
|
49
|
+
for (const file of walk(v3css, [".css"]).sort()) {
|
|
50
|
+
const base = file.split("/").pop();
|
|
51
|
+
if (TOKEN_FILES.has(base) || /\.[0-9a-f]{8}\.css$/.test(base)) continue; // tokens + hashed builds
|
|
52
|
+
cssChecked++;
|
|
53
|
+
const lines = readFileSync(file, "utf8").split("\n");
|
|
54
|
+
lines.forEach((line, i) => {
|
|
55
|
+
const code = line.split("/*")[0];
|
|
56
|
+
// mask-image gradients use #000/transparent as ALPHA values, not themed
|
|
57
|
+
// colors — they are theme-independent and exempt.
|
|
58
|
+
if (/mask-image|-webkit-mask/.test(code)) return;
|
|
59
|
+
if (colorRe.test(code)) fail(`${relative(repo, file)}:${i + 1} raw color (use a var(--…) token): ${line.trim().slice(0, 90)}`);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
console.log(`tokens: ${cssChecked} non-token v3 css files scanned for raw colors`);
|
|
63
|
+
|
|
64
|
+
// ── 3. inline style colors in view JS ────────────────────────────────────
|
|
65
|
+
const inlineColorRe = /style\s*=\s*["'`][^"'`]*(?:color|background)\s*:\s*(#|rgb)/i;
|
|
66
|
+
const styleAssignRe = /\.style\.(color|background(?:Color)?)\s*=\s*["'`](#|rgb)/i;
|
|
67
|
+
let jsChecked = 0;
|
|
68
|
+
for (const file of modules) {
|
|
69
|
+
if (/\.[0-9a-f]{8}\.js$/.test(file)) continue; // hashed builds mirror sources
|
|
70
|
+
jsChecked++;
|
|
71
|
+
const lines = readFileSync(file, "utf8").split("\n");
|
|
72
|
+
lines.forEach((line, i) => {
|
|
73
|
+
if (inlineColorRe.test(line) || styleAssignRe.test(line)) {
|
|
74
|
+
fail(`${relative(repo, file)}:${i + 1} inline style color (use a token/class): ${line.trim().slice(0, 90)}`);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
console.log(`inline-style: ${jsChecked} v3 source modules scanned`);
|
|
79
|
+
|
|
80
|
+
// ── 4. no CDN/external asset URLs in shipped static files ────────────────
|
|
81
|
+
const cdnRe = /https?:\/\/(fonts\.googleapis\.com|fonts\.gstatic\.com|cdn\.jsdelivr\.net|unpkg\.com|cdnjs\.cloudflare\.com)/;
|
|
82
|
+
let shippedChecked = 0;
|
|
83
|
+
for (const file of walk(staticRoot, [".html", ".css", ".js"]).sort()) {
|
|
84
|
+
if (file.includes(`${join("static", "vendor")}`)) continue; // vendored copies may cite origins in comments
|
|
85
|
+
shippedChecked++;
|
|
86
|
+
const lines = readFileSync(file, "utf8").split("\n");
|
|
87
|
+
lines.forEach((line, i) => {
|
|
88
|
+
if (cdnRe.test(line)) fail(`${relative(repo, file)}:${i + 1} CDN reference (vendor it under static/vendor): ${line.trim().slice(0, 90)}`);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
console.log(`privacy: ${shippedChecked} shipped static files scanned for CDN URLs`);
|
|
92
|
+
|
|
93
|
+
if (failed) {
|
|
94
|
+
console.error(`\nv3 frontend lint: ${failed} failure(s)`);
|
|
95
|
+
process.exit(1);
|
|
31
96
|
}
|
|
32
|
-
console.log(`\nv3 frontend:
|
|
33
|
-
process.exit(failed ? 1 : 0);
|
|
97
|
+
console.log(`\nv3 frontend lint: all checks pass`);
|