ltcai 3.4.1 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +206 -247
  2. package/docs/CARRYOVER_AUDIT_v3.6.0.md +61 -0
  3. package/docs/CHANGELOG.md +32 -0
  4. package/docs/HANDOVER_v3.6.0.md +46 -0
  5. package/docs/RUNTIME_HOOK_COVERAGE_v3.5.0.md +56 -0
  6. package/docs/RUNTIME_HOOK_COVERAGE_v3.6.0.md +49 -0
  7. package/docs/architecture.md +13 -12
  8. package/docs/kg-schema.md +55 -0
  9. package/docs/privacy.md +18 -2
  10. package/docs/security-model.md +17 -0
  11. package/kg_schema.py +46 -0
  12. package/knowledge_graph.py +520 -1
  13. package/latticeai/__init__.py +1 -1
  14. package/latticeai/api/auth.py +37 -9
  15. package/latticeai/api/browser.py +217 -0
  16. package/latticeai/api/chat.py +4 -1
  17. package/latticeai/api/computer_use.py +21 -8
  18. package/latticeai/api/portability.py +93 -0
  19. package/latticeai/api/tools.py +29 -26
  20. package/latticeai/core/config.py +3 -0
  21. package/latticeai/core/marketplace.py +1 -1
  22. package/latticeai/core/multi_agent.py +1 -1
  23. package/latticeai/core/oidc.py +205 -0
  24. package/latticeai/core/security.py +59 -5
  25. package/latticeai/core/workspace_os.py +1 -1
  26. package/latticeai/server_app.py +39 -0
  27. package/latticeai/services/ingestion.py +271 -0
  28. package/latticeai/services/kg_portability.py +177 -0
  29. package/package.json +5 -4
  30. package/requirements.txt +1 -0
  31. package/scripts/build_vsix.mjs +72 -0
  32. package/scripts/check_python.py +87 -0
  33. package/static/css/reference/account.css +1 -1
  34. package/static/css/reference/admin.css +1 -1
  35. package/static/css/reference/base.css +8 -5
  36. package/static/css/reference/chat.css +8 -8
  37. package/static/css/reference/graph.css +2 -2
  38. package/static/css/responsive.css +2 -2
  39. package/static/v3/asset-manifest.json +9 -9
  40. package/static/v3/css/{lattice.shell.6ceea7c8.css → lattice.shell.8fcc9d33.css} +2 -1
  41. package/static/v3/css/lattice.shell.css +2 -1
  42. package/static/v3/js/{app.d086489d.js → app.c541f955.js} +1 -1
  43. package/static/v3/js/core/{api.12b568ad.js → api.33d6320e.js} +38 -0
  44. package/static/v3/js/core/api.js +38 -0
  45. package/static/v3/js/core/{routes.d214b399.js → routes.2ce3815a.js} +1 -1
  46. package/static/v3/js/core/routes.js +1 -1
  47. package/static/v3/js/core/{shell.d05266f5.js → shell.8c163e0e.js} +2 -2
  48. package/static/v3/js/views/knowledge-graph.a96040a5.js +513 -0
  49. package/static/v3/js/views/knowledge-graph.js +293 -17
  50. package/static/workspace.css +1 -1
  51. package/tools/__init__.py +276 -0
  52. package/tools/commands.py +188 -0
  53. package/tools/computer.py +185 -0
  54. package/tools/documents.py +243 -0
  55. package/tools/filesystem.py +560 -0
  56. package/tools/knowledge.py +97 -0
  57. package/tools/local_files.py +69 -0
  58. package/tools/network.py +66 -0
  59. package/static/v3/js/views/knowledge-graph.a14ea7e7.js +0 -237
  60. package/tools.py +0 -1525
