ltcai 4.3.1 → 4.3.3

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 (54) hide show
  1. package/README.md +186 -278
  2. package/docs/CHANGELOG.md +91 -0
  3. package/docs/V4_3_2_DEADCODE_AUDIT_REPORT.md +174 -0
  4. package/docs/V4_3_2_DOCUMENTATION_CLEANUP_REPORT.md +81 -0
  5. package/docs/V4_3_2_GITHUB_VERCEL_CHECK_REPORT.md +75 -0
  6. package/docs/V4_3_2_GRAPH_UX_REPORT.md +48 -0
  7. package/docs/V4_3_2_INDEPENDENT_AUDIT_PACKAGE.md +209 -0
  8. package/docs/V4_3_2_PRODUCT_POLISH_REPORT.md +57 -0
  9. package/docs/V4_3_2_SELF_AUDIT_REPORT.md +63 -0
  10. package/docs/V4_3_2_VALIDATION_REPORT.md +97 -0
  11. package/docs/V4_3_3_VALIDATION_REPORT.md +46 -0
  12. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +18 -19
  13. package/frontend/openapi.json +1 -1
  14. package/frontend/src/components/primitives.tsx +92 -10
  15. package/frontend/src/pages/Act.tsx +11 -9
  16. package/frontend/src/pages/Ask.tsx +2 -2
  17. package/frontend/src/pages/Brain.tsx +607 -65
  18. package/frontend/src/pages/Capture.tsx +11 -7
  19. package/frontend/src/pages/Library.tsx +3 -3
  20. package/frontend/src/pages/System.tsx +186 -23
  21. package/lattice_brain/__init__.py +1 -1
  22. package/latticeai/__init__.py +1 -1
  23. package/latticeai/core/marketplace.py +1 -1
  24. package/latticeai/core/multi_agent.py +1 -1
  25. package/latticeai/core/workspace_os.py +1 -1
  26. package/package.json +3 -6
  27. package/scripts/build_vercel_static.mjs +77 -0
  28. package/scripts/check_markdown_links.mjs +75 -0
  29. package/src-tauri/Cargo.lock +1 -1
  30. package/src-tauri/Cargo.toml +1 -1
  31. package/src-tauri/src/main.rs +12 -2
  32. package/src-tauri/tauri.conf.json +1 -1
  33. package/static/app/asset-manifest.json +5 -5
  34. package/static/app/assets/index-CHHal8Zl.css +2 -0
  35. package/static/app/assets/index-pdzil9ac.js +333 -0
  36. package/static/app/assets/index-pdzil9ac.js.map +1 -0
  37. package/static/app/index.html +2 -2
  38. package/latticeai/api/deps.py +0 -15
  39. package/scripts/capture/README.md +0 -28
  40. package/scripts/capture/capture_enterprise.js +0 -8
  41. package/scripts/capture/capture_graph.js +0 -8
  42. package/scripts/capture/capture_onboarding.js +0 -8
  43. package/scripts/capture/capture_page.js +0 -43
  44. package/scripts/capture/capture_release_media.js +0 -125
  45. package/scripts/capture/capture_skills.js +0 -8
  46. package/scripts/capture/capture_v340.js +0 -88
  47. package/scripts/capture/capture_workspace.js +0 -8
  48. package/scripts/generate_diagrams.py +0 -512
  49. package/scripts/release-0.3.1.sh +0 -105
  50. package/scripts/take_screenshots.js +0 -69
  51. package/static/app/assets/index-BhPuj8rT.js +0 -333
  52. package/static/app/assets/index-BhPuj8rT.js.map +0 -1
  53. package/static/app/assets/index-yZswHE3d.css +0 -2
  54. package/static/css/tokens.3ba22e37.css +0 -260
@@ -2,7 +2,7 @@ import * as React from "react";
2
2
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
3
  import { FolderPlus, Globe2, HardDrive, Upload } from "lucide-react";
4
4
  import { latticeApi } from "@/api/client";
5
- import { ActionButton, DataPanel, EntityList, JsonView, Tabs } from "@/components/primitives";
5
+ import { ActionButton, DataPanel, EntityList, OperationResult, StructuredView, Tabs } from "@/components/primitives";
6
6
  import { Button } from "@/components/ui/button";
7
7
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
8
8
  import { Input } from "@/components/ui/input";
