@reddb-io/ui 0.1.1

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 (169) hide show
  1. package/LICENSE +17 -0
  2. package/bin/red-ui.js +259 -0
  3. package/build/CNAME +1 -0
  4. package/build/_app/env.js +1 -0
  5. package/build/_app/immutable/assets/0.C63jY5jX.css +1 -0
  6. package/build/_app/immutable/assets/10.Dblyk0Kr.css +1 -0
  7. package/build/_app/immutable/assets/EmptyState.RXcH7ryE.css +1 -0
  8. package/build/_app/immutable/assets/Workspace.BCzG1DsH.css +1 -0
  9. package/build/_app/immutable/assets/inter-cyrillic-ext-wght-normal.BOeWTOD4.woff2 +0 -0
  10. package/build/_app/immutable/assets/inter-cyrillic-wght-normal.DqGufNeO.woff2 +0 -0
  11. package/build/_app/immutable/assets/inter-greek-ext-wght-normal.DlzME5K_.woff2 +0 -0
  12. package/build/_app/immutable/assets/inter-greek-wght-normal.CkhJZR-_.woff2 +0 -0
  13. package/build/_app/immutable/assets/inter-latin-ext-wght-normal.DO1Apj_S.woff2 +0 -0
  14. package/build/_app/immutable/assets/inter-latin-wght-normal.Dx4kXJAl.woff2 +0 -0
  15. package/build/_app/immutable/assets/inter-vietnamese-wght-normal.CBcvBZtf.woff2 +0 -0
  16. package/build/_app/immutable/assets/jetbrains-mono-cyrillic-wght-normal.D73BlboJ.woff2 +0 -0
  17. package/build/_app/immutable/assets/jetbrains-mono-greek-wght-normal.Bw9x6K1M.woff2 +0 -0
  18. package/build/_app/immutable/assets/jetbrains-mono-latin-ext-wght-normal.DBQx-q_a.woff2 +0 -0
  19. package/build/_app/immutable/assets/jetbrains-mono-latin-wght-normal.B9CIFXIH.woff2 +0 -0
  20. package/build/_app/immutable/assets/jetbrains-mono-vietnamese-wght-normal.Bt-aOZkq.woff2 +0 -0
  21. package/build/_app/immutable/chunks/3j4cZ58b.js +1 -0
  22. package/build/_app/immutable/chunks/69_IOA4Y.js +1 -0
  23. package/build/_app/immutable/chunks/ALqaDrNQ.js +2 -0
  24. package/build/_app/immutable/chunks/BGcIe2ep.js +2 -0
  25. package/build/_app/immutable/chunks/BMHKbvs8.js +1 -0
  26. package/build/_app/immutable/chunks/BQHbwf54.js +916 -0
  27. package/build/_app/immutable/chunks/Bgso8cDH.js +311 -0
  28. package/build/_app/immutable/chunks/Bmgx1mfv.js +310 -0
  29. package/build/_app/immutable/chunks/BrvvOVaJ.js +1 -0
  30. package/build/_app/immutable/chunks/Bzak7iHL.js +1 -0
  31. package/build/_app/immutable/chunks/D9CTkMjK.js +1 -0
  32. package/build/_app/immutable/chunks/DIeogL5L.js +1 -0
  33. package/build/_app/immutable/chunks/DXY_Rxa7.js +113 -0
  34. package/build/_app/immutable/chunks/DhEqZVGG.js +1 -0
  35. package/build/_app/immutable/chunks/Ov8N-vxt.js +1 -0
  36. package/build/_app/immutable/chunks/oiO4RLqp.js +1 -0
  37. package/build/_app/immutable/entry/app.D_IVtpdv.js +2 -0
  38. package/build/_app/immutable/entry/start.3oC0UQ_Q.js +1 -0
  39. package/build/_app/immutable/nodes/0.CHPiFxDo.js +1 -0
  40. package/build/_app/immutable/nodes/1.BC_4YZ0h.js +1 -0
  41. package/build/_app/immutable/nodes/10.B7DMKSNc.js +60 -0
  42. package/build/_app/immutable/nodes/2.CAG5_cGP.js +1 -0
  43. package/build/_app/immutable/nodes/3.CAG5_cGP.js +1 -0
  44. package/build/_app/immutable/nodes/4.CAG5_cGP.js +1 -0
  45. package/build/_app/immutable/nodes/5.CAG5_cGP.js +1 -0
  46. package/build/_app/immutable/nodes/6.CAG5_cGP.js +1 -0
  47. package/build/_app/immutable/nodes/7.BZX9Iird.js +134 -0
  48. package/build/_app/immutable/nodes/8.CAG5_cGP.js +1 -0
  49. package/build/_app/immutable/nodes/9.CAG5_cGP.js +1 -0
  50. package/build/_app/version.json +1 -0
  51. package/build/favicon.png +0 -0
  52. package/build/index.html +59 -0
  53. package/dist/embed/RedUiEmbedRoot-BGoLxJf1.js +26562 -0
  54. package/dist/embed/RedUiEmbedRoot-BGoLxJf1.js.map +1 -0
  55. package/dist/embed/bundle-mjs-UuVRz3O4.js +3047 -0
  56. package/dist/embed/bundle-mjs-UuVRz3O4.js.map +1 -0
  57. package/dist/embed/core-QuRYvMt8.js +118 -0
  58. package/dist/embed/core-QuRYvMt8.js.map +1 -0
  59. package/dist/embed/index-B6ZQhCsu.js +577 -0
  60. package/dist/embed/index-B6ZQhCsu.js.map +1 -0
  61. package/dist/embed/index-client-ItCEU64C.js +3087 -0
  62. package/dist/embed/index-client-ItCEU64C.js.map +1 -0
  63. package/dist/embed/index-fad77a13.esm-BqPj0saH.js +1318 -0
  64. package/dist/embed/index-fad77a13.esm-BqPj0saH.js.map +1 -0
  65. package/dist/embed/index.js +11 -0
  66. package/dist/embed/index.js.map +1 -0
  67. package/dist/embed/sigma-node-border.esm-CGtlgqmo.js +432 -0
  68. package/dist/embed/sigma-node-border.esm-CGtlgqmo.js.map +1 -0
  69. package/dist/embed/sigma.esm-y0qgY-OC.js +2355 -0
  70. package/dist/embed/sigma.esm-y0qgY-OC.js.map +1 -0
  71. package/dist/embed/ui.css +1 -0
  72. package/package.json +101 -0
  73. package/src/app.css +238 -0
  74. package/src/lib/ActivityIndicator.svelte +100 -0
  75. package/src/lib/ClusterNode.svelte +119 -0
  76. package/src/lib/ClusterView.svelte +541 -0
  77. package/src/lib/CollectionHistory.svelte +266 -0
  78. package/src/lib/CollectionsView.svelte +14 -0
  79. package/src/lib/CommandPalette.svelte +237 -0
  80. package/src/lib/ConnectDropdown.svelte +222 -0
  81. package/src/lib/EmptyState.svelte +27 -0
  82. package/src/lib/LiveChanges.svelte +195 -0
  83. package/src/lib/MasterPasswordDialog.svelte +168 -0
  84. package/src/lib/PageHeader.svelte +32 -0
  85. package/src/lib/PendingChangesPanel.svelte +153 -0
  86. package/src/lib/QueryEditor.svelte +122 -0
  87. package/src/lib/ResultsPane.svelte +747 -0
  88. package/src/lib/SchemaTree.svelte +193 -0
  89. package/src/lib/SecurityView.svelte +255 -0
  90. package/src/lib/ShortcutOverlay.svelte +104 -0
  91. package/src/lib/Splash.svelte +46 -0
  92. package/src/lib/StatusBar.svelte +163 -0
  93. package/src/lib/SubNav.svelte +62 -0
  94. package/src/lib/Topbar.svelte +119 -0
  95. package/src/lib/Workspace.svelte +247 -0
  96. package/src/lib/activity.svelte.ts +91 -0
  97. package/src/lib/build-info.ts +41 -0
  98. package/src/lib/capability.ts +221 -0
  99. package/src/lib/cn.ts +6 -0
  100. package/src/lib/collection-catalog.ts +116 -0
  101. package/src/lib/collection-pages.ts +33 -0
  102. package/src/lib/connection-history.ts +301 -0
  103. package/src/lib/connections.svelte.ts +493 -0
  104. package/src/lib/embed/RedUiEmbedRoot.svelte +31 -0
  105. package/src/lib/embed/index.ts +234 -0
  106. package/src/lib/graph-loader/contract.ts +336 -0
  107. package/src/lib/graph-loader/drilldown.ts +334 -0
  108. package/src/lib/graph-loader/index.ts +13 -0
  109. package/src/lib/index.ts +114 -0
  110. package/src/lib/pending-changes.svelte.ts +149 -0
  111. package/src/lib/query-tabs.svelte.ts +130 -0
  112. package/src/lib/reddb/boot-params.ts +254 -0
  113. package/src/lib/reddb/cdc-stream-client.ts +234 -0
  114. package/src/lib/reddb/client.ts +698 -0
  115. package/src/lib/reddb/connection-bootstrap.ts +260 -0
  116. package/src/lib/reddb/connection-provider.contract.ts +72 -0
  117. package/src/lib/reddb/connection-provider.ts +72 -0
  118. package/src/lib/reddb/index.ts +14 -0
  119. package/src/lib/reddb/injected-client-provider.ts +76 -0
  120. package/src/lib/reddb/local-url-provider.ts +245 -0
  121. package/src/lib/reddb/secure-store.ts +41 -0
  122. package/src/lib/reddb/server-capabilities.ts +71 -0
  123. package/src/lib/reddb/tauri-encrypted-store.ts +64 -0
  124. package/src/lib/reddb/transports.ts +89 -0
  125. package/src/lib/reddb/types.ts +32 -0
  126. package/src/lib/reddb/uri.ts +54 -0
  127. package/src/lib/reddb/web-encrypted-store.ts +201 -0
  128. package/src/lib/renderers/DiffRenderer.svelte +184 -0
  129. package/src/lib/renderers/DocumentRenderer.svelte +134 -0
  130. package/src/lib/renderers/GraphRenderer.svelte +1328 -0
  131. package/src/lib/renderers/HypertableRenderer.svelte +253 -0
  132. package/src/lib/renderers/JsonRenderer.svelte +30 -0
  133. package/src/lib/renderers/KvRenderer.svelte +150 -0
  134. package/src/lib/renderers/QueueRenderer.svelte +286 -0
  135. package/src/lib/renderers/StatsRenderer.svelte +123 -0
  136. package/src/lib/renderers/TableRenderer.svelte +440 -0
  137. package/src/lib/renderers/VectorRenderer.svelte +388 -0
  138. package/src/lib/renderers/diff-render.ts +122 -0
  139. package/src/lib/renderers/diff.ts +9 -0
  140. package/src/lib/renderers/document-render.ts +64 -0
  141. package/src/lib/renderers/document.ts +9 -0
  142. package/src/lib/renderers/graph-render.ts +631 -0
  143. package/src/lib/renderers/graph-sigma.ts +395 -0
  144. package/src/lib/renderers/graph.ts +9 -0
  145. package/src/lib/renderers/hypertable-render.ts +320 -0
  146. package/src/lib/renderers/hypertable.ts +9 -0
  147. package/src/lib/renderers/index.ts +42 -0
  148. package/src/lib/renderers/json-render.ts +89 -0
  149. package/src/lib/renderers/json.ts +10 -0
  150. package/src/lib/renderers/kv-render.ts +193 -0
  151. package/src/lib/renderers/kv.ts +9 -0
  152. package/src/lib/renderers/queue-render.ts +197 -0
  153. package/src/lib/renderers/queue.ts +9 -0
  154. package/src/lib/renderers/registry.ts +68 -0
  155. package/src/lib/renderers/stats-render.ts +73 -0
  156. package/src/lib/renderers/stats.ts +9 -0
  157. package/src/lib/renderers/table-render.ts +156 -0
  158. package/src/lib/renderers/table.ts +11 -0
  159. package/src/lib/renderers/types.ts +33 -0
  160. package/src/lib/renderers/vector-render.ts +154 -0
  161. package/src/lib/renderers/vector.ts +9 -0
  162. package/src/lib/router.svelte.ts +163 -0
  163. package/src/lib/secureStore.svelte.ts +148 -0
  164. package/src/lib/shadow-dom-spike/ShadowDomSpikeCore.svelte +198 -0
  165. package/src/lib/shadow-dom-spike/mount-shadow-dom-spike.ts +94 -0
  166. package/src/lib/shortcuts.ts +22 -0
  167. package/src/lib/surface.ts +80 -0
  168. package/src/lib/tabs.svelte.ts +115 -0
  169. package/src/lib/theme.svelte.ts +63 -0