@@ -0,0 +1,177 @@
1
+ """Knowledge Graph portability — local export / import / backup / restore.
2
+
3
+ The Knowledge Graph is the user's durable asset, so it must be portable without
4
+ any cloud service. Two complementary mechanisms, both fully local:
5
+
6
+ * **Logical export/import** (JSON): nodes/edges/chunks/sources/provenance with a
7
+ versioned header (schema + projection + embed-dim). Re-embeds on import, so it
8
+ is portable across machines.
9
+ * **Binary backup/restore** (ZIP): a faithful snapshot of the SQLite DB (incl.
10
+ vector embeddings) plus the blob directory, integrity-checked, for
11
+ same-machine recovery.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import hashlib
17
+ import json
18
+ import shutil
19
+ import tempfile
20
+ import zipfile
21
+ from datetime import datetime, timezone
22
+ from pathlib import Path
23
+ from typing import Any, Dict, Optional
24
+
25
+ FORMAT = "latticeai.kg.export"
26
+ FORMAT_VERSION = 1
27
+ BACKUP_FORMAT = "latticeai.kg.backup"
28
+
29
+
30
+ def _now_iso() -> str:
31
+ return datetime.now(timezone.utc).isoformat()
32
+
33
+
34
+ def _stamp() -> str:
35
+ return _now_iso().replace(":", "").replace("-", "").replace(".", "")[:15]
36
+
37
+
38
+ def _sha256_file(path: Path) -> str:
39
+ h = hashlib.sha256()
40
+ with open(path, "rb") as fh:
41
+ for block in iter(lambda: fh.read(65536), b""):
42
+ h.update(block)
43
+ return h.hexdigest()
44
+
45
+
46
+ class KGPortabilityService:
47
+ def __init__(self, *, knowledge_graph: Any, data_dir, enable_graph: bool = True) -> None:
48
+ self._kg = knowledge_graph
49
+ self._data_dir = Path(data_dir)
50
+ self._enable = bool(enable_graph)
51
+ self._exports_dir = self._data_dir / "workspace_exports"
52
+
53
+ def available(self) -> bool:
54
+ return self._enable and self._kg is not None
55
+
56
+ def _require(self) -> None:
57
+ if not self.available():
58
+ raise RuntimeError("Knowledge Graph is disabled (LATTICEAI_ENABLE_GRAPH).")
59
+
60
+ # ── logical export / import ──────────────────────────────────────────────
61
+ def export(self, *, workspace_id: Optional[str] = None) -> Dict[str, Any]:
62
+ self._require()
63
+ data = self._kg.export_graph_data()
64
+ header = {
65
+ "format": FORMAT,
66
+ "format_version": FORMAT_VERSION,
67
+ **self._kg.schema_versions(),
68
+ "exported_at": _now_iso(),
69
+ "workspace_id": workspace_id,
70
+ "counts": data.get("counts"),
71
+ }
72
+ return {"header": header, **data}
73
+
74
+ def export_to_file(self, path=None, *, workspace_id: Optional[str] = None) -> Dict[str, Any]:
75
+ artifact = self.export(workspace_id=workspace_id)
76
+ self._exports_dir.mkdir(parents=True, exist_ok=True)
77
+ path = Path(path) if path else self._exports_dir / f"kg-export-{_stamp()}.json"
78
+ path.write_text(json.dumps(artifact, ensure_ascii=False, indent=2), encoding="utf-8")
79
+ return {"path": str(path), "header": artifact["header"], "bytes": path.stat().st_size}
80
+
81
+ def import_data(self, artifact: Dict[str, Any], *, mode: str = "merge", dry_run: bool = False) -> Dict[str, Any]:
82
+ self._require()
83
+ if not isinstance(artifact, dict) or "nodes" not in artifact:
84
+ raise ValueError("Invalid Knowledge Graph export artifact.")
85
+ if mode not in ("merge", "replace"):
86
+ raise ValueError("mode must be 'merge' or 'replace'.")
87
+ result = self._kg.import_graph_data(artifact, mode=mode, dry_run=dry_run)
88
+ result["header"] = artifact.get("header")
89
+ return result
90
+
91
+ def import_from_file(self, path, *, mode: str = "merge", dry_run: bool = False) -> Dict[str, Any]:
92
+ artifact = json.loads(Path(path).read_text(encoding="utf-8"))
93
+ return self.import_data(artifact, mode=mode, dry_run=dry_run)
94
+
95
+ # ── binary backup / restore ──────────────────────────────────────────────
96
+ def backup(self, dest_path=None) -> Dict[str, Any]:
97
+ self._require()
98
+ self._exports_dir.mkdir(parents=True, exist_ok=True)
99
+ dest = Path(dest_path) if dest_path else self._exports_dir / f"kg-backup-{_stamp()}.zip"
100
+ with tempfile.TemporaryDirectory() as tmp_s:
101
+ tmp = Path(tmp_s)
102
+ db_copy = tmp / "knowledge_graph.sqlite"
103
+ self._kg.backup_database(db_copy)
104
+ manifest = {
105
+ "format": BACKUP_FORMAT,
106
+ "format_version": FORMAT_VERSION,
107
+ **self._kg.schema_versions(),
108
+ "created_at": _now_iso(),
109
+ "db_sha256": _sha256_file(db_copy),
110
+ "has_blobs": Path(self._kg.blob_dir).exists(),
111
+ }
112
+ with zipfile.ZipFile(dest, "w", zipfile.ZIP_DEFLATED) as zf:
113
+ zf.write(db_copy, "knowledge_graph.sqlite")
114
+ zf.writestr("manifest.json", json.dumps(manifest, ensure_ascii=False, indent=2))
115
+ blob_dir = Path(self._kg.blob_dir)
116
+ if blob_dir.exists():
117
+ for f in blob_dir.rglob("*"):
118
+ if f.is_file():
119
+ zf.write(f, f"blobs/{f.relative_to(blob_dir)}")
120
+ return {"path": str(dest), "bytes": dest.stat().st_size, "manifest": manifest}
121
+
122
+ def restore(self, archive_path, *, verify: bool = True) -> Dict[str, Any]:
123
+ self._require()
124
+ archive = Path(archive_path)
125
+ if not archive.exists():
126
+ raise FileNotFoundError(f"Backup archive not found: {archive}")
127
+ with zipfile.ZipFile(archive) as zf:
128
+ names = zf.namelist()
129
+ if "knowledge_graph.sqlite" not in names:
130
+ raise ValueError("Archive is missing knowledge_graph.sqlite.")
131
+ manifest = json.loads(zf.read("manifest.json")) if "manifest.json" in names else {}
132
+ with tempfile.TemporaryDirectory() as tmp_s:
133
+ tmp = Path(tmp_s)
134
+ zf.extractall(tmp)
135
+ db_src = tmp / "knowledge_graph.sqlite"
136
+ if verify and manifest.get("db_sha256"):
137
+ if _sha256_file(db_src) != manifest["db_sha256"]:
138
+ raise ValueError("Backup integrity check failed (db sha256 mismatch).")
139
+ db_dest = Path(self._kg.db_path)
140
+ blob_dest = Path(self._kg.blob_dir)
141
+ db_dest.parent.mkdir(parents=True, exist_ok=True)
142
+ # Drop the live DB + stale WAL/SHM siblings so the restored copy
143
+ # is authoritative (no stale journal overlaying old pages).
144
+ for sib in (db_dest, Path(str(db_dest) + "-wal"), Path(str(db_dest) + "-shm")):
145
+ if sib.exists():
146
+ sib.unlink()
147
+ shutil.copyfile(db_src, db_dest)
148
+ blob_src = tmp / "blobs"
149
+ if blob_src.exists():
150
+ if blob_dest.exists():
151
+ shutil.rmtree(blob_dest)
152
+ shutil.copytree(blob_src, blob_dest)
153
+ else:
154
+ blob_dest.mkdir(parents=True, exist_ok=True)
155
+ stats = self._kg.stats()
156
+ return {
157
+ "restored": True,
158
+ "manifest": manifest,
159
+ "nodes": sum(stats.get("nodes", {}).values()),
160
+ }
161
+
162
+ # ── status surface ───────────────────────────────────────────────────────
163
+ def snapshot_metadata(self) -> Dict[str, Any]:
164
+ if not self.available():
165
+ return {"available": False}
166
+ return {
167
+ "available": True,
168
+ **self._kg.schema_versions(),
169
+ "stats": self._kg.stats(),
170
+ "provenance": self._kg.provenance_stats(),
171
+ }
172
+
173
+ def recent_ingestions(self, *, limit: int = 50, source_type: Optional[str] = None) -> Dict[str, Any]:
174
+ """Recent provenance records (newest first) for the ingestion-sources UI."""
175
+ if not self.available():
176
+ return {"items": [], "count": 0}
177
+ return self._kg.list_provenance(limit=limit, source_type=source_type)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "3.4.1",
3
+ "version": "3.6.0",
4
4
  "description": "Lattice AI v3 local-first AI workspace platform with knowledge graph, vector index, hybrid search, agents, and workspace modes.",
5
5
  "homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
6
6
  "repository": {
@@ -20,7 +20,7 @@
20
20
  "build": "npm run build:assets && npm run build:python",
21
21
  "build:assets": "node scripts/build_v3_assets.mjs",
22
22
  "build:python": "python3 -m build",
23
- "check:python": "python3 -m py_compile ltcai_cli.py server.py latticeai/server_app.py latticeai/api/auth.py latticeai/api/chat.py latticeai/api/computer_use.py latticeai/api/deps.py latticeai/api/garden.py latticeai/api/local_files.py latticeai/api/permissions.py latticeai/api/setup.py latticeai/api/static_routes.py latticeai/api/tools.py latticeai/api/plugins.py latticeai/api/workflow_designer.py latticeai/api/agents.py latticeai/api/realtime.py latticeai/api/marketplace.py latticeai/api/search.py latticeai/services/search_service.py latticeai/core/local_embeddings.py latticeai/core/embedding_providers.py latticeai/services/agent_runtime.py latticeai/core/config.py latticeai/api/admin.py latticeai/services/app_context.py latticeai/services/model_runtime.py latticeai/services/model_catalog.py latticeai/services/model_recommendation.py latticeai/services/tool_dispatch.py latticeai/services/upload_service.py latticeai/core/tool_registry.py latticeai/core/enterprise.py latticeai/core/enterprise_admin.py latticeai/core/agent_prompts.py latticeai/core/workspace_os.py latticeai/core/plugins.py latticeai/core/marketplace.py latticeai/core/workflow_engine.py latticeai/core/multi_agent.py latticeai/core/realtime.py latticeai/api/mcp.py latticeai/core/hooks.py latticeai/core/builtin_hooks.py latticeai/api/hooks.py latticeai/core/agent_registry.py latticeai/api/agent_registry.py latticeai/services/memory_service.py latticeai/api/memory.py knowledge_graph.py knowledge_graph_api.py local_knowledge_api.py llm_router.py p_reinforce.py telegram_bot.py tools.py codex_telegram_bot.py",
23
+ "check:python": "python3 scripts/check_python.py",
24
24
  "lint": "node --check static/scripts/account.js && node --check static/scripts/admin.js && node --check static/scripts/chat.js && node --check static/scripts/graph.js && node --check static/scripts/platform.js && node --check static/scripts/ux.js && node --check static/scripts/workspace.js && node --check tests/visual/mock_server.cjs && node --check tests/visual/v3.spec.js && npm run lint:v3",
25
25
  "lint:v3": "node scripts/lint_v3.mjs",
26
26
  "typecheck": "cd vscode-extension && npm run build",
@@ -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
- "release:artifacts": "npm run build:assets && npm run build:python && npm pack && cd vscode-extension && npm run package:vsix",
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",
@@ -70,7 +71,7 @@
70
71
  "llm_router.py",
71
72
  "p_reinforce.py",
72
73
  "telegram_bot.py",
73
- "tools.py",
74
+ "tools/",
74
75
  "codex_telegram_bot.py",
75
76
  "mcp_registry.py",
76
77
  "latticeai/**/*.py",
package/requirements.txt CHANGED
@@ -10,6 +10,7 @@ python-pptx
10
10
  python-multipart
11
11
  keyring
12
12
  authlib
13
+ cryptography
13
14
  pdfplumber
14
15
  pypdfium2
15
16
  watchdog
@@ -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,87 @@
1
+ #!/usr/bin/env python3
2
+ """Discover-and-compile every first-party Python module.
3
+
4
+ Replaces the hand-maintained ``py_compile`` enumeration in CI and
5
+ ``package.json``: walks the repository, skips vendored / virtualenv / build /
6
+ cache / generated directories, and byte-compiles everything that remains. New
7
+ modules are picked up automatically — there is nothing to update when a file is
8
+ added, so the syntax gate can never silently fall behind the codebase.
9
+
10
+ Usage::
11
+
12
+ python scripts/check_python.py # compile all discovered modules
13
+ python scripts/check_python.py --list # just print what would be compiled
14
+
15
+ Exit code is non-zero if any module fails to compile.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import py_compile
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ ROOT = Path(__file__).resolve().parent.parent
25
+
26
+ # Directory names excluded anywhere in the tree: virtualenvs, build/cache
27
+ # artifacts, generated agent output, and vendored snapshots of older releases.
28
+ EXCLUDE_DIRS = {
29
+ ".git",
30
+ ".venv",
31
+ "venv",
32
+ "env",
33
+ ".build-venv",
34
+ ".npm-cache",
35
+ "build",
36
+ "dist",
37
+ "node_modules",
38
+ "__pycache__",
39
+ ".pytest_cache",
40
+ ".mypy_cache",
41
+ ".ruff_cache",
42
+ "agent_workspace",
43
+ "outputs",
44
+ "playwright-report",
45
+ "test-results",
46
+ "ltcai.egg-info",
47
+ ".ltcai",
48
+ ".ltcai-brain",
49
+ ".ltcai-test",
50
+ # Vendored snapshot of an older packaged release — not part of the build.
51
+ "ltcai-0.3.1",
52
+ }
53
+
54
+
55
+ def iter_modules():
56
+ for path in ROOT.rglob("*.py"):
57
+ parts = path.relative_to(ROOT).parts
58
+ if any(part in EXCLUDE_DIRS for part in parts):
59
+ continue
60
+ yield path
61
+
62
+
63
+ def main(argv: list[str]) -> int:
64
+ modules = sorted(iter_modules())
65
+ if "--list" in argv:
66
+ for path in modules:
67
+ print(path.relative_to(ROOT))
68
+ return 0
69
+
70
+ failures: list[str] = []
71
+ for path in modules:
72
+ try:
73
+ py_compile.compile(str(path), doraise=True)
74
+ except py_compile.PyCompileError as exc:
75
+ failures.append(str(exc))
76
+
77
+ if failures:
78
+ print("\n".join(failures))
79
+ print(f"check:python FAILED — {len(failures)} of {len(modules)} module(s) did not compile")
80
+ return 1
81
+
82
+ print(f"check:python OK — compiled {len(modules)} modules")
83
+ return 0
84
+
85
+
86
+ if __name__ == "__main__":
87
+ raise SystemExit(main(sys.argv[1:]))
@@ -117,7 +117,7 @@
117
117
  box-shadow: var(--shadow), inset 0 1px 0 rgba(255,255,255,0.9);