@@ -59,7 +59,11 @@ function FilesPanel() {
59
59
  <span className="text-sm text-muted-foreground">PDF, DOCX, XLSX, PPTX, TXT, MD, CSV according to backend policy.</span>
60
60
  <input type="file" multiple className="sr-only" onChange={(e) => e.target.files && upload.mutate(e.target.files)} />
61
61
  </label>
62
- {upload.data ? <JsonView value={upload.data.map((item) => item.data)} /> : null}
62
+ {upload.data ? (
63
+ <div className="mt-3 space-y-2">
64
+ {upload.data.map((item, index) => <OperationResult key={index} result={item} successLabel="Upload completed" />)}
65
+ </div>
66
+ ) : null}
63
67
  </CardContent>
64
68
  </Card>
65
69
  <DataPanel title="Uploaded documents" result={docs.data}>
@@ -88,7 +92,7 @@ function LocalPanel() {
88
92
  <CardContent className="space-y-3">
89
93
  <Input value={path} onChange={(e) => setPath(e.target.value)} placeholder="/Users/me/Documents/project" />
90
94
  <Button disabled={!path.trim() || connect.isPending} onClick={() => connect.mutate()}>Connect and watch</Button>
91
- {connect.data ? <JsonView value={connect.data.data || connect.data.error} /> : null}
95
+ {connect.data ? <OperationResult result={connect.data} successLabel="Folder connection requested" /> : null}
92
96
  </CardContent>
93
97
  </Card>
94
98
  <DataPanel title="Connected sources" result={local.data}>
@@ -107,7 +111,7 @@ function LocalPanel() {
107
111
  )}
108
112
  </DataPanel>
109
113
  <DataPanel title="Local runtime probe" result={agent.data} className="xl:col-span-2">
110
- {(data) => <JsonView value={data} />}
114
+ {(data) => <StructuredView value={data} />}
111
115
  </DataPanel>
112
116
  </div>
113
117
  );
@@ -127,7 +131,7 @@ function BrowserPanel() {
127
131
  <Input value={url} onChange={(e) => setUrl(e.target.value)} placeholder="https://example.com/article" />
128
132
  <Button disabled={!url.trim() || read.isPending} onClick={() => read.mutate()}>Capture URL</Button>
129
133
  </div>
130
- {read.data ? <JsonView value={read.data.data || read.data.error} /> : null}
134
+ {read.data ? <OperationResult result={read.data} successLabel="URL capture requested" /> : null}
131
135
  </CardContent>
132
136
  </Card>
133
137
  );
@@ -139,10 +143,10 @@ function PipelinePanel() {
139
143
  return (
140
144
  <div className="grid gap-4 xl:grid-cols-2">
141
145
  <DataPanel title="Index pipeline" result={index.data}>
142
- {(data) => <JsonView value={data} />}
146
+ {(data) => <StructuredView value={data} />}
143
147
  </DataPanel>
144
148
  <DataPanel title="Graph totals" result={stats.data}>
145
- {(data) => <JsonView value={data} />}
149
+ {(data) => <StructuredView value={data} />}
146
150
  </DataPanel>
147
151
  <Card className="xl:col-span-2">
148
152
  <CardHeader>
@@ -2,7 +2,7 @@ import * as React from "react";
2
2
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
3
  import { Boxes, Cpu, PackagePlus, Plug, Puzzle } from "lucide-react";
4
4
  import { latticeApi } from "@/api/client";
5
- import { ActionButton, DataPanel, EntityList, JsonView, Tabs } from "@/components/primitives";
5
+ import { ActionButton, DataPanel, EntityList, OperationResult, StructuredView, Tabs } from "@/components/primitives";
6
6
  import { Badge } from "@/components/ui/badge";
7
7
  import { Button } from "@/components/ui/button";
8
8
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
@@ -82,7 +82,7 @@ function ModelsPanel() {
82
82
  )}
83
83
  </DataPanel>
84
84
  <DataPanel title="Embedding provider" result={emb.data}>
85
- {(data) => <JsonView value={data} />}
85
+ {(data) => <StructuredView value={data} />}
86
86
  </DataPanel>
87
87
  </div>
88
88
  );