@@ -0,0 +1,234 @@
1
+ import appCss from "../../app.css?inline";
2
+ import type { Component } from "svelte";
3
+ import type { ConnectionProvider } from "../reddb";
4
+ import type { Theme } from "../theme.svelte";
5
+ import { RED_UI_VERSION, readBuildInfo } from "../build-info";
6
+
7
+ /** Build version of the embeddable bundle, stamped at build time. */
8
+ export { RED_UI_VERSION, readBuildInfo };
9
+
10
+ declare global {
11
+ interface Window {
12
+ __RED_UI_EMBED_SURFACE__?: boolean;
13
+ }
14
+ }
15
+
16
+ export {
17
+ InjectedClientProvider,
18
+ RedClient,
19
+ type Connection,
20
+ type ConnectionProvider,
21
+ type InjectedClientOptions,
22
+ type RedClientOptions,
23
+ } from "../reddb";
24
+
25
+ export interface RedUiEmbedOptions {
26
+ connectionProvider: ConnectionProvider;
27
+ initialRoute?: string;
28
+ theme?: Theme;
29
+ shadowRoot?: ShadowRootInit;
30
+ }
31
+
32
+ export interface RedUiEmbedHandle {
33
+ host: HTMLElement;
34
+ shadowRoot: ShadowRoot;
35
+ portalTarget: HTMLElement;
36
+ destroy(): void;
37
+ }
38
+
39
+ export type RedUiElementTagName = `${string}-${string}`;
40
+
41
+ const shadowCss = `
42
+ :host {
43
+ display: block;
44
+ min-height: 720px;
45
+ color-scheme: light dark;
46
+ contain: content;
47
+ font-family: var(--font-sans);
48
+ }
49
+
50
+ :host,
51
+ :host([data-theme='light']) {
52
+ --color-bg-0: #ffffff;
53
+ --color-bg-1: #fafafa;
54
+ --color-bg-2: #f4f4f5;
55
+ --color-bg-3: #e7e7ea;
56
+ --color-fg-0: #0a0a0b;
57
+ --color-fg-1: #3f3f46;
58
+ --color-fg-2: #52525b;
59
+ --color-fg-3: #71717a;
60
+ --color-line-1: #ececef;
61
+ --color-line-2: #d4d4d8;
62
+ --color-line-3: #a1a1aa;
63
+ --color-accent: #e11d48;
64
+ --color-accent-soft: rgba(225, 29, 72, 0.10);
65
+ --color-accent-glow: rgba(225, 29, 72, 0.30);
66
+ --color-ok: #16a34a;
67
+ --color-warn: #b45309;
68
+ --color-danger: #dc2626;
69
+ --color-info: #2563eb;
70
+ --color-role-primary: #e11d48;
71
+ --color-role-replica: #2563eb;
72
+ --color-role-embedded: #16a34a;
73
+ --color-ambient-glow: rgba(225, 29, 72, 0.04);
74
+ --shadow-md: 0 4px 12px rgba(15, 23, 42, 0.08), 0 0 0 1px var(--color-line-2);
75
+ --shadow-lg: 0 16px 48px rgba(15, 23, 42, 0.14), 0 0 0 1px var(--color-line-2);
76
+ }
77
+
78
+ :host([data-theme='dark']) {
79
+ --color-bg-0: #050607;
80
+ --color-bg-1: #0d0f13;
81
+ --color-bg-2: #181b21;
82
+ --color-bg-3: #232730;
83
+ --color-fg-0: #f7f8fa;
84
+ --color-fg-1: #c8ccd4;
85
+ --color-fg-2: #9ca1a9;
86
+ --color-fg-3: #7a7f87;
87
+ --color-line-1: #181b21;
88
+ --color-line-2: #2a2e36;
89
+ --color-line-3: #3d424b;
90
+ --color-accent: #ff2056;
91
+ --color-accent-soft: rgba(255, 32, 86, 0.12);
92
+ --color-accent-glow: rgba(255, 32, 86, 0.45);
93
+ --color-ok: #4ade80;
94
+ --color-warn: #fbbf24;
95
+ --color-danger: #ff5470;
96
+ --color-info: #60a5fa;
97
+ --color-role-primary: #ff2056;
98
+ --color-role-replica: #60a5fa;
99
+ --color-role-embedded: #4ade80;
100
+ --color-ambient-glow: rgba(255, 32, 86, 0.06);
101
+ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4), 0 0 0 1px var(--color-line-2);
102
+ --shadow-lg: 0 16px 48px rgba(0, 0, 0, 0.6), 0 0 0 1px var(--color-line-2);
103
+ }
104
+
105
+ :host * {
106
+ box-sizing: border-box;
107
+ }
108
+
109
+ .red-ui-shadow-mount {
110
+ min-height: 720px;
111
+ background: var(--color-bg-0);
112
+ color: var(--color-fg-0);
113
+ font-family: var(--font-sans);
114
+ font-size: 13px;
115
+ line-height: 1.5;
116
+ -webkit-font-smoothing: antialiased;
117
+ text-rendering: optimizeLegibility;
118
+ }
119
+
120
+ [data-red-ui-shadow-portal] {
121
+ position: relative;
122
+ z-index: 2147483647;
123
+ }
124
+ `;
125
+
126
+ function markEmbedSurface() {
127
+ if (typeof window !== "undefined") window.__RED_UI_EMBED_SURFACE__ = true;
128
+ }
129
+
130
+ export async function mountRedUi(
131
+ host: HTMLElement,
132
+ opts: RedUiEmbedOptions
133
+ ): Promise<RedUiEmbedHandle> {
134
+ markEmbedSurface();
135
+ host.dataset.theme = opts.theme ?? host.dataset.theme ?? "dark";
136
+
137
+ const shadowRoot =
138
+ host.shadowRoot ?? host.attachShadow(opts.shadowRoot ?? { mode: "open" });
139
+ shadowRoot.textContent = "";
140
+
141
+ const style = document.createElement("style");
142
+ style.textContent = `${appCss}\n${shadowCss}`;
143
+
144
+ const mountTarget = document.createElement("div");
145
+ mountTarget.className = "red-ui-shadow-mount";
146
+
147
+ const portalTarget = document.createElement("div");
148
+ portalTarget.setAttribute("data-red-ui-shadow-portal", "");
149
+
150
+ shadowRoot.append(style, mountTarget, portalTarget);
151
+
152
+ const [{ mount, unmount }, root] = await Promise.all([
153
+ import("svelte"),
154
+ import("./RedUiEmbedRoot.svelte") as Promise<{
155
+ default: Component<Record<string, unknown>>;
156
+ }>,
157
+ ]);
158
+
159
+ const instance = mount(root.default, {
160
+ target: mountTarget,
161
+ props: {
162
+ connectionProvider: opts.connectionProvider,
163
+ portalTarget,
164
+ shadowHost: host,
165
+ initialRoute: opts.initialRoute,
166
+ initialTheme: opts.theme,
167
+ },
168
+ });
169
+
170
+ return {
171
+ host,
172
+ shadowRoot,
173
+ portalTarget,
174
+ destroy: () => {
175
+ void unmount(instance);
176
+ shadowRoot.textContent = "";
177
+ },
178
+ };
179
+ }
180
+
181
+ export class RedUiElement extends HTMLElement {
182
+ #connectionProvider: ConnectionProvider | null = null;
183
+ initialRoute: string | undefined;
184
+ theme: Theme | undefined;
185
+ #handle: RedUiEmbedHandle | null = null;
186
+ #mounting: Promise<void> | null = null;
187
+
188
+ /** Build version of the embedded bundle (also reflected as a `version`
189
+ * attribute on connect, so a host can read `el.getAttribute('version')`). */
190
+ static readonly version = RED_UI_VERSION;
191
+ get version(): string {
192
+ return RED_UI_VERSION;
193
+ }
194
+
195
+ get connectionProvider(): ConnectionProvider | null {
196
+ return this.#connectionProvider;
197
+ }
198
+
199
+ set connectionProvider(provider: ConnectionProvider | null) {
200
+ this.#connectionProvider = provider;
201
+ if (this.isConnected) this.#mounting = this.#mount();
202
+ }
203
+
204
+ connectedCallback() {
205
+ if (!this.hasAttribute("version"))
206
+ this.setAttribute("version", RED_UI_VERSION);
207
+ this.#mounting ??= this.#mount();
208
+ }
209
+
210
+ disconnectedCallback() {
211
+ this.#handle?.destroy();
212
+ this.#handle = null;
213
+ this.#mounting = null;
214
+ }
215
+
216
+ async #mount() {
217
+ if (!this.#connectionProvider) return;
218
+ this.#handle?.destroy();
219
+ this.#handle = await mountRedUi(this, {
220
+ connectionProvider: this.#connectionProvider,
221
+ initialRoute: this.initialRoute,
222
+ theme: this.theme,
223
+ });
224
+ }
225
+ }
226
+
227
+ export function registerRedUiElement(
228
+ tagName: RedUiElementTagName = "red-ui-app"
229
+ ): typeof RedUiElement {
230
+ markEmbedSurface();
231
+ if (!customElements.get(tagName))
232
+ customElements.define(tagName, RedUiElement);
233
+ return customElements.get(tagName) as typeof RedUiElement;
234
+ }
@@ -0,0 +1,336 @@
1
+ // Universal graph.json loader.
2
+ //
3
+ // Parses an arbitrary `graph.json` that conforms to the cross-repo export
4
+ // contract (reddb-io/red-skills#234 — the shape emitted by the memory
5
+ // `export` skill, graphify, and codebase-graph tooling) and adapts it into
6
+ // the *same* `QueryResult` shape the existing `GraphRenderer` already
7
+ // consumes. We deliberately reuse `extractGraph`/`GraphRenderer` rather than
8
+ // building a parallel renderer: the loader is purely a data path that hands
9
+ // the canvas a graph-shaped result, so community coloring, force layout,
10
+ // search, and centrality all keep working unchanged and offline.
11
+ //
12
+ // Server-backed affordances inside the renderer (GRAPH NEIGHBORHOOD /
13
+ // SHORTEST_PATH / CENTRALITY) issue live queries and are inert without a
14
+ // connection — expected for a static file.
15
+
16
+ import type { QueryResult } from "#reddb";
17
+ import { colorForCommunity } from "$lib/renderers/graph-render";
18
+
19
+ /** Major version of the graph.json contract this loader understands. */
20
+ export const SUPPORTED_GRAPH_VERSION_MAJOR = 1;
21
+
22
+ /** A normalized node in the loaded model. */
23
+ export interface LoadedGraphNode {
24
+ id: string;
25
+ label: string;
26
+ type?: string;
27
+ community?: number;
28
+ /** Full original node object, preserved for the renderer's detail panel. */
29
+ data: Record<string, unknown>;
30
+ }
31
+
32
+ /** A normalized edge in the loaded model. */
33
+ export interface LoadedGraphEdge {
34
+ id: string;
35
+ source: string;
36
+ target: string;
37
+ type?: string;
38
+ /** Full original edge object. */
39
+ data: Record<string, unknown>;
40
+ }
41
+
42
+ /** A normalized community descriptor. */
43
+ export interface LoadedGraphCommunity {
44
+ id: number;
45
+ label: string;
46
+ color: string;
47
+ size: number;
48
+ }
49
+
50
+ /** The result of loading a contract-conformant graph.json. */
51
+ export interface LoadedGraph {
52
+ /** Declared contract version string. */
53
+ version: string;
54
+ /** Normalized node/edge/community model (what the unit tests assert on). */
55
+ model: {
56
+ nodes: LoadedGraphNode[];
57
+ edges: LoadedGraphEdge[];
58
+ communities: LoadedGraphCommunity[];
59
+ };
60
+ /** A graph-shaped QueryResult ready to hand straight to `GraphRenderer`. */
61
+ result: QueryResult;
62
+ /** Free-form metadata block carried by the export, if any. */
63
+ meta: Record<string, unknown>;
64
+ }
65
+
66
+ /** Raised when an export is structurally malformed or the wrong version. */
67
+ export class GraphContractError extends Error {
68
+ constructor(message: string) {
69
+ super(message);
70
+ this.name = "GraphContractError";
71
+ }
72
+ }
73
+
74
+ function isObject(value: unknown): value is Record<string, unknown> {
75
+ return typeof value === "object" && value !== null && !Array.isArray(value);
76
+ }
77
+
78
+ function firstDefined(obj: Record<string, unknown>, keys: string[]): unknown {
79
+ for (const k of keys) {
80
+ const v = obj[k];
81
+ if (v !== undefined && v !== null && v !== "") return v;
82
+ }
83
+ return undefined;
84
+ }
85
+
86
+ function parseVersion(raw: Record<string, unknown>): string {
87
+ const v = firstDefined(raw, ["version", "schema_version", "schemaVersion"]);
88
+ if (typeof v !== "string" || v.trim() === "") {
89
+ throw new GraphContractError(
90
+ 'Graph export is missing a "version" string. ' +
91
+ `This viewer supports the version ${SUPPORTED_GRAPH_VERSION_MAJOR}.x contract.`
92
+ );
93
+ }
94
+ const major = Number.parseInt(v.split(".")[0] ?? "", 10);
95
+ if (!Number.isFinite(major)) {
96
+ throw new GraphContractError(
97
+ `Graph export declares an unparseable version "${v}". ` +
98
+ `This viewer supports version ${SUPPORTED_GRAPH_VERSION_MAJOR}.x.`
99
+ );
100
+ }
101
+ if (major !== SUPPORTED_GRAPH_VERSION_MAJOR) {
102
+ throw new GraphContractError(
103
+ `Unsupported graph export version "${v}". ` +
104
+ `This viewer supports version ${SUPPORTED_GRAPH_VERSION_MAJOR}.x.`
105
+ );
106
+ }
107
+ return v;
108
+ }
109
+
110
+ function normalizeNode(raw: unknown, index: number): LoadedGraphNode {
111
+ if (!isObject(raw)) {
112
+ throw new GraphContractError(
113
+ `Graph node at index ${index} is not an object.`
114
+ );
115
+ }
116
+ const rawId = firstDefined(raw, ["id", "rid", "_id", "uid"]);
117
+ if (rawId === undefined) {
118
+ throw new GraphContractError(
119
+ `Graph node at index ${index} is missing the required "id" field.`
120
+ );
121
+ }
122
+ const id = String(rawId);
123
+ const label = firstDefined(raw, ["label", "name", "title"]);
124
+ const type = firstDefined(raw, ["type", "node_type", "kind"]);
125
+ const community = raw.community;
126
+ return {
127
+ id,
128
+ label: label !== undefined ? String(label) : id,
129
+ type: type !== undefined ? String(type) : undefined,
130
+ community: typeof community === "number" ? community : undefined,
131
+ data: raw,
132
+ };
133
+ }
134
+
135
+ function normalizeEdge(raw: unknown, index: number): LoadedGraphEdge {
136
+ if (!isObject(raw)) {
137
+ throw new GraphContractError(
138
+ `Graph edge at index ${index} is not an object.`
139
+ );
140
+ }
141
+ const source = firstDefined(raw, [
142
+ "source",
143
+ "from",
144
+ "src",
145
+ "start",
146
+ "from_rid",
147
+ ]);
148
+ if (source === undefined) {
149
+ throw new GraphContractError(
150
+ `Graph edge at index ${index} is missing the required "source" field.`
151
+ );
152
+ }
153
+ const target = firstDefined(raw, ["target", "to", "dst", "end", "to_rid"]);
154
+ if (target === undefined) {
155
+ throw new GraphContractError(
156
+ `Graph edge at index ${index} is missing the required "target" field.`
157
+ );
158
+ }
159
+ const type = firstDefined(raw, ["kind", "type", "label", "rel"]);
160
+ const s = String(source);
161
+ const t = String(target);
162
+ const rawId = firstDefined(raw, ["id", "rid"]);
163
+ const id =
164
+ rawId !== undefined ? String(rawId) : `${s}->${t}:${type ?? ""}#${index}`;
165
+ return {
166
+ id,
167
+ source: s,
168
+ target: t,
169
+ type: type !== undefined ? String(type) : undefined,
170
+ data: raw,
171
+ };
172
+ }
173
+
174
+ function normalizeCommunities(raw: unknown): LoadedGraphCommunity[] {
175
+ if (raw === undefined || raw === null) return [];
176
+ if (!Array.isArray(raw)) {
177
+ throw new GraphContractError(
178
+ 'Graph export "communities" must be an array when present.'
179
+ );
180
+ }
181
+ return raw.map((entry, index) => {
182
+ if (!isObject(entry)) {
183
+ throw new GraphContractError(
184
+ `Community at index ${index} is not an object.`
185
+ );
186
+ }
187
+ if (typeof entry.id !== "number") {
188
+ throw new GraphContractError(
189
+ `Community at index ${index} is missing a numeric "id".`
190
+ );
191
+ }
192
+ const id = entry.id;
193
+ return {
194
+ id,
195
+ label: typeof entry.label === "string" ? entry.label : `Community ${id}`,
196
+ color:
197
+ typeof entry.color === "string" ? entry.color : colorForCommunity(id),
198
+ size: typeof entry.size === "number" ? entry.size : 0,
199
+ };
200
+ });
201
+ }
202
+
203
+ /**
204
+ * Build a graph-shaped `QueryResult` from the normalized model. All nodes and
205
+ * edges go into a single record's `nodes`/`edges` maps (keyed by id) — exactly
206
+ * the "Shape A" that `extractGraph`/`hasGraphShape` recognize, so the existing
207
+ * `GraphRenderer` paints it without modification.
208
+ */
209
+ function toQueryResult(
210
+ nodes: LoadedGraphNode[],
211
+ edges: LoadedGraphEdge[]
212
+ ): QueryResult {
213
+ const nodeMap: Record<string, Record<string, unknown>> = {};
214
+ for (const n of nodes) {
215
+ nodeMap[n.id] = {
216
+ ...n.data,
217
+ id: n.id,
218
+ label: n.label,
219
+ ...(n.type !== undefined ? { node_type: n.type } : {}),
220
+ };
221
+ }
222
+ const edgeMap: Record<string, Record<string, unknown>> = {};
223
+ for (const e of edges) {
224
+ edgeMap[e.id] = {
225
+ ...e.data,
226
+ source: e.source,
227
+ target: e.target,
228
+ ...(e.type !== undefined ? { type: e.type } : {}),
229
+ };
230
+ }
231
+ return {
232
+ ok: true,
233
+ query: "",
234
+ capability: "graph",
235
+ record_count: nodes.length + edges.length,
236
+ result: {
237
+ columns: ["nodes", "edges"],
238
+ records: [{ values: {}, nodes: nodeMap, edges: edgeMap }],
239
+ },
240
+ } as QueryResult;
241
+ }
242
+
243
+ /**
244
+ * Validate and normalize a parsed graph.json value.
245
+ *
246
+ * Accepts both the nested envelope (`{ version, graph: { nodes, edges } }`) and
247
+ * the flat one (`{ version, nodes, edges }`), since "universal" means liberal
248
+ * in what we accept. Throws a {@link GraphContractError} with an actionable
249
+ * message when the value is not an object, declares an unsupported version, or
250
+ * is missing required node/edge fields.
251
+ */
252
+ export function parseGraphExport(raw: unknown): LoadedGraph {
253
+ if (!isObject(raw)) {
254
+ throw new GraphContractError(
255
+ 'Graph export must be a JSON object with "version" and graph data.'
256
+ );
257
+ }
258
+
259
+ const contract = isObject(raw.contract) ? raw.contract : null;
260
+ const version = parseVersion(contract ?? raw);
261
+
262
+ const container = contract ?? (isObject(raw.graph) ? raw.graph : raw);
263
+ const nodesRaw = container.nodes;
264
+ const edgesRaw = container.edges;
265
+ if (!Array.isArray(nodesRaw)) {
266
+ throw new GraphContractError('Graph export "nodes" must be an array.');
267
+ }
268
+ if (!Array.isArray(edgesRaw)) {
269
+ throw new GraphContractError('Graph export "edges" must be an array.');
270
+ }
271
+
272
+ const nodes = nodesRaw.map(normalizeNode);
273
+ const edges = edgesRaw.map(normalizeEdge);
274
+ const communities = normalizeCommunities(
275
+ raw.communities ?? container.communities
276
+ );
277
+
278
+ return {
279
+ version,
280
+ model: { nodes, edges, communities },
281
+ result: toQueryResult(nodes, edges),
282
+ meta: isObject(raw.meta) ? raw.meta : {},
283
+ };
284
+ }
285
+
286
+ /**
287
+ * Parse a raw JSON string into a {@link LoadedGraph}. Wraps
288
+ * {@link parseGraphExport}, converting JSON syntax errors into a
289
+ * {@link GraphContractError} so callers handle a single error type.
290
+ */
291
+ export function parseGraphExportJson(text: string): LoadedGraph {
292
+ let value: unknown;
293
+ try {
294
+ value = JSON.parse(text);
295
+ } catch (e) {
296
+ const detail = e instanceof Error ? e.message : String(e);
297
+ throw new GraphContractError(`Graph export is not valid JSON: ${detail}`);
298
+ }
299
+ return parseGraphExport(value);
300
+ }
301
+
302
+ /**
303
+ * Fetch a graph.json from a path or URL and normalize it. `src` may be an
304
+ * absolute URL (`https://…/graph.json`) or a same-origin path
305
+ * (`/exports/memory.json`). Inject `opts.fetch` for tests.
306
+ */
307
+ export async function loadGraphExport(
308
+ src: string,
309
+ opts: { fetch?: typeof fetch } = {}
310
+ ): Promise<LoadedGraph> {
311
+ const fetchFn = opts.fetch ?? globalThis.fetch;
312
+ if (typeof fetchFn !== "function") {
313
+ throw new GraphContractError(
314
+ "No fetch implementation available to load the graph."
315
+ );
316
+ }
317
+
318
+ let res: Response;
319
+ try {
320
+ res = await fetchFn(src);
321
+ } catch (e) {
322
+ const detail = e instanceof Error ? e.message : String(e);
323
+ throw new GraphContractError(
324
+ `Failed to fetch graph from "${src}": ${detail}`
325
+ );
326
+ }
327
+
328
+ if (!res.ok) {
329
+ throw new GraphContractError(
330
+ `Failed to load graph from "${src}": HTTP ${res.status} ${res.statusText}`.trim()
331
+ );
332
+ }
333
+
334
+ const text = await res.text();
335
+ return parseGraphExportJson(text);
336
+ }