118
118
  position: relative;
119
119
  z-index: 1;
120
- backdrop-filter: blur(28px);
120
+ backdrop-filter: none; /* glass removed v3.5.0 */
121
121
  }
122
122
 
123
123
  .card::before {
@@ -45,7 +45,7 @@
45
45
  padding: 22px 28px;
46
46
  border-bottom: 1px solid rgba(111,66,232,0.10);
47
47
  background: var(--sidebar);
48
- backdrop-filter: blur(20px);
48
+ backdrop-filter: none; /* glass removed v3.5.0 */
49
49
  position: sticky;
50
50
  top: 0;
51
51
  z-index: 2;
@@ -46,7 +46,7 @@
46
46
  background: var(--card);
47
47
  border-color: var(--accent-soft);
48
48
  box-shadow: 0 20px 64px rgba(102, 82, 168, 0.20), inset 0 1px 0 rgba(255,255,255,0.86);
49
- backdrop-filter: blur(26px);
49
+ backdrop-filter: none; /* glass removed v3.5.0 */
50
50
  }
51
51
 
52
52
  .lattice-ref-auth .card::before {
@@ -65,7 +65,7 @@
65
65
  color: var(--ref-ink);
66
66
  background: var(--surface);
67
67
  border-bottom: 1px solid rgba(111,66,232,0.08);
68
- backdrop-filter: blur(12px);
68
+ backdrop-filter: none; /* glass removed v3.5.0 */
69
69
  }
