ltcai 9.5.0 → 9.7.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 (71) hide show
  1. package/README.md +67 -43
  2. package/auto_setup.py +11 -1
  3. package/docs/CHANGELOG.md +86 -0
  4. package/docs/COMMUNITY_AND_PLUGINS.md +2 -2
  5. package/docs/DEVELOPMENT.md +8 -8
  6. package/docs/LEGACY_COMPATIBILITY.md +1 -1
  7. package/docs/ONBOARDING.md +2 -2
  8. package/docs/OPERATIONS.md +1 -1
  9. package/docs/PERFORMANCE.md +106 -0
  10. package/docs/TRUST_MODEL.md +1 -1
  11. package/docs/WHY_LATTICE.md +1 -1
  12. package/docs/kg-schema.md +1 -1
  13. package/docs/mcp-tools.md +1 -1
  14. package/kg_schema.py +11 -1
  15. package/knowledge_graph.py +12 -2
  16. package/knowledge_graph_api.py +11 -1
  17. package/lattice_brain/__init__.py +1 -1
  18. package/lattice_brain/graph/proactive.py +583 -0
  19. package/lattice_brain/graph/retrieval.py +211 -7
  20. package/lattice_brain/graph/retrieval_vector.py +102 -0
  21. package/lattice_brain/ingestion.py +360 -4
  22. package/lattice_brain/quality.py +44 -9
  23. package/lattice_brain/runtime/multi_agent.py +38 -2
  24. package/latticeai/__init__.py +1 -1
  25. package/latticeai/api/brain_intelligence.py +27 -2
  26. package/latticeai/api/change_proposals.py +89 -0
  27. package/latticeai/api/chat_agent_http.py +2 -0
  28. package/latticeai/api/review_queue.py +66 -2
  29. package/latticeai/app_factory.py +23 -0
  30. package/latticeai/cli/entrypoint.py +3 -1
  31. package/latticeai/core/agent.py +273 -101
  32. package/latticeai/core/agent_eval.py +411 -0
  33. package/latticeai/core/agent_trace.py +104 -0
  34. package/latticeai/core/legacy_compatibility.py +15 -1
  35. package/latticeai/core/marketplace.py +1 -1
  36. package/latticeai/core/tool_governor.py +101 -0
  37. package/latticeai/core/workspace_os.py +1 -1
  38. package/latticeai/runtime/router_registration.py +2 -0
  39. package/latticeai/services/architecture_readiness.py +1 -1
  40. package/latticeai/services/brain_intelligence.py +97 -1
  41. package/latticeai/services/change_proposals.py +322 -0
  42. package/latticeai/services/product_readiness.py +1 -1
  43. package/latticeai/services/review_queue.py +47 -3
  44. package/latticeai/tools/__init__.py +6 -1
  45. package/llm_router.py +10 -1
  46. package/local_knowledge_api.py +10 -1
  47. package/ltcai_cli.py +11 -1
  48. package/mcp_registry.py +10 -1
  49. package/p_reinforce.py +10 -1
  50. package/package.json +1 -1
  51. package/scripts/agent_eval.py +46 -0
  52. package/scripts/brain_quality_eval.py +1 -1
  53. package/scripts/check_current_release_docs.mjs +2 -1
  54. package/scripts/profile_kg.py +360 -0
  55. package/setup_wizard.py +10 -1
  56. package/src-tauri/Cargo.lock +1 -1
  57. package/src-tauri/Cargo.toml +1 -1
  58. package/src-tauri/tauri.conf.json +1 -1
  59. package/static/app/asset-manifest.json +11 -11
  60. package/static/app/assets/{Act-Cdfqx4wN.js → Act-B6c39ays.js} +2 -1
  61. package/static/app/assets/{Brain-w_tAuyg6.js → Brain-D7Qg4k6M.js} +1 -1
  62. package/static/app/assets/{Capture-iJwi9LmS.js → Capture-VF_di68r.js} +1 -1
  63. package/static/app/assets/{Library-Do-jjhzi.js → Library-D_Gis2PA.js} +1 -1
  64. package/static/app/assets/{System-DFg_q3E6.js → System-C5s5H2ov.js} +1 -1
  65. package/static/app/assets/{index-BN6HIWVC.css → index-85wQvEie.css} +1 -1
  66. package/static/app/assets/index-DJC_2oub.js +18 -0
  67. package/static/app/assets/{primitives-ZTUlU7pR.js → primitives-DL4Nip8C.js} +1 -1
  68. package/static/app/assets/{textarea-CMfhqPhE.js → textarea-woZfCXHy.js} +1 -1
  69. package/static/app/index.html +2 -2
  70. package/static/sw.js +1 -1
  71. package/static/app/assets/index-aJuRi-Xo.js +0 -17
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "9.5.0",
3
+ "version": "9.7.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": {
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env python3
2
+ """Agent-loop evaluation gate (v9.6.0).
3
+
4
+ Runs the deterministic scenario suite in ``latticeai.core.agent_eval``
5
+ against the real SingleAgentRuntime state machine (scripted model, fake
6
+ ports) and fails the release when any scenario regresses.
7
+
8
+ Usage: .venv/bin/python scripts/agent_eval.py [--verbose]
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
18
+
19
+ from latticeai.core.agent_eval import run_agent_eval # noqa: E402
20
+
21
+
22
+ def main() -> int:
23
+ verbose = "--verbose" in sys.argv
24
+ report = run_agent_eval()
25
+ headline = {
26
+ "scenarios": report["scenarios"],
27
+ "passed": report["passed"],
28
+ "success_rate": report["success_rate"],
29
+ "parse_errors": report["parse_errors"],
30
+ "parse_recovered": report["parse_recovered"],
31
+ "recovery_rate": report["recovery_rate"],
32
+ }
33
+ if verbose:
34
+ print(json.dumps(report, ensure_ascii=False, indent=2))
35
+ failures = [r for r in report["results"] if not r["ok"]]
36
+ if failures:
37
+ print(f"agent-loop-eval: FAIL {headline}")
38
+ for result in failures:
39
+ print(f" ✗ {result['name']}: {'; '.join(result['failures'])}")
40
+ return 1
41
+ print(f"agent-loop-eval: OK {headline}")
42
+ return 0
43
+
44
+
45
+ if __name__ == "__main__":
46
+ raise SystemExit(main())
@@ -16,7 +16,7 @@ REPO_ROOT = Path(__file__).resolve().parents[1]
16
16
  if str(REPO_ROOT) not in sys.path:
17
17
  sys.path.insert(0, str(REPO_ROOT))
18
18
 
19
- from knowledge_graph import KnowledgeGraphStore # noqa: E402
19
+ from lattice_brain.graph.store import KnowledgeGraphStore # noqa: E402
20
20
  from lattice_brain.quality import RetrievalBenchmarkRunner # noqa: E402
21
21
  from latticeai.services.search_service import SearchService # noqa: E402
22
22
  from latticeai.services.memory_service import MemoryService # noqa: E402
@@ -6,11 +6,12 @@ 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 = "Command Center";
9
+ const releaseTheme = "Proactive Hybrid Brain";
10
10
  const title = `${version} — ${releaseTheme}`;
11
11
  const escapedVersion = version.replaceAll(".", "\\.");
12
12
 
13
13
  const currentReleaseFiles = [
14
+ "AGENTS.md",
14
15
  "README.md",
15
16
  "ARCHITECTURE.md",
16
17
  "FEATURE_STATUS.md",
@@ -0,0 +1,360 @@
1
+ #!/usr/bin/env python3
2
+ """Synthetic Knowledge Graph performance / memory profiler.
3
+
4
+ Builds a synthetic knowledge graph against a throwaway SQLite path using the
5
+ real ``KnowledgeGraphStore`` API (the same construction pattern the unit tests
6
+ use: ``KnowledgeGraphStore(db_path, blob_dir)``), then measures wall-time
7
+ percentiles (p50/p95) and tracemalloc peak memory for the hot read/write paths:
8
+
9
+ * ``ingest_source()`` — ingestion throughput (items/sec)
10
+ * ``search()`` — FTS5/LIKE keyword search
11
+ * ``context_for_query()`` — RAG context assembly
12
+ * ``neighbors()`` / ``traverse()`` — 1-hop and depth-2 graph walks
13
+ * ``stats()`` — aggregate counters
14
+ * ``rebuild_vector_index()`` / ``vector_search()`` — offline hash-embedder
15
+ vector path (skipped gracefully if the vector path is unavailable)
16
+
17
+ Everything runs offline: no model downloads, no network, no LLM router — the
18
+ concept/triple extractors fall back to their rule-based paths and the default
19
+ embedder is the deterministic local hash model.
20
+
21
+ Honesty note: this is a *synthetic* baseline. Corpus text is generated from a
22
+ fixed vocabulary, so extraction density and FTS selectivity differ from real
23
+ user data. Timings include tracemalloc instrumentation overhead (allocation
24
+ tracking roughly doubles Python allocation cost), so treat absolute numbers as
25
+ upper bounds and use them for release-over-release comparison, not marketing.
26
+
27
+ Usage::
28
+
29
+ python scripts/profile_kg.py # 5000 sources, 50 queries (~2-2.5 min)
30
+ python scripts/profile_kg.py --nodes 500 # quick run (~10 s)
31
+ python scripts/profile_kg.py --json # machine-readable output
32
+ python scripts/profile_kg.py --db-path /tmp/kg # keep the DB around
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import argparse
38
+ import json
39
+ import random
40
+ import statistics
41
+ import sys
42
+ import tempfile
43
+ import time
44
+ import tracemalloc
45
+ from pathlib import Path
46
+ from typing import Any, Callable, Dict, List, Optional
47
+
48
+ ROOT = Path(__file__).resolve().parent.parent
49
+ if str(ROOT) not in sys.path:
50
+ sys.path.insert(0, str(ROOT))
51
+
52
+ from lattice_brain.graph.store import KnowledgeGraphStore # noqa: E402
53
+
54
+
55
+ # ── Synthetic corpus ─────────────────────────────────────────────────────────
56
+ # Recurring entities so concept nodes are shared across documents and edges
57
+ # actually form; a Korean slice exercises the FTS5 trigram path.
58
+ ENTITIES = [
59
+ "Lattice", "GraphStore", "Kubernetes", "Postgres", "FastAPI", "SQLite",
60
+ "Telegram", "MLX", "Embedding", "Pipeline", "Workspace", "Provenance",
61
+ "Scheduler", "Retrieval", "Ingestion", "Router", "Registry", "Backup",
62
+ ]
63
+ FILLER = (
64
+ "the service processes requests and stores results in the database "
65
+ "while the worker retries failed jobs and reports metrics to the "
66
+ "dashboard for the on-call engineer to review during the incident "
67
+ ).split()
68
+ KOREAN = ["프로젝트", "일정", "회의", "결정", "지식그래프", "검색", "성능", "메모리"]
69
+
70
+
71
+ def _make_text(rng: random.Random, size: str) -> str:
72
+ lengths = {"small": 40, "medium": 160, "large": 520} # words
73
+ words: List[str] = []
74
+ target = lengths[size]
75
+ while len(words) < target:
76
+ words.extend(rng.sample(FILLER, k=min(8, target - len(words))))
77
+ if rng.random() < 0.6:
78
+ words.append(rng.choice(ENTITIES))
79
+ if rng.random() < 0.25:
80
+ words.append(rng.choice(KOREAN))
81
+ if rng.random() < 0.1:
82
+ words.append(f"{rng.choice(ENTITIES)} improves {rng.choice(ENTITIES)}.")
83
+ return " ".join(words)
84
+
85
+
86
+ def _pick_size(rng: random.Random) -> str:
87
+ r = rng.random()
88
+ if r < 0.6:
89
+ return "small"
90
+ if r < 0.9:
91
+ return "medium"
92
+ return "large"
93
+
94
+
95
+ # ── Measurement helpers ──────────────────────────────────────────────────────
96
+ def _percentiles(samples_s: List[float]) -> Dict[str, float]:
97
+ if not samples_s:
98
+ return {"p50_ms": 0.0, "p95_ms": 0.0, "mean_ms": 0.0}
99
+ ordered = sorted(samples_s)
100
+
101
+ def q(p: float) -> float:
102
+ idx = min(len(ordered) - 1, max(0, round(p * (len(ordered) - 1))))
103
+ return ordered[idx]
104
+
105
+ return {
106
+ "p50_ms": round(q(0.50) * 1000, 3),
107
+ "p95_ms": round(q(0.95) * 1000, 3),
108
+ "mean_ms": round(statistics.fmean(ordered) * 1000, 3),
109
+ }
110
+
111
+
112
+ def _timed_phase(fn: Callable[[], List[float]]) -> Dict[str, Any]:
113
+ """Run one phase; return sample percentiles + phase peak memory."""
114
+ tracemalloc.reset_peak()
115
+ wall_start = time.perf_counter()
116
+ samples = fn()
117
+ wall = time.perf_counter() - wall_start
118
+ _, peak = tracemalloc.get_traced_memory()
119
+ result = _percentiles(samples)
120
+ result.update(
121
+ {
122
+ "calls": len(samples),
123
+ "wall_s": round(wall, 3),
124
+ "peak_mem_mb": round(peak / (1024 * 1024), 2),
125
+ }
126
+ )
127
+ return result
128
+
129
+
130
+ # ── Profiler ─────────────────────────────────────────────────────────────────
131
+ def run_profile(
132
+ nodes: int,
133
+ queries: int,
134
+ db_path: Optional[Path],
135
+ seed: int = 42,
136
+ ) -> Dict[str, Any]:
137
+ rng = random.Random(seed)
138
+ tmp_ctx = None
139
+ if db_path is None:
140
+ tmp_ctx = tempfile.TemporaryDirectory(prefix="ltcai-profile-kg-")
141
+ base = Path(tmp_ctx.name)
142
+ else:
143
+ base = Path(db_path)
144
+ base.mkdir(parents=True, exist_ok=True)
145
+
146
+ report: Dict[str, Any] = {
147
+ "schema_version": "kg-profile/v1",
148
+ "params": {"nodes": nodes, "queries": queries, "seed": seed,
149
+ "db_path": str(base), "ephemeral": tmp_ctx is not None},
150
+ "phases": {},
151
+ "notes": [],
152
+ }
153
+
154
+ tracemalloc.start()
155
+ try:
156
+ store = KnowledgeGraphStore(base / "graph.sqlite", base / "blobs")
157
+ report["fts_enabled"] = bool(getattr(store, "_fts_enabled", False))
158
+
159
+ # Phase 1: ingestion ------------------------------------------------
160
+ doc_ids: List[str] = []
161
+ char_total = 0
162
+
163
+ def _ingest() -> List[float]:
164
+ nonlocal char_total
165
+ samples: List[float] = []
166
+ for i in range(nodes):
167
+ size = _pick_size(rng)
168
+ text = _make_text(rng, size)
169
+ char_total += len(text)
170
+ t0 = time.perf_counter()
171
+ result = store.ingest_source(
172
+ source_type="note",
173
+ title=f"Synthetic note {i} {rng.choice(ENTITIES)}",
174
+ text=text,
175
+ source_uri=f"synthetic://note/{i}",
176
+ owner="profiler@example.com",
177
+ )
178
+ samples.append(time.perf_counter() - t0)
179
+ doc_ids.append(result["node_id"])
180
+ return samples
181
+
182
+ ingest = _timed_phase(_ingest)
183
+ ingest["items_per_sec"] = round(nodes / ingest["wall_s"], 1) if ingest["wall_s"] else 0.0
184
+ ingest["corpus_chars"] = char_total
185
+ report["phases"]["ingest_source"] = ingest
186
+
187
+ # Query terms: mix entity names, Korean tokens, and rare misses.
188
+ terms = [rng.choice(ENTITIES + KOREAN) for _ in range(max(1, queries))]
189
+ for i in range(0, len(terms), 10):
190
+ terms[i] = f"missing-term-{i}" # cache-unfriendly misses
191
+
192
+ # Phase 2: search ----------------------------------------------------
193
+ def _search() -> List[float]:
194
+ samples = []
195
+ for term in terms:
196
+ t0 = time.perf_counter()
197
+ store.search(term, limit=30)
198
+ samples.append(time.perf_counter() - t0)
199
+ return samples
200
+
201
+ report["phases"]["search"] = _timed_phase(_search)
202
+
203
+ # Phase 3: context_for_query ----------------------------------------
204
+ def _context() -> List[float]:
205
+ samples = []
206
+ for term in terms:
207
+ t0 = time.perf_counter()
208
+ store.context_for_query(term, limit=6)
209
+ samples.append(time.perf_counter() - t0)
210
+ return samples
211
+
212
+ report["phases"]["context_for_query"] = _timed_phase(_context)
213
+
214
+ # Phase 4: neighbors / traverse -------------------------------------
215
+ sample_ids = rng.sample(doc_ids, k=min(len(doc_ids), max(1, queries)))
216
+
217
+ def _neighbors() -> List[float]:
218
+ samples = []
219
+ for node_id in sample_ids:
220
+ t0 = time.perf_counter()
221
+ store.neighbors(node_id)
222
+ samples.append(time.perf_counter() - t0)
223
+ return samples
224
+
225
+ report["phases"]["neighbors"] = _timed_phase(_neighbors)
226
+
227
+ def _traverse() -> List[float]:
228
+ samples = []
229
+ for node_id in sample_ids:
230
+ t0 = time.perf_counter()
231
+ store.traverse(node_id, depth=2, limit=100)
232
+ samples.append(time.perf_counter() - t0)
233
+ return samples
234
+
235
+ report["phases"]["traverse_depth2"] = _timed_phase(_traverse)
236
+
237
+ # Phase 5: stats -----------------------------------------------------
238
+ def _stats() -> List[float]:
239
+ samples = []
240
+ for _ in range(5):
241
+ t0 = time.perf_counter()
242
+ store.stats()
243
+ samples.append(time.perf_counter() - t0)
244
+ return samples
245
+
246
+ report["phases"]["stats"] = _timed_phase(_stats)
247
+ graph_stats = store.stats()
248
+ node_counts = graph_stats.get("nodes") or {}
249
+ edge_counts = graph_stats.get("edges") or {}
250
+ report["graph"] = {
251
+ "nodes_total": sum(node_counts.values()) if isinstance(node_counts, dict) else node_counts,
252
+ "edges_total": sum(edge_counts.values()) if isinstance(edge_counts, dict) else edge_counts,
253
+ "by_node_type": node_counts if isinstance(node_counts, dict) else None,
254
+ }
255
+
256
+ # Phase 6: vector index (offline hash embedder) ----------------------
257
+ try:
258
+ def _vector_build() -> List[float]:
259
+ t0 = time.perf_counter()
260
+ store.rebuild_vector_index(full=True, include_nodes=True, include_chunks=True)
261
+ return [time.perf_counter() - t0]
262
+
263
+ report["phases"]["rebuild_vector_index"] = _timed_phase(_vector_build)
264
+
265
+ # vector_search is a brute-force scan over all stored embeddings
266
+ # (O(index size) per query); cap the query count so the default
267
+ # profile stays inside its time budget while still sampling p50/p95.
268
+ vector_terms = terms[: min(len(terms), 10)]
269
+
270
+ def _vector_search() -> List[float]:
271
+ samples = []
272
+ for term in vector_terms:
273
+ t0 = time.perf_counter()
274
+ store.vector_search(term, limit=30)
275
+ samples.append(time.perf_counter() - t0)
276
+ return samples
277
+
278
+ report["phases"]["vector_search"] = _timed_phase(_vector_search)
279
+ report["notes"].append(
280
+ "Vector phase uses the built-in deterministic hash embedder "
281
+ "(offline); real embedding providers will be slower to index "
282
+ "but produce semantically meaningful rankings."
283
+ )
284
+ except Exception as exc: # pragma: no cover - environment dependent
285
+ report["phases"]["vector"] = {"skipped": True, "reason": str(exc)[:200]}
286
+ report["notes"].append(
287
+ "Vector phase skipped: the vector path was unavailable in this "
288
+ "environment (no embedding provider required for the rest of "
289
+ "the profile)."
290
+ )
291
+
292
+ current, overall_peak = tracemalloc.get_traced_memory()
293
+ report["memory"] = {
294
+ "tracemalloc_current_mb": round(current / (1024 * 1024), 2),
295
+ "phase_peaks_are_reset_per_phase": True,
296
+ }
297
+ report["db_size_mb"] = round((base / "graph.sqlite").stat().st_size / (1024 * 1024), 2)
298
+ report["notes"].append(
299
+ "Synthetic baseline: fixed-vocabulary corpus, rule-based extraction, "
300
+ "tracemalloc enabled for the whole run (adds allocation-tracking "
301
+ "overhead to every timing)."
302
+ )
303
+ finally:
304
+ tracemalloc.stop()
305
+ if tmp_ctx is not None:
306
+ tmp_ctx.cleanup()
307
+
308
+ return report
309
+
310
+
311
+ def _print_report(report: Dict[str, Any]) -> None:
312
+ p = report["params"]
313
+ print("Knowledge Graph synthetic profile")
314
+ print(f" sources={p['nodes']} queries={p['queries']} seed={p['seed']}")
315
+ print(f" db={p['db_path']} ephemeral={p['ephemeral']} fts_enabled={report.get('fts_enabled')}")
316
+ graph = report.get("graph") or {}
317
+ print(f" graph: nodes={graph.get('nodes_total')} edges={graph.get('edges_total')} db_size={report.get('db_size_mb')} MB")
318
+ print()
319
+ header = f" {'phase':<22}{'calls':>6}{'p50 ms':>10}{'p95 ms':>10}{'mean ms':>10}{'wall s':>9}{'peak MB':>9}"
320
+ print(header)
321
+ print(" " + "-" * (len(header) - 2))
322
+ for name, phase in report["phases"].items():
323
+ if phase.get("skipped"):
324
+ print(f" {name:<22} skipped: {phase.get('reason', '')}")
325
+ continue
326
+ print(
327
+ f" {name:<22}{phase['calls']:>6}{phase['p50_ms']:>10}{phase['p95_ms']:>10}"
328
+ f"{phase['mean_ms']:>10}{phase['wall_s']:>9}{phase['peak_mem_mb']:>9}"
329
+ )
330
+ if "items_per_sec" in phase:
331
+ print(f" {'':<22} throughput: {phase['items_per_sec']} items/sec corpus: {phase['corpus_chars']} chars")
332
+ print()
333
+ for note in report.get("notes", []):
334
+ print(f" note: {note}")
335
+
336
+
337
+ def main(argv: Optional[List[str]] = None) -> int:
338
+ parser = argparse.ArgumentParser(description="Profile KnowledgeGraphStore on a synthetic corpus (offline).")
339
+ parser.add_argument("--nodes", type=int, default=5000, help="synthetic sources to ingest (default: 5000)")
340
+ parser.add_argument("--queries", type=int, default=50, help="queries per read phase (default: 50)")
341
+ parser.add_argument("--db-path", type=Path, default=None, help="directory for the profile DB (default: temp dir, auto-removed)")
342
+ parser.add_argument("--json", action="store_true", help="emit the full report as JSON")
343
+ parser.add_argument("--seed", type=int, default=42, help="corpus RNG seed (default: 42)")
344
+ args = parser.parse_args(argv)
345
+
346
+ if args.nodes < 1 or args.nodes > 200_000:
347
+ parser.error("--nodes must be between 1 and 200000")
348
+ if args.queries < 1 or args.queries > 10_000:
349
+ parser.error("--queries must be between 1 and 10000")
350
+
351
+ report = run_profile(args.nodes, args.queries, args.db_path, seed=args.seed)
352
+ if args.json:
353
+ print(json.dumps(report, ensure_ascii=False, indent=2))
354
+ else:
355
+ _print_report(report)
356
+ return 0
357
+
358
+
359
+ if __name__ == "__main__":
360
+ raise SystemExit(main())
package/setup_wizard.py CHANGED
@@ -1,7 +1,16 @@
1
1
  """Compatibility shim for :mod:`latticeai.setup.wizard`."""
2
2
 
3
3
  import sys
4
+ import warnings
4
5
 
5
- from latticeai.setup import wizard as _impl
6
+ warnings.warn(
7
+ "Importing 'setup_wizard' from the repository root is deprecated; "
8
+ "use 'from latticeai.setup import wizard' instead. "
9
+ "The root shim will be removed in a future major release.",
10
+ DeprecationWarning,
11
+ stacklevel=2,
12
+ )
13
+
14
+ from latticeai.setup import wizard as _impl # noqa: E402
6
15
 
7
16
  sys.modules[__name__] = _impl
@@ -1584,7 +1584,7 @@ dependencies = [
1584
1584
 
1585
1585
  [[package]]
1586
1586
  name = "lattice-ai-desktop"
1587
- version = "9.5.0"
1587
+ version = "9.7.0"
1588
1588
  dependencies = [
1589
1589
  "plist",
1590
1590
  "serde",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "lattice-ai-desktop"
3
- version = "9.5.0"
3
+ version = "9.7.0"
4
4
  description = "Lattice AI Digital Brain desktop shell"
5
5
  authors = ["TaeSoo Park"]
6
6
  edition = "2021"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://schema.tauri.app/config/2",
3
3
  "productName": "Lattice AI",
4
- "version": "9.5.0",
4
+ "version": "9.7.0",
5
5
  "identifier": "ai.lattice.desktop",
6
6
  "build": {
7
7
  "beforeDevCommand": "npm run frontend:dev",
@@ -1,19 +1,19 @@
1
1
  {
2
- "version": "9.5.0",
2
+ "version": "9.7.0",
3
3
  "generated_at": "vite",
4
4
  "entrypoints": {
5
- "app": "/static/app/assets/index-aJuRi-Xo.js"
5
+ "app": "/static/app/assets/index-DJC_2oub.js"
6
6
  },
7
7
  "assets": {
8
8
  "../node_modules/@tauri-apps/api/core.js": "/static/app/assets/core-CwxXejkd.js",
9
- "_primitives-ZTUlU7pR.js": "/static/app/assets/primitives-ZTUlU7pR.js",
10
- "_textarea-CMfhqPhE.js": "/static/app/assets/textarea-CMfhqPhE.js",
11
- "index.html": "/static/app/assets/index-aJuRi-Xo.js",
12
- "assets/index-BN6HIWVC.css": "/static/app/assets/index-BN6HIWVC.css",
13
- "src/pages/Act.tsx": "/static/app/assets/Act-Cdfqx4wN.js",
14
- "src/pages/Brain.tsx": "/static/app/assets/Brain-w_tAuyg6.js",
15
- "src/pages/Capture.tsx": "/static/app/assets/Capture-iJwi9LmS.js",
16
- "src/pages/Library.tsx": "/static/app/assets/Library-Do-jjhzi.js",
17
- "src/pages/System.tsx": "/static/app/assets/System-DFg_q3E6.js"
9
+ "_primitives-DL4Nip8C.js": "/static/app/assets/primitives-DL4Nip8C.js",
10
+ "_textarea-woZfCXHy.js": "/static/app/assets/textarea-woZfCXHy.js",
11
+ "index.html": "/static/app/assets/index-DJC_2oub.js",
12
+ "assets/index-85wQvEie.css": "/static/app/assets/index-85wQvEie.css",
13
+ "src/pages/Act.tsx": "/static/app/assets/Act-B6c39ays.js",
14
+ "src/pages/Brain.tsx": "/static/app/assets/Brain-D7Qg4k6M.js",
15
+ "src/pages/Capture.tsx": "/static/app/assets/Capture-VF_di68r.js",
16
+ "src/pages/Library.tsx": "/static/app/assets/Library-D_Gis2PA.js",
17
+ "src/pages/System.tsx": "/static/app/assets/System-C5s5H2ov.js"
18
18
  }
19
19
  }