@@ -151,7 +151,7 @@ function McpPanel() {
151
151
  <Input value={query} onChange={(e) => setQuery(e.target.value)} />
152
152
  <Button onClick={() => rec.mutate()} disabled={!query.trim() || rec.isPending}>Recommend</Button>
153
153
  </div>
154
- {rec.data ? <JsonView value={rec.data.data || rec.data.error} /> : null}
154
+ {rec.data ? <OperationResult result={rec.data} successLabel="Recommendation completed" /> : null}
155
155
  </CardContent>
156
156
  </Card>
157
157
  </div>
@@ -1,14 +1,14 @@
1
1
  import * as React from "react";
2
2
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
- import { Activity, KeyRound, Network, ShieldCheck, UserCircle, Users } from "lucide-react";
3
+ import { Network, ShieldCheck, UserCircle, Users } from "lucide-react";
4
4
  import { latticeApi } from "@/api/client";
5
- import { ActionButton, DataPanel, EntityList, JsonView, KeyValueList, Tabs } from "@/components/primitives";
5
+ import { ActionButton, DataPanel, EmptyState, EntityList, KeyValueList, OperationResult, StatGrid, StructuredView, Tabs } from "@/components/primitives";
6
6
  import { Badge } from "@/components/ui/badge";
7
7
  import { Button } from "@/components/ui/button";
8
8
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
9
9
  import { Input } from "@/components/ui/input";
10
10
  import { useAppStore } from "@/store/appStore";
11
- import { asArray, titleize } from "@/lib/utils";
11
+ import { asArray, shortId, titleize } from "@/lib/utils";
12
12
 
13
13
  type SystemTab = "account" | "workspaces" | "snapshots" | "activity" | "network" | "settings" | "admin";
14
14
 
@@ -84,11 +84,13 @@ function AccountPanel() {
84
84
  <Button variant="outline" onClick={() => changePassword.mutate()} disabled={!password || !newPassword || changePassword.isPending}>Change password</Button>
85
85
  <ActionButton label="Logout" action={() => latticeApi.logout()} invalidate={["profile"]} />
86
86
  </div>
87
- {[login.data, register.data, saveProfile.data, changePassword.data].filter(Boolean).map((item, i) => <JsonView key={i} value={item?.data || item?.error} />)}
87
+ {[login.data, register.data, saveProfile.data, changePassword.data].filter(Boolean).map((item, i) => (
88
+ <OperationResult key={i} result={item} successLabel="Account request completed" />
89
+ ))}
88
90
  </CardContent>
89
91
  </Card>
90
92
  <DataPanel title="SSO config" result={sso.data} className="xl:col-span-2">
91
- {(data) => <JsonView value={data} />}
93
+ {(data) => <StructuredView value={data} />}
92
94
  </DataPanel>
93
95
  </div>
94
96
  );
@@ -195,7 +197,7 @@ function SnapshotsPanel() {
195
197
  <Input value={after} onChange={(e) => setAfter(e.target.value)} placeholder="after id" />
196
198
  </div>
197
199
  <Button variant="outline" onClick={() => compare.mutate()} disabled={!before || !after || compare.isPending}>Compare</Button>
198
- {compare.data ? <JsonView value={compare.data.data || compare.data.error} /> : null}
200
+ {compare.data ? <OperationResult result={compare.data} successLabel="Snapshot comparison completed" /> : null}
199
201
  </CardContent>
200
202
  </Card>
201
203
  <DataPanel title="Time machine" result={timeline.data} className="xl:col-span-2">
@@ -214,7 +216,7 @@ function ActivityPanel() {
214
216
  {(data) => <EntityList items={(data as Record<string, unknown>).events} titleKey="event_type" metaKey="area" limit={14} />}
215
217
  </DataPanel>
216
218
  <DataPanel title="Presence" result={presence.data}>
217
- {(data) => <JsonView value={data} />}
219
+ {(data) => <PresenceView data={data as Record<string, unknown>} />}
218
220
  </DataPanel>
219
221
  </div>
220
222
  );
@@ -234,7 +236,7 @@ function NetworkPanel() {
234
236
  return (
235
237
  <div className="grid gap-4 xl:grid-cols-[0.8fr_1.2fr]">
236
238
  <DataPanel title="Device identity" result={identity.data}>
237
- {(data) => <JsonView value={data} />}
239
+ {(data) => <DeviceIdentityView data={data as Record<string, unknown>} />}
238
240
  </DataPanel>
239
241
  <Card>
240
242
  <CardHeader>
@@ -246,7 +248,7 @@ function NetworkPanel() {
246
248
  <Input value={baseUrl} onChange={(e) => setBaseUrl(e.target.value)} placeholder="http://peer.local:8765" />
247
249
  <Input value={publicKey} onChange={(e) => setPublicKey(e.target.value)} placeholder="trusted public key" />
248
250
  <Button disabled={!name || !baseUrl || !publicKey || pair.isPending} onClick={() => pair.mutate()}>Pair</Button>
249
- {pair.data ? <JsonView value={pair.data.data || pair.data.error} /> : null}
251
+ {pair.data ? <OperationResult result={pair.data} successLabel="Peer pairing request completed" /> : null}
250
252
  </CardContent>
251
253
  </Card>
252
254
  <DataPanel title="Peers" result={peers.data} className="xl:col-span-2">
@@ -289,6 +291,7 @@ function SettingsPanel() {
289
291
  const [restorePath, setRestorePath] = React.useState("");
290
292
  const [archivePassphrase, setArchivePassphrase] = React.useState("");
291
293
  const [restoreConfirm, setRestoreConfirm] = React.useState(false);
294
+ const [importConfirm, setImportConfirm] = React.useState(false);
292
295
  const docker = useMutation({ mutationFn: (consent: boolean) => latticeApi.dockerPostgres({ consent, dry_run: !consent, port: 5432 }) });
293
296
  const migration = useMutation({
294
297
  mutationFn: () => latticeApi.migratePostgres({ dsn, schema_name: schema || "lattice_brain", dry_run: true }),
@@ -313,6 +316,16 @@ function SettingsPanel() {
313
316
  qc.invalidateQueries({ queryKey: ["backupHealth"] });
314
317
  },
315
318
  });
319
+ const archiveImportDryRun = useMutation({
320
+ mutationFn: () => latticeApi.brainArchiveImport({ path: restorePath, passphrase: archivePassphrase, dry_run: true, confirm: false }),
321
+ });
322
+ const archiveImport = useMutation({
323
+ mutationFn: () => latticeApi.brainArchiveImport({ path: restorePath, passphrase: archivePassphrase, dry_run: false, confirm: importConfirm }),
324
+ onSuccess: () => {
325
+ qc.invalidateQueries({ queryKey: ["brainStorage"] });
326
+ qc.invalidateQueries({ queryKey: ["backupHealth"] });
327
+ },
328
+ });
316
329
  return (
317
330
  <div className="grid gap-4 xl:grid-cols-3">
318
331
  <Card>
@@ -329,16 +342,16 @@ function SettingsPanel() {
329
342
  </CardContent>
330
343
  </Card>
331
344
  <DataPanel title="Server health" result={health.data}>
332
- {(data) => <JsonView value={data} />}
345
+ {(data) => <HealthView data={data as Record<string, unknown>} />}
333
346
  </DataPanel>
334
347
  <DataPanel title="Host telemetry" result={sys.data}>
335
- {(data) => <JsonView value={data} />}
348
+ {(data) => <StructuredView value={data} />}
336
349
  </DataPanel>
337
350
  <DataPanel title="Brain storage" result={storage.data} className="xl:col-span-3">
338
- {(data) => <JsonView value={data} />}
351
+ {(data) => <StorageView data={data as Record<string, unknown>} />}
339
352
  </DataPanel>
340
353
  <DataPanel title="Backup health" result={backupHealth.data} className="xl:col-span-3">
341
- {(data) => <JsonView value={data} />}
354
+ {(data) => <BackupHealthView data={data as Record<string, unknown>} />}
342
355
  </DataPanel>
343
356
  <Card className="xl:col-span-3">
344
357
  <CardHeader>
@@ -356,14 +369,20 @@ function SettingsPanel() {
356
369
  <Button variant="outline" onClick={() => archiveInspect.mutate()} disabled={!restorePath || archiveInspect.isPending}>Inspect</Button>
357
370
  <Button variant="outline" onClick={() => archiveVerify.mutate()} disabled={!restorePath || !archivePassphrase || archiveVerify.isPending}>Verify</Button>
358
371
  <Button variant="outline" onClick={() => archiveDryRun.mutate()} disabled={!restorePath || !archivePassphrase || archiveDryRun.isPending}>Restore dry run</Button>
372
+ <Button variant="outline" onClick={() => archiveImportDryRun.mutate()} disabled={!restorePath || !archivePassphrase || archiveImportDryRun.isPending}>Import dry run</Button>
359
373
  <label className="flex items-center gap-2 rounded-md border border-border px-3 py-2 text-sm">
360
374
  <input type="checkbox" checked={restoreConfirm} onChange={(e) => setRestoreConfirm(e.target.checked)} />
361
375
  Confirm restore
362
376
  </label>
363
377
  <Button variant="destructive" onClick={() => archiveRestore.mutate()} disabled={!restorePath || !archivePassphrase || !restoreConfirm || archiveRestore.isPending}>Restore</Button>
378
+ <label className="flex items-center gap-2 rounded-md border border-border px-3 py-2 text-sm">
379
+ <input type="checkbox" checked={importConfirm} onChange={(e) => setImportConfirm(e.target.checked)} />
380
+ Confirm import
381
+ </label>
382
+ <Button variant="outline" onClick={() => archiveImport.mutate()} disabled={!restorePath || !archivePassphrase || !importConfirm || archiveImport.isPending}>Import</Button>
364
383
  </div>
365
- {[archiveCreate.data, archiveInspect.data, archiveVerify.data, archiveDryRun.data, archiveRestore.data].filter(Boolean).map((item, i) => (
366
- <JsonView key={i} value={item?.data || item?.error} />
384
+ {[archiveCreate.data, archiveInspect.data, archiveVerify.data, archiveDryRun.data, archiveRestore.data, archiveImportDryRun.data, archiveImport.data].filter(Boolean).map((item, i) => (
385
+ <OperationResult key={i} result={item} successLabel="Archive request completed" />
367
386
  ))}
368
387
  </CardContent>
369
388
  </Card>
@@ -386,14 +405,14 @@ function SettingsPanel() {
386
405
  <Button onClick={() => docker.mutate(true)} disabled={!dockerConsent || docker.isPending}>Start Docker</Button>
387
406
  <Button variant="outline" onClick={() => migration.mutate()} disabled={!dsn || migration.isPending}>Plan migration</Button>
388
407
  </div>
389
- {docker.data ? <JsonView value={docker.data.data || docker.data.error} /> : null}
390
- {migration.data ? <JsonView value={migration.data.data || migration.data.error} /> : null}
408
+ {docker.data ? <OperationResult result={docker.data} successLabel="Docker setup request completed" /> : null}
409
+ {migration.data ? <OperationResult result={migration.data} successLabel="Migration plan completed" /> : null}
391
410
  </CardContent>
392
411
  </Card>
393
412
  <DataPanel title="Computer memory" result={comp.data} className="xl:col-span-3">
394
413
  {(data) => (
395
414
  <div className="space-y-3">
396
- <JsonView value={data} />
415
+ <StructuredView value={data} />
397
416
  <div className="flex gap-2">
398
417
  <ActionButton label="Enable memory" action={() => latticeApi.setComputerMemory(true)} invalidate={["computerMemory"]} />
399
418
  <ActionButton label="Disable memory" action={() => latticeApi.setComputerMemory(false)} invalidate={["computerMemory"]} variant="destructive" />
@@ -405,6 +424,150 @@ function SettingsPanel() {
405
424
  );
406
425
  }
407
426
 
427
+ function isRecord(value: unknown): value is Record<string, unknown> {
428
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
429
+ }
430
+
431
+ function textValue(value: unknown, fallback = "not reported") {
432
+ if (value === null || value === undefined || value === "") return fallback;
433
+ if (typeof value === "boolean") return value ? "enabled" : "disabled";
434
+ return String(value);
435
+ }
436
+
437
+ function PresenceView({ data }: { data: Record<string, unknown> }) {
438
+ const rows = asArray<Record<string, unknown>>(data.presence || data.clients || data);
439
+ if (!rows.length) return <EmptyState title="No active presence" detail="No live collaborators or realtime clients are currently reported." />;
440
+ return <EntityList items={rows} titleKey="user" metaKey="workspace_id" />;
441
+ }
442
+
443
+ function DeviceIdentityView({ data }: { data: Record<string, unknown> }) {
444
+ const publicKey = textValue(data.public_key, "");
445
+ return (
446
+ <div className="space-y-3">
447
+ <div className="flex flex-wrap items-center gap-2">
448
+ <Badge variant="success">local device</Badge>
449
+ <Badge variant="muted">{textValue(data.algorithm, "identity key")}</Badge>
450
+ </div>
451
+ <KeyValueList data={{
452
+ device_id: data.device_id || data.id || "not reported",
453
+ fingerprint: data.fingerprint || "not reported",
454
+ public_key: publicKey ? shortId(publicKey.replace(/\s+/g, " "), 72) : "not reported",
455
+ }} />
456
+ </div>
457
+ );
458
+ }
459
+
460
+ function HealthView({ data }: { data: Record<string, unknown> }) {
461
+ return (
462
+ <div className="space-y-3">
463
+ <StatGrid stats={[
464
+ { label: "Status", value: data.status || data.ok || "reported" },
465
+ { label: "Version", value: data.version || "not reported" },
466
+ { label: "Mode", value: data.mode || data.environment || "local" },
467
+ { label: "Port", value: data.port || data.backend_port || "configured" },
468
+ ]} />
469
+ <StructuredView value={data} />
470
+ </div>
471
+ );
472
+ }
473
+
474
+ function StorageView({ data }: { data: Record<string, unknown> }) {
475
+ const active = isRecord(data.active) ? data.active : data;
476
+ const postgres = isRecord(data.postgres) ? data.postgres : {};
477
+ const backup = isRecord(data.backup_health) ? data.backup_health : {};
478
+ const vector = active.vector_search || active.vector || data.vector_search || data.sqlite_vec;
479
+ const postgresAvailable = Boolean(postgres.available || postgres.connected || postgres.enabled);
480
+ return (
481
+ <div className="space-y-4">
482
+ <StatGrid stats={[
483
+ { label: "Active engine", value: active.engine || data.engine || "sqlite" },
484
+ { label: "SQLite default", value: active.engine === "postgres" ? "scale mode" : "enabled" },
485
+ { label: "Vector search", value: vector || "not reported" },
486
+ { label: "Postgres", value: postgresAvailable ? "available" : "optional" },
487
+ ]} />
488
+ <div className="grid gap-3 md:grid-cols-3">
489
+ <StatusCard title="SQLite" status={active.available === false ? "unavailable" : "default"} detail={textValue(active.reason || active.path || data.path, "Local brain storage is active by default.")} />
490
+ <StatusCard title="Vector search" status={textValue(vector, "reported")} detail={textValue(active.vector_reason || active.sqlite_vec_reason || data.vector_reason, "Uses the configured local vector capability or reports why it is unavailable.")} />
491
+ <StatusCard title="Postgres" status={postgresAvailable ? "available" : "not enabled"} detail={textValue(postgres.reason || postgres.dsn || postgres.status, "Postgres scale mode is opt-in and never required for local use.")} />
492
+ </div>
493
+ {Object.keys(backup).length ? <StructuredView value={{ backup_health: backup }} /> : null}
494
+ </div>
495
+ );
496
+ }
497
+
498
+ function BackupHealthView({ data }: { data: Record<string, unknown> }) {
499
+ return (
500
+ <div className="space-y-3">
501
+ <StatGrid stats={[
502
+ { label: "Available", value: data.available === false ? "no" : "yes" },
503
+ { label: "Backups", value: data.count || data.backups || 0 },
504
+ { label: "Encrypted", value: data.encrypted_archives || 0 },
505
+ { label: "Zip backups", value: data.zip_backups || 0 },
506
+ ]} />
507
+ <KeyValueList data={{
508
+ directory: data.directory || "not reported",
509
+ latest: data.latest || "none reported",
510
+ last_verified: data.last_verified || data.verified_at || "not reported",
511
+ failure: data.error || data.reason || "none reported",
512
+ }} />
513
+ </div>
514
+ );
515
+ }
516
+
517
+ function StatusCard({ title, status, detail }: { title: string; status: string; detail: string }) {
518
+ const variant = /unavailable|failed|denied|disabled|not enabled/i.test(status) ? "warning" : "success";
519
+ return (
520
+ <div className="rounded-md border border-border bg-background p-3">
521
+ <div className="flex items-center justify-between gap-2">
522
+ <div className="font-medium">{title}</div>
523
+ <Badge variant={variant}>{status}</Badge>
524
+ </div>
525
+ <p className="mt-2 text-sm text-muted-foreground">{detail}</p>
526
+ </div>
527
+ );
528
+ }
529
+
530
+ function HardeningView({ data }: { data: Record<string, unknown> }) {
531
+ const startup = isRecord(data.startup) ? data.startup : {};
532
+ const privacy = isRecord(data.privacy) ? data.privacy : {};
533
+ const storage = isRecord(data.storage) ? data.storage : {};
534
+ const backup = isRecord(data.backup) ? data.backup : {};
535
+ const identity = isRecord(data.device_identity) ? data.device_identity : {};
536
+ const permissions = isRecord(data.permissions) ? data.permissions : {};
537
+ return (
538
+ <div className="space-y-3">
539
+ <StatGrid stats={[
540
+ { label: "Version", value: data.version || "reported" },
541
+ { label: "Local only", value: privacy.local_only_default ?? startup.local_only_default ?? "reported" },
542
+ { label: "Storage", value: isRecord(storage.active) ? (storage.active as Record<string, unknown>).engine : "reported" },
543
+ { label: "Backups", value: backup.count || backup.available || "reported" },
544
+ ]} />
545
+ <div className="grid gap-3 md:grid-cols-2">
546
+ <StatusCard title="Startup" status={startup.network_exposed ? "network exposed" : "local-only"} detail={`Host ${textValue(startup.host, "127.0.0.1")} on port ${textValue(startup.port, "configured")}.`} />
547
+ <StatusCard title="Integrations" status={privacy.local_only_default === false ? "review required" : "opt-in"} detail="External integrations remain disabled until the user explicitly enables them." />
548
+ <StatusCard title="Device identity" status={textValue(identity.algorithm || identity.fingerprint, "reported")} detail={textValue(identity.storage, "Stored locally and used for signed bundle exchange.")} />
549
+ <StatusCard title="Permissions" status={permissions.destructive_restore_requires_confirmation === false ? "review required" : "guarded"} detail="Export, import, and destructive restore permissions are surfaced through admin status." />
550
+ </div>
551
+ </div>
552
+ );
553
+ }
554
+
555
+ function SecurityView({ data }: { data: Record<string, unknown> }) {
556
+ const cards = isRecord(data.cards) ? data.cards : {};
557
+ const severities = isRecord(data.severity_counts) ? data.severity_counts : {};
558
+ return (
559
+ <div className="space-y-3">
560
+ <StatGrid stats={[
561
+ { label: "Events today", value: cards.events_today || 0 },
562
+ { label: "High risk", value: cards.high_risk_events || severities.high || 0 },
563
+ { label: "Review", value: cards.review_required || 0 },
564
+ { label: "Risk rate", value: data.risk_rate || 0 },
565
+ ]} />
566
+ <StructuredView value={{ severity_counts: severities, sensitive_fields: data.field_counts || {} }} />
567
+ </div>
568
+ );
569
+ }
570
+
408
571
  function AdminPanel() {
409
572
  const summary = useQuery({ queryKey: ["adminSummary"], queryFn: latticeApi.adminSummary });
410
573
  const users = useQuery({ queryKey: ["adminUsers"], queryFn: latticeApi.adminUsers });
@@ -419,15 +582,15 @@ function AdminPanel() {
419
582
  <DataPanel title="Admin summary" result={summary.data}>{(data) => <KeyValueList data={data as Record<string, unknown>} />}</DataPanel>
420
583
  <DataPanel title="Users" result={users.data}>{(data) => <EntityList items={data} titleKey="email" metaKey="role" />}</DataPanel>
421
584
  <DataPanel title="Audit" result={audit.data}>{(data) => <EntityList items={(data as Record<string, unknown>).recent_events || data} titleKey="act" metaKey="sev" />}</DataPanel>
422
- <DataPanel title="Roles" result={roles.data}>{(data) => <JsonView value={data} />}</DataPanel>
423
- <DataPanel title="Policies" result={policies.data}>{(data) => <JsonView value={data} />}</DataPanel>
424
- <DataPanel title="Product hardening" result={hardening.data}>{(data) => <JsonView value={data} />}</DataPanel>
425
- <DataPanel title="Security overview" result={security.data}>{(data) => <JsonView value={data} />}</DataPanel>
585
+ <DataPanel title="Roles" result={roles.data}>{(data) => <EntityList items={(data as Record<string, unknown>).roles || data} titleKey="role" metaKey="members" />}</DataPanel>
586
+ <DataPanel title="Policies" result={policies.data}>{(data) => <EntityList items={(data as Record<string, unknown>).policies || data} titleKey="label" metaKey="enforced" />}</DataPanel>
587
+ <DataPanel title="Product hardening" result={hardening.data}>{(data) => <HardeningView data={data as Record<string, unknown>} />}</DataPanel>
588
+ <DataPanel title="Security overview" result={security.data}>{(data) => <SecurityView data={data as Record<string, unknown>} />}</DataPanel>
426
589
  <DataPanel title="Private VPC" result={vpc.data} className="xl:col-span-2">
427
590
  {(data) => (
428
591
  <div className="space-y-2">
429
592
  <Badge variant="muted">Community-disabled features remain honest unavailable states.</Badge>
430
- <JsonView value={data} />
593
+ <StructuredView value={data} />
431
594
  </div>
432
595
  )}
433
596
  </DataPanel>
@@ -19,7 +19,7 @@ from .storage import (
19
19
  storage_from_env,
20
20
  )
21
21
 
22
- __version__ = "4.3.1"
22
+ __version__ = "4.3.3"
23
23
 
24
24
  __all__ = [
25
25
  "AssembledContext",
@@ -1,3 +1,3 @@
1
1
  """Lattice AI - modular server package."""
2
2
 
3
- __version__ = "4.3.1"
3
+ __version__ = "4.3.3"
@@ -11,7 +11,7 @@ from copy import deepcopy
11
11
  from typing import Any, Dict, List, Optional
12
12
 
13
13
 
14
- MARKETPLACE_VERSION = "4.3.1"
14
+ MARKETPLACE_VERSION = "4.3.3"
15
15
  TEMPLATE_KINDS = ("plugin", "workflow", "agent")
16
16
 
17
17
 
@@ -14,7 +14,7 @@ from datetime import datetime
14
14
  from typing import Any, Callable, Dict, List, Optional
15
15
 
16
16
 
17
- MULTI_AGENT_VERSION = "4.3.1"
17
+ MULTI_AGENT_VERSION = "4.3.3"
18
18
 
19
19
  AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
20
20
  CORE_PIPELINE = ("planner", "executor", "reviewer")
@@ -19,7 +19,7 @@ from pathlib import Path
19
19
  from typing import Any, Callable, Dict, Iterable, List, Optional
20
20
 
21
21
 
22
- WORKSPACE_OS_VERSION = "4.3.1"
22
+ WORKSPACE_OS_VERSION = "4.3.3"
23
23
 
24
24
  # Workspace types separate single-user Personal workspaces from shared
25
25
  # Organization workspaces. Both keep the same local-first JSON store; the type
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "4.3.1",
3
+ "version": "4.3.3",
4
4
  "description": "Lattice AI — local-first Digital Brain Platform (knowledge graph, durable memory, hybrid search, agents, portable encrypted brain archives)",
5
5
  "homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
6
6
  "repository": {
@@ -25,17 +25,14 @@
25
25
  "check:python": "node scripts/run_python.mjs scripts/check_python.py",
26
26
  "lint": "node --check tests/visual/mock_server.cjs && node --check tests/visual/v3.spec.js && npm run lint:frontend",
27
27
  "lint:frontend": "node scripts/lint_frontend.mjs",
28
+ "docs:check-links": "node scripts/check_markdown_links.mjs",
28
29
  "typecheck": "npm run typecheck:frontend && cd vscode-extension && npm run build",
29
30
  "typecheck:frontend": "npx tsc -p tsconfig.json --noEmit",
30
31
  "test": "node scripts/run_python.mjs -m pytest tests/ -v",
31
32
  "test:unit": "node scripts/run_python.mjs -m pytest tests/unit/ -v",
32
33
  "test:integration": "node scripts/run_python.mjs -m pytest tests/integration/ -v",
33
34
  "test:visual": "playwright test",
34
- "capture:workspace": "node scripts/capture/capture_workspace.js",
35
- "capture:graph": "node scripts/capture/capture_graph.js",
36
- "capture:skills": "node scripts/capture/capture_skills.js",
37
- "capture:enterprise": "node scripts/capture/capture_enterprise.js",
38
- "capture:onboarding": "node scripts/capture/capture_onboarding.js",
35
+ "vercel:build": "node scripts/build_vercel_static.mjs",
39
36
  "desktop:tauri": "tauri dev",
40
37
  "desktop:tauri:build": "tauri build",
41
38
  "desktop:tauri:check": "cd src-tauri && cargo check",
@@ -0,0 +1,77 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { readFileSync } from "node:fs";
3
+
4
+ const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
5
+ const outDir = new URL("../vercel-static/", import.meta.url);
6
+
7
+ await mkdir(outDir, { recursive: true });
8
+
9
+ const html = `<!doctype html>
10
+ <html lang="en">
11
+ <head>
12
+ <meta charset="utf-8" />
13
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
14
+ <title>Lattice AI ${pkg.version}</title>
15
+ <style>
16
+ :root {
17
+ color-scheme: dark;
18
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
19
+ background: #0f141b;
20
+ color: #e5edf4;
21
+ }
22
+ body {
23
+ margin: 0;
24
+ min-height: 100vh;
25
+ display: grid;
26
+ place-items: center;
27
+ padding: 32px;
28
+ }
29
+ main {
30
+ max-width: 760px;
31
+ border: 1px solid #2d3745;
32
+ border-radius: 8px;
33
+ padding: 32px;
34
+ background: #151b24;
35
+ }
36
+ h1 {
37
+ margin: 0 0 12px;
38
+ font-size: 32px;
39
+ }
40
+ p {
41
+ line-height: 1.6;
42
+ color: #b8c4d2;
43
+ }
44
+ a {
45
+ color: #41ddd2;
46
+ }
47
+ code {
48
+ background: #0f141b;
49
+ border: 1px solid #2d3745;
50
+ border-radius: 4px;
51
+ padding: 2px 6px;
52
+ }
53
+ </style>
54
+ </head>
55
+ <body>
56
+ <main>
57
+ <h1>Lattice AI ${pkg.version}</h1>
58
+ <p>
59
+ Lattice AI is a local-first desktop Digital Brain. The product runtime is the
60
+ Tauri desktop app plus a localhost FastAPI sidecar; it is not hosted on Vercel.
61
+ </p>
62
+ <p>
63
+ This Vercel build is intentionally documentation-only so Git integration checks
64
+ do not try to deploy the desktop runtime or a fake cloud app.
65
+ </p>
66
+ <p>
67
+ Use the validated desktop/package artifacts from the GitHub release process.
68
+ Repository: <a href="${pkg.homepage}">${pkg.homepage}</a>
69
+ </p>
70
+ <p>Runtime route when installed locally: <code>http://127.0.0.1:4825/app</code></p>
71
+ </main>
72
+ </body>
73
+ </html>
74
+ `;
75
+
76
+ await writeFile(new URL("index.html", outDir), html, "utf8");
77
+ console.log(`Vercel static placeholder generated for Lattice AI ${pkg.version}`);
@@ -0,0 +1,75 @@
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ const root = process.cwd();
5
+ const readme = path.join(root, "README.md");
6
+ const checkedMarkdown = new Set();
7
+ const failures = [];
8
+
9
+ function stripAnchor(target) {
10
+ const hash = target.indexOf("#");
11
+ return hash >= 0 ? target.slice(0, hash) : target;
12
+ }
13
+
14
+ function isExternal(target) {
15
+ return /^(https?:|mailto:|tel:)/i.test(target);
16
+ }
17
+
18
+ function decodeTarget(target) {
19
+ return decodeURIComponent(target.replace(/^<|>$/g, ""));
20
+ }
21
+
22
+ function localPath(fromFile, target) {
23
+ const cleaned = stripAnchor(target).trim();
24
+ if (!cleaned || isExternal(cleaned)) return null;
25
+ return path.resolve(path.dirname(fromFile), decodeTarget(cleaned));
26
+ }
27
+
28
+ function links(markdown) {
29
+ const out = [];
30
+ const linkPattern = /!?\[[^\]]*\]\(([^)]+)\)/g;
31
+ let match;
32
+ while ((match = linkPattern.exec(markdown))) {
33
+ out.push(match[1].trim());
34
+ }
35
+ return out;
36
+ }
37
+
38
+ function checkFileLink(fromFile, target) {
39
+ const resolved = localPath(fromFile, target);
40
+ if (!resolved) return;
41
+ if (!resolved.startsWith(root)) {
42
+ failures.push(`${path.relative(root, fromFile)} links outside repo: ${target}`);
43
+ return;
44
+ }
45
+ if (!existsSync(resolved)) {
46
+ failures.push(`${path.relative(root, fromFile)} has missing link: ${target}`);
47
+ }
48
+ }
49
+
50
+ function checkMarkdownFile(file) {
51
+ if (checkedMarkdown.has(file)) return;
52
+ checkedMarkdown.add(file);
53
+ const markdown = readFileSync(file, "utf8");
54
+ for (const target of links(markdown)) {
55
+ checkFileLink(file, target);
56
+ }
57
+ }
58
+
59
+ checkMarkdownFile(readme);
60
+
61
+ for (const target of links(readFileSync(readme, "utf8"))) {
62
+ const resolved = localPath(readme, target);
63
+ if (!resolved || !existsSync(resolved)) continue;
64
+ if (statSync(resolved).isFile() && resolved.endsWith(".md")) {
65
+ checkMarkdownFile(resolved);
66
+ }
67
+ }
68
+
69
+ if (failures.length) {
70
+ console.error("Markdown link check failed:");
71
+ for (const failure of failures) console.error(`- ${failure}`);
72
+ process.exit(1);
73
+ }
74
+
75
+ console.log(`Markdown link check passed for README and ${checkedMarkdown.size - 1} README-linked Markdown files.`);