70
70
 
71
71
  .auth-window-brand {
@@ -712,7 +712,10 @@
712
712
  font-weight: 700;
713
713
  background: transparent;
714
714
  cursor: pointer;
715
- transition: 160ms ease;
715
+ /* Animate surface affordances, but never `color`: a color transition on
716
+ theme switch would briefly render the rail link dark-on-dark (the v226
717
+ dark-mode contrast gate caught this on slow CI). Color flips instantly. */
718
+ transition: background-color 160ms ease, border-color 160ms ease, transform 160ms ease;
716
719
  }
717
720
 
718
721
  .reference-rail a.active,
@@ -842,7 +845,7 @@
842
845
  background: var(--sidebar);
843
846
  border-bottom: 1px solid rgba(111,66,232,0.11);
844
847
  box-shadow: 0 2px 12px rgba(88,72,150,0.06);
845
- backdrop-filter: blur(14px);
848
+ backdrop-filter: none; /* glass removed v3.5.0 */
846
849
  }
847
850
 
848
851
  .lattice-ref-chat .messages-viewport {
@@ -1295,7 +1298,7 @@
1295
1298
  height: 96px;
1296
1299
  background: var(--sidebar);
1297
1300
  border-bottom: 1px solid rgba(111,66,232,0.10);
1298
- backdrop-filter: blur(14px);
1301
+ backdrop-filter: none; /* glass removed v3.5.0 */
1299
1302
  padding: 22px 34px 8px;
1300
1303
  position: sticky;
1301
1304
  }
