ltcai 3.5.0 → 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.
- package/README.md +62 -28
- 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/architecture.md +13 -12
- package/docs/kg-schema.md +55 -0
- package/docs/privacy.md +18 -2
- package/docs/security-model.md +17 -0
- package/kg_schema.py +46 -0
- package/knowledge_graph.py +520 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/browser.py +217 -0
- package/latticeai/api/portability.py +93 -0
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/multi_agent.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/server_app.py +32 -0
- package/latticeai/services/ingestion.py +271 -0
- package/latticeai/services/kg_portability.py +177 -0
- package/package.json +3 -2
- package/scripts/build_vsix.mjs +72 -0
- package/static/v3/asset-manifest.json +7 -7
- package/static/v3/js/{app.d086489d.js → app.c541f955.js} +1 -1
- package/static/v3/js/core/{api.12b568ad.js → api.33d6320e.js} +38 -0
- package/static/v3/js/core/api.js +38 -0
- package/static/v3/js/core/{routes.d214b399.js → routes.2ce3815a.js} +1 -1
- package/static/v3/js/core/routes.js +1 -1
- package/static/v3/js/core/{shell.d05266f5.js → shell.8c163e0e.js} +2 -2
- package/static/v3/js/views/knowledge-graph.a96040a5.js +513 -0
- package/static/v3/js/views/knowledge-graph.js +293 -17
- package/static/v3/js/views/knowledge-graph.a14ea7e7.js +0 -237
|
@@ -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.
|
|
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": {
|
|
@@ -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",
|
|
@@ -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, ".")}`);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "3.
|
|
2
|
+
"version": "3.6.0",
|
|
3
3
|
"generated_at": "deterministic",
|
|
4
4
|
"entrypoints": {
|
|
5
|
-
"app": "/static/v3/js/app.
|
|
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",
|
|
@@ -19,13 +19,13 @@
|
|
|
19
19
|
"static/v3/css/lattice.components.css": "/static/v3/css/lattice.components.9b49d614.css",
|
|
20
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.
|
|
23
|
-
"static/v3/js/core/api.js": "/static/v3/js/core/api.
|
|
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.
|
|
28
|
-
"static/v3/js/core/shell.js": "/static/v3/js/core/shell.
|
|
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.
|
|
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",
|
|
@@ -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.
|
|
6
|
+
import { boot } from "./core/shell.8c163e0e.js";
|
|
7
7
|
|
|
8
8
|
const root = document.getElementById("app");
|
|
9
9
|
if (root) boot(root);
|
|
@@ -507,6 +507,44 @@ export const api = {
|
|
|
507
507
|
// Hooks dispatch (real backend: POST /api/hooks/run + GET /api/hooks/runs)
|
|
508
508
|
hookRun(body) { return raw("/api/hooks/run", { method: "POST", body }); },
|
|
509
509
|
hookRuns(limit = 50, kind) { return withFallback(`/api/hooks/runs?limit=${encodeURIComponent(limit)}${kind ? "&kind=" + encodeURIComponent(kind) : ""}`, {}, { runs: [], total: 0 }); },
|
|
510
|
+
|
|
511
|
+
/* ── v3.6 Knowledge Graph First: ingestion provenance + portability ─────
|
|
512
|
+
* The graph is the durable asset; these surface its health, where every node
|
|
513
|
+
* came from, and local export/import/backup. All fallback-safe; never fake. */
|
|
514
|
+
|
|
515
|
+
/** GET /api/knowledge-graph/portability — schema versions + stats + provenance counts. */
|
|
516
|
+
async kgPortability() {
|
|
517
|
+
const res = await raw("/api/knowledge-graph/portability");
|
|
518
|
+
if (res.ok && res.data && res.data.available) {
|
|
519
|
+
return { ok: true, status: res.status, data: res.data, source: "live" };
|
|
520
|
+
}
|
|
521
|
+
return {
|
|
522
|
+
ok: false, status: res.status, source: "unavailable",
|
|
523
|
+
data: { available: false, graph_schema_version: null, embed_dim: null,
|
|
524
|
+
stats: { nodes: {}, edges: {} },
|
|
525
|
+
provenance: { total: 0, by_source_type: {}, embedded: 0, duplicates: 0, last_ingested_at: null } },
|
|
526
|
+
};
|
|
527
|
+
},
|
|
528
|
+
|
|
529
|
+
/** GET /api/knowledge-graph/provenance — recent ingestions (newest first). */
|
|
530
|
+
kgProvenance(limit = 50, sourceType) {
|
|
531
|
+
const qs = `?limit=${encodeURIComponent(limit)}${sourceType ? "&source_type=" + encodeURIComponent(sourceType) : ""}`;
|
|
532
|
+
return withFallback(`/api/knowledge-graph/provenance${qs}`, {}, { items: [], count: 0 });
|
|
533
|
+
},
|
|
534
|
+
|
|
535
|
+
/** POST /api/knowledge-graph/export — logical JSON export of the whole graph. */
|
|
536
|
+
graphExport() { return raw("/api/knowledge-graph/export", { method: "POST", body: {} }); },
|
|
537
|
+
|
|
538
|
+
/** POST /api/knowledge-graph/import — import an export artifact (merge|replace). */
|
|
539
|
+
graphImport(artifact, mode = "merge", dryRun = false) {
|
|
540
|
+
return raw("/api/knowledge-graph/import", { method: "POST", body: { artifact, mode, dry_run: dryRun } });
|
|
541
|
+
},
|
|
542
|
+
|
|
543
|
+
/** POST /api/knowledge-graph/backup — binary backup (sqlite + blobs) to a local zip. */
|
|
544
|
+
graphBackup() { return raw("/api/knowledge-graph/backup", { method: "POST", body: {} }); },
|
|
545
|
+
|
|
546
|
+
/** POST /api/browser/read-url — fetch a public URL locally into the graph. */
|
|
547
|
+
browserReadUrl(url) { return raw("/api/browser/read-url", { method: "POST", body: { url } }); },
|
|
510
548
|
};
|
|
511
549
|
|
|
512
550
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
package/static/v3/js/core/api.js
CHANGED
|
@@ -507,6 +507,44 @@ export const api = {
|
|
|
507
507
|
// Hooks dispatch (real backend: POST /api/hooks/run + GET /api/hooks/runs)
|
|
508
508
|
hookRun(body) { return raw("/api/hooks/run", { method: "POST", body }); },
|
|
509
509
|
hookRuns(limit = 50, kind) { return withFallback(`/api/hooks/runs?limit=${encodeURIComponent(limit)}${kind ? "&kind=" + encodeURIComponent(kind) : ""}`, {}, { runs: [], total: 0 }); },
|
|
510
|
+
|
|
511
|
+
/* ── v3.6 Knowledge Graph First: ingestion provenance + portability ─────
|
|
512
|
+
* The graph is the durable asset; these surface its health, where every node
|
|
513
|
+
* came from, and local export/import/backup. All fallback-safe; never fake. */
|
|
514
|
+
|
|
515
|
+
/** GET /api/knowledge-graph/portability — schema versions + stats + provenance counts. */
|
|
516
|
+
async kgPortability() {
|
|
517
|
+
const res = await raw("/api/knowledge-graph/portability");
|
|
518
|
+
if (res.ok && res.data && res.data.available) {
|
|
519
|
+
return { ok: true, status: res.status, data: res.data, source: "live" };
|
|
520
|
+
}
|
|
521
|
+
return {
|
|
522
|
+
ok: false, status: res.status, source: "unavailable",
|
|
523
|
+
data: { available: false, graph_schema_version: null, embed_dim: null,
|
|
524
|
+
stats: { nodes: {}, edges: {} },
|
|
525
|
+
provenance: { total: 0, by_source_type: {}, embedded: 0, duplicates: 0, last_ingested_at: null } },
|
|
526
|
+
};
|
|
527
|
+
},
|
|
528
|
+
|
|
529
|
+
/** GET /api/knowledge-graph/provenance — recent ingestions (newest first). */
|
|
530
|
+
kgProvenance(limit = 50, sourceType) {
|
|
531
|
+
const qs = `?limit=${encodeURIComponent(limit)}${sourceType ? "&source_type=" + encodeURIComponent(sourceType) : ""}`;
|
|
532
|
+
return withFallback(`/api/knowledge-graph/provenance${qs}`, {}, { items: [], count: 0 });
|
|
533
|
+
},
|
|
534
|
+
|
|
535
|
+
/** POST /api/knowledge-graph/export — logical JSON export of the whole graph. */
|
|
536
|
+
graphExport() { return raw("/api/knowledge-graph/export", { method: "POST", body: {} }); },
|
|
537
|
+
|
|
538
|
+
/** POST /api/knowledge-graph/import — import an export artifact (merge|replace). */
|
|
539
|
+
graphImport(artifact, mode = "merge", dryRun = false) {
|
|
540
|
+
return raw("/api/knowledge-graph/import", { method: "POST", body: { artifact, mode, dry_run: dryRun } });
|
|
541
|
+
},
|
|
542
|
+
|
|
543
|
+
/** POST /api/knowledge-graph/backup — binary backup (sqlite + blobs) to a local zip. */
|
|
544
|
+
graphBackup() { return raw("/api/knowledge-graph/backup", { method: "POST", body: {} }); },
|
|
545
|
+
|
|
546
|
+
/** POST /api/browser/read-url — fetch a public URL locally into the graph. */
|
|
547
|
+
browserReadUrl(url) { return raw("/api/browser/read-url", { method: "POST", body: { url } }); },
|
|
510
548
|
};
|
|
511
549
|
|
|
512
550
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -33,7 +33,7 @@ export const ROUTES = [
|
|
|
33
33
|
|
|
34
34
|
// Retrieval (the product identity)
|
|
35
35
|
{ key: "hybrid-search", label: "Search", icon: "arrows-join", group: "retrieval", minMode: "basic", view: "hybrid-search", title: "Hybrid Search", desc: "Graph structure fused with vector similarity." },
|
|
36
|
-
{ key: "knowledge-graph", label: "Knowledge", icon: "chart-dots-3", group: "retrieval", minMode: "basic", view: "knowledge-graph", title: "Knowledge Graph", desc: "
|
|
36
|
+
{ key: "knowledge-graph", label: "Knowledge", icon: "chart-dots-3", group: "retrieval", minMode: "basic", view: "knowledge-graph", title: "Knowledge Graph", desc: "Your digital brain — every source converges here. Explore, ingest, and export." },
|
|
37
37
|
{ key: "memory", label: "Memory", icon: "brain", group: "retrieval", minMode: "basic", view: "memory", title: "Memory", desc: "Long-term workspace, project, agent, and conversation memory." },
|
|
38
38
|
|
|
39
39
|
// Compute
|
|
@@ -33,7 +33,7 @@ export const ROUTES = [
|
|
|
33
33
|
|
|
34
34
|
// Retrieval (the product identity)
|
|
35
35
|
{ key: "hybrid-search", label: "Search", icon: "arrows-join", group: "retrieval", minMode: "basic", view: "hybrid-search", title: "Hybrid Search", desc: "Graph structure fused with vector similarity." },
|
|
36
|
-
{ key: "knowledge-graph", label: "Knowledge", icon: "chart-dots-3", group: "retrieval", minMode: "basic", view: "knowledge-graph", title: "Knowledge Graph", desc: "
|
|
36
|
+
{ key: "knowledge-graph", label: "Knowledge", icon: "chart-dots-3", group: "retrieval", minMode: "basic", view: "knowledge-graph", title: "Knowledge Graph", desc: "Your digital brain — every source converges here. Explore, ingest, and export." },
|
|
37
37
|
{ key: "memory", label: "Memory", icon: "brain", group: "retrieval", minMode: "basic", view: "memory", title: "Memory", desc: "Long-term workspace, project, agent, and conversation memory." },
|
|
38
38
|
|
|
39
39
|
// Compute
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
import { h, icon, $, $$ } from "./dom.a2773eb0.js";
|
|
9
9
|
import { store } from "./store.34ebd5e6.js";
|
|
10
|
-
import { api } from "./api.
|
|
10
|
+
import { api } from "./api.33d6320e.js";
|
|
11
11
|
import * as c from "./components.f25b3b93.js";
|
|
12
12
|
import { createRouter } from "./router.584570f2.js";
|
|
13
|
-
import { GROUPS, ROUTES, ROUTE_BY_KEY, MODE_RANK, visibleRoutes, loadView } from "./routes.
|
|
13
|
+
import { GROUPS, ROUTES, ROUTE_BY_KEY, MODE_RANK, visibleRoutes, loadView } from "./routes.2ce3815a.js";
|
|
14
14
|
|
|
15
15
|
const MODES = [
|
|
16
16
|
{ key: "basic", label: "Basic", icon: "circle" },
|