@@ -81,8 +81,8 @@
81
81
  display: flex;
82
82
  flex-direction: column;
83
83
  min-width: 240px;
84
- backdrop-filter: blur(24px);
85
- -webkit-backdrop-filter: blur(24px);
84
+ backdrop-filter: none; /* glass removed v3.5.0 */
85
+ -webkit-backdrop-filter: none; /* glass removed v3.5.0 */
86
86
  }
87
87
 
88
88
  .sidebar-header {
@@ -342,8 +342,8 @@
342
342
  padding: 10px 22px;
343
343
  border-bottom: 1px solid rgba(111,66,232,0.10);
344
344
  background: var(--sidebar);
345
- backdrop-filter: blur(20px);
346
- -webkit-backdrop-filter: blur(20px);
345
+ backdrop-filter: none; /* glass removed v3.5.0 */
346
+ -webkit-backdrop-filter: none; /* glass removed v3.5.0 */
347
347
  position: relative;
348
348
  z-index: 50;
349
349
  }
@@ -636,7 +636,7 @@
636
636
  justify-content: space-between;
637
637
  gap: 12px;
638
638
  box-shadow: var(--shadow-sm), inset 0 1px 0 rgba(255,255,255,0.80);
639
- backdrop-filter: blur(10px);
639
+ backdrop-filter: none; /* glass removed v3.5.0 */
640
640
  transition: all .2s;
641
641
  }
642
642
 
@@ -1323,7 +1323,7 @@
1323
1323
  box-shadow: var(--shadow), 0 0 0 1px rgba(111,66,232,0.05);
1324
1324
  position: relative;
1325
1325
  z-index: 1;
1326
- backdrop-filter: blur(24px);
1326
+ backdrop-filter: none; /* glass removed v3.5.0 */
1327
1327
  }
1328
1328
 
1329
1329
  .auth-card::before {
@@ -2754,7 +2754,7 @@
2754
2754
  .admin-panel {
2755
2755
  background: var(--card);
2756
2756
  border-left: 1px solid var(--border);
2757
- backdrop-filter: blur(24px);
2757
+ backdrop-filter: none; /* glass removed v3.5.0 */
2758
2758
  color: var(--text);
2759
2759
  }
2760
2760
  .admin-header {
@@ -4254,7 +4254,7 @@
4254
4254
  background: var(--sidebar);
4255
4255
  border-bottom: 1px solid var(--line);
4256
4256
  box-shadow: none;
4257
- backdrop-filter: blur(14px);
4257
+ backdrop-filter: none; /* glass removed v3.5.0 */
4258
4258
  }
4259
4259
 
4260
4260
  /* Messages viewport */
@@ -120,7 +120,7 @@
120
120
  z-index: 20;
121
121
  border: 1px solid var(--line);
122
122
  background: var(--panel);
123
- backdrop-filter: blur(18px);
123
+ backdrop-filter: none; /* glass removed v3.5.0 */
124
124
  box-shadow: var(--shadow);
125
125
  }
126
126
 
@@ -904,7 +904,7 @@
904
904
  border-radius: 10px;
905
905
  background: var(--surface-2);
906
906
  box-shadow: var(--shadow);
907
- backdrop-filter: blur(18px);
907
+ backdrop-filter: none; /* glass removed v3.5.0 */
908
908
  }
909
909
 
910
910
  .focus-chip span {
@@ -319,8 +319,8 @@ select {
319
319
  z-index: 99;
320
320
  display: none;
321
321
  background: rgba(15, 12, 30, 0.42);
322
- -webkit-backdrop-filter: blur(2px);
323
- backdrop-filter: blur(2px);
322
+ -webkit-backdrop-filter: none; /* glass removed v3.5.0 */
323
+ backdrop-filter: none; /* glass removed v3.5.0 */
324
324
  }
325
325
  body.sidebar-open .sidebar-overlay { display: block; }
326
326
 
@@ -1,14 +1,14 @@
1
1
  {
2
- "version": "3.4.1",
2
+ "version": "3.6.0",
3
3
  "generated_at": "deterministic",
4
4
  "entrypoints": {
5
- "app": "/static/v3/js/app.d086489d.js",
5
+ "app": "/static/v3/js/app.c541f955.js",
6
6
  "styles": [
7
7
  "/static/css/tokens.3ba22e37.css",
8
8
  "/static/v3/css/lattice.tokens.e7018963.css",
9
9
  "/static/v3/css/lattice.base.e4cdd05d.css",
10
10
  "/static/v3/css/lattice.components.9b49d614.css",
11
- "/static/v3/css/lattice.shell.6ceea7c8.css",
11
+ "/static/v3/css/lattice.shell.8fcc9d33.css",
12
12
  "/static/v3/css/lattice.views.22f69117.css"
13
13
  ]
14
14
  },
@@ -17,15 +17,15 @@
17
17
  "static/v3/css/lattice.tokens.css": "/static/v3/css/lattice.tokens.e7018963.css",
18
18
  "static/v3/css/lattice.base.css": "/static/v3/css/lattice.base.e4cdd05d.css",
19
19
  "static/v3/css/lattice.components.css": "/static/v3/css/lattice.components.9b49d614.css",
20
- "static/v3/css/lattice.shell.css": "/static/v3/css/lattice.shell.6ceea7c8.css",
20
+ "static/v3/css/lattice.shell.css": "/static/v3/css/lattice.shell.8fcc9d33.css",
21
21
  "static/v3/css/lattice.views.css": "/static/v3/css/lattice.views.22f69117.css",
22
- "static/v3/js/app.js": "/static/v3/js/app.d086489d.js",
23
- "static/v3/js/core/api.js": "/static/v3/js/core/api.12b568ad.js",
22
+ "static/v3/js/app.js": "/static/v3/js/app.c541f955.js",
23
+ "static/v3/js/core/api.js": "/static/v3/js/core/api.33d6320e.js",
24
24
  "static/v3/js/core/components.js": "/static/v3/js/core/components.f25b3b93.js",
25
25
  "static/v3/js/core/dom.js": "/static/v3/js/core/dom.a2773eb0.js",
26
26
  "static/v3/js/core/router.js": "/static/v3/js/core/router.584570f2.js",
27
- "static/v3/js/core/routes.js": "/static/v3/js/core/routes.d214b399.js",
28
- "static/v3/js/core/shell.js": "/static/v3/js/core/shell.d05266f5.js",
27
+ "static/v3/js/core/routes.js": "/static/v3/js/core/routes.2ce3815a.js",
28
+ "static/v3/js/core/shell.js": "/static/v3/js/core/shell.8c163e0e.js",
29
29
  "static/v3/js/core/store.js": "/static/v3/js/core/store.34ebd5e6.js",
30
30
  "static/v3/js/views/admin-audit.js": "/static/v3/js/views/admin-audit.660a1fb1.js",
31
31
  "static/v3/js/views/admin-permissions.js": "/static/v3/js/views/admin-permissions.a7ae5f09.js",
@@ -39,7 +39,7 @@
39
39
  "static/v3/js/views/home.js": "/static/v3/js/views/home.24f8b8ae.js",
40
40
  "static/v3/js/views/hooks.js": "/static/v3/js/views/hooks.37895880.js",
41
41
  "static/v3/js/views/hybrid-search.js": "/static/v3/js/views/hybrid-search.b22b97e0.js",
42
- "static/v3/js/views/knowledge-graph.js": "/static/v3/js/views/knowledge-graph.a14ea7e7.js",
42
+ "static/v3/js/views/knowledge-graph.js": "/static/v3/js/views/knowledge-graph.a96040a5.js",
43
43
  "static/v3/js/views/marketplace.js": "/static/v3/js/views/marketplace.ab0583d4.js",
44
44
  "static/v3/js/views/mcp.js": "/static/v3/js/views/mcp.99b5c6a7.js",
45
45
  "static/v3/js/views/memory.js": "/static/v3/js/views/memory.4ebdf474.js",
@@ -349,8 +349,9 @@
349
349
  .lt3-scrim {
350
350
  position: fixed; inset: 0;
351
351
  z-index: var(--lt3-z-scrim);
352
+ /* Solid dim scrim — no backdrop blur (glassmorphism removed in v3.5.0 for a
353
+ crisp, stable surface). */
352
354
  background: var(--overlay);
353
- backdrop-filter: blur(2px);
354
355
  opacity: 0;
355
356
  animation: lt3-fade var(--lt3-dur-2) var(--lt3-ease) forwards;
356
357
  }
@@ -349,8 +349,9 @@
349
349
  .lt3-scrim {
350
350
  position: fixed; inset: 0;
351
351
  z-index: var(--lt3-z-scrim);
352
+ /* Solid dim scrim — no backdrop blur (glassmorphism removed in v3.5.0 for a
353
+ crisp, stable surface). */
352
354
  background: var(--overlay);
353
- backdrop-filter: blur(2px);
354
355
  opacity: 0;
355
356
  animation: lt3-fade var(--lt3-dur-2) var(--lt3-ease) forwards;
356
357
  }
@@ -3,7 +3,7 @@
3
3
  * Boots the shell. Views are lazy-loaded by the router (see core/routes.js).
4
4
  * ========================================================================== */
5
5
 
6
- import { boot } from "./core/shell.d05266f5.js";
6
+ import { boot } from "./core/shell.8c163e0e.js";
7
7
 
8
8
  const root = document.getElementById("app");
9
9
  if (root) boot(root);