diff --git a/.claude/ralph-loop.local.md b/.claude/ralph-loop.local.md
new file mode 100644
index 00000000..fa15fd74
--- /dev/null
+++ b/.claude/ralph-loop.local.md
@@ -0,0 +1,18 @@
+---
+active: false
+iteration: 3
+session_id: qa-fix-loop-20260412
+max_iterations: 20
+completion_promise: "ALL_CLEAR"
+started_at: "2026-04-12T08:00:00Z"
+completed_at: "2026-04-12T08:20:00Z"
+---
+
+ALL_CLEAR — All three chat modes (Graph RAG, Doc RAG, Agent) return substantive answers with grounded data. Agent mode now forwards explainability graph from graph-rag pipeline. No stuck spinners. No console errors.
+
+Fixes applied:
+1. Graph-rag service: send answer + explain data in single message (agent was getting empty explain event as first response)
+2. Doc RAG pipeline: fixed types, added content to Qdrant payload, seeded 10 document chunks
+3. Agent service: forward explain events from KnowledgeQuery tool calls
+4. Client: handle explain events embedded in answer message (Graph RAG) and as separate chunks (Agent)
+5. Gateway: added "agent" to TERM_BEARING_RESPONSE_SERVICES for triple format translation
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 00000000..9a897b64
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Ignored default folder with query files
+/queries/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/GitLink.xml b/.idea/GitLink.xml
new file mode 100644
index 00000000..009597cc
--- /dev/null
+++ b/.idea/GitLink.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 00000000..6b327b41
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/discord.xml b/.idea/discord.xml
new file mode 100644
index 00000000..30bab2ab
--- /dev/null
+++ b/.idea/discord.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 00000000..379858e1
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,130 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 00000000..babc8cb3
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/trustgraph.iml b/.idea/trustgraph.iml
new file mode 100644
index 00000000..c956989b
--- /dev/null
+++ b/.idea/trustgraph.iml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 00000000..35eb1ddf
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.playwright-mcp/console-2026-04-12T05-27-22-237Z.log b/.playwright-mcp/console-2026-04-12T05-27-22-237Z.log
new file mode 100644
index 00000000..df833967
--- /dev/null
+++ b/.playwright-mcp/console-2026-04-12T05-27-22-237Z.log
@@ -0,0 +1,18 @@
+[ 188ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=714749ff:20102
+[ 232ms] [LOG] SOCKET: opening socket... without auth user: user @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:40
+[ 232ms] [LOG] SOCKET: connecting to /api/socket @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:135
+[ 232ms] [LOG] SOCKET: socket opened @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:42
+[ 234ms] [WARNING] WebSocket connection to 'ws://localhost:5173/api/socket' failed: WebSocket is closed before the connection is established. @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:270
+[ 234ms] [LOG] SOCKET: opening socket... without auth user: user @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:40
+[ 235ms] [LOG] SOCKET: connecting to /api/socket @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:135
+[ 235ms] [LOG] SOCKET: socket opened @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:42
+[ 370ms] [ERROR] WebSocket connection to 'ws://localhost:5173/api/socket' failed: Connection closed before receiving a handshake response @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:137
+[ 379ms] [ERROR] [socket error] Event @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:202
+[ 392ms] [LOG] [socket close] 1006 @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:176
+[ 392ms] [LOG] [socket] Reconnecting in 2304.210165117683ms (attempt 1/10) @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:235
+[ 2696ms] [LOG] [socket reopen] @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:245
+[ 2697ms] [LOG] SOCKET: connecting to /api/socket @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:135
+[ 2698ms] [ERROR] WebSocket connection to 'ws://localhost:5173/api/socket' failed: Connection closed before receiving a handshake response @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:137
+[ 2698ms] [ERROR] [socket error] Event @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:202
+[ 2711ms] [LOG] [socket close] 1006 @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:176
+[ 2711ms] [LOG] [socket] Reconnection already in progress, skipping @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:212
diff --git a/.playwright-mcp/console-2026-04-12T05-27-32-160Z.log b/.playwright-mcp/console-2026-04-12T05-27-32-160Z.log
new file mode 100644
index 00000000..62cd3567
--- /dev/null
+++ b/.playwright-mcp/console-2026-04-12T05-27-32-160Z.log
@@ -0,0 +1,32 @@
+[ 80ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=714749ff:20102
+[ 128ms] [LOG] SOCKET: opening socket... without auth user: user @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:40
+[ 128ms] [LOG] SOCKET: connecting to /api/socket @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:135
+[ 128ms] [LOG] SOCKET: socket opened @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:42
+[ 129ms] [WARNING] WebSocket connection to 'ws://localhost:5173/api/socket' failed: WebSocket is closed before the connection is established. @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:270
+[ 129ms] [LOG] SOCKET: opening socket... without auth user: user @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:40
+[ 129ms] [LOG] SOCKET: connecting to /api/socket @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:135
+[ 129ms] [LOG] SOCKET: socket opened @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:42
+[ 184ms] [LOG] Request 3vyu2yhjsasom22g-1 waiting for socket reconnection... @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/service-call.js:174
+[ 185ms] [LOG] Request 3vyu2yhjsasom22g-2 waiting for socket reconnection... @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/service-call.js:174
+[ 223ms] [ERROR] WebSocket connection to 'ws://localhost:5173/api/socket' failed: Connection closed before receiving a handshake response @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:137
+[ 223ms] [ERROR] [socket error] Event @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:202
+[ 245ms] [LOG] [socket close] 1006 @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:176
+[ 245ms] [LOG] [socket] Reconnecting in 2122.8106630994694ms (attempt 1/10) @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:235
+[ 266ms] [VERBOSE] [DOM] Password field is not contained in a form: (More info: https://goo.gl/9p2vKq) %o @ http://localhost:5173/settings:0
+[ 2368ms] [LOG] [socket reopen] @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:245
+[ 2368ms] [LOG] SOCKET: connecting to /api/socket @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:135
+[ 2373ms] [ERROR] WebSocket connection to 'ws://localhost:5173/api/socket' failed: Connection closed before receiving a handshake response @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:137
+[ 2373ms] [ERROR] [socket error] Event @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:202
+[ 2391ms] [LOG] [socket close] 1006 @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:176
+[ 2391ms] [LOG] [socket] Reconnection already in progress, skipping @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/trustgraph-socket.js:212
+[ 4734ms] [LOG] Request 3vyu2yhjsasom22g-2 waiting for socket reconnection... @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/service-call.js:174
+[ 5123ms] [LOG] Request 3vyu2yhjsasom22g-1 waiting for socket reconnection... @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/service-call.js:174
+[ 12903ms] [LOG] Request 3vyu2yhjsasom22g-2 waiting for socket reconnection... @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/service-call.js:174
+[ 13362ms] [LOG] Request 3vyu2yhjsasom22g-1 waiting for socket reconnection... @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/service-call.js:174
+[ 29608ms] [LOG] Request 3vyu2yhjsasom22g-1 ran out of retries @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/service-call.js:147
+[ 29803ms] [LOG] Request 3vyu2yhjsasom22g-2 ran out of retries @ http://localhost:5173/@fs/home/elpresidank/YeeBois/dev/trustgraph/ts/packages/client/dist/socket/service-call.js:147
+[ 69887ms] [VERBOSE] [DOM] Password field is not contained in a form: (More info: https://goo.gl/9p2vKq) %o @ http://localhost:5173/mcp-tools:0
+[ 142680ms] [VERBOSE] [DOM] Password field is not contained in a form: (More info: https://goo.gl/9p2vKq) %o @ http://localhost:5173/mcp-tools:0
+[ 219289ms] [VERBOSE] [DOM] Password field is not contained in a form: (More info: https://goo.gl/9p2vKq) %o @ http://localhost:5173/mcp-tools:0
+[ 238347ms] [VERBOSE] [DOM] Password field is not contained in a form: (More info: https://goo.gl/9p2vKq) %o @ http://localhost:5173/mcp-tools:0
+[ 255087ms] [VERBOSE] [DOM] Password field is not contained in a form: (More info: https://goo.gl/9p2vKq) %o @ http://localhost:5173/mcp-tools:0
diff --git a/.playwright-mcp/page-2026-04-12T05-27-22-648Z.yml b/.playwright-mcp/page-2026-04-12T05-27-22-648Z.yml
new file mode 100644
index 00000000..22a563ff
--- /dev/null
+++ b/.playwright-mcp/page-2026-04-12T05-27-22-648Z.yml
@@ -0,0 +1,77 @@
+- generic [ref=e3]:
+ - link "Skip to content" [ref=e4] [cursor=pointer]:
+ - /url: "#main-content"
+ - complementary "Sidebar" [ref=e5]:
+ - generic [ref=e6]:
+ - img [ref=e7]
+ - generic [ref=e10]: TrustGraph
+ - generic [ref=e13]:
+ - generic [ref=e14]:
+ - generic [ref=e15]:
+ - img [ref=e16]
+ - text: Flow
+ - generic [ref=e20]:
+ - combobox "Flow" [ref=e21]:
+ - option "default" [selected]
+ - img
+ - generic [ref=e22]:
+ - img [ref=e23]
+ - generic [ref=e27]: default
+ - navigation "Main navigation" [ref=e29]:
+ - link "Chat" [ref=e30] [cursor=pointer]:
+ - /url: /chat
+ - generic [ref=e31]:
+ - img [ref=e32]
+ - generic [ref=e34]: Chat
+ - link "Library" [ref=e35] [cursor=pointer]:
+ - /url: /library
+ - generic [ref=e36]:
+ - img [ref=e37]
+ - generic [ref=e40]: Library
+ - link "Graph" [ref=e41] [cursor=pointer]:
+ - /url: /graph
+ - generic [ref=e42]:
+ - img [ref=e43]
+ - generic [ref=e47]: Graph
+ - link "Prompts" [ref=e48] [cursor=pointer]:
+ - /url: /prompts
+ - generic [ref=e49]:
+ - img [ref=e50]
+ - generic [ref=e54]: Prompts
+ - link "Token Cost" [ref=e55] [cursor=pointer]:
+ - /url: /token-cost
+ - generic [ref=e56]:
+ - img [ref=e57]
+ - generic [ref=e62]: Token Cost
+ - link "Knowledge Cores" [ref=e63] [cursor=pointer]:
+ - /url: /knowledge-cores
+ - generic [ref=e64]:
+ - img [ref=e65]
+ - generic [ref=e77]: Knowledge Cores
+ - link "Flows" [ref=e78] [cursor=pointer]:
+ - /url: /flows
+ - generic [ref=e79]:
+ - img [ref=e80]
+ - generic [ref=e84]: Flows
+ - link "Settings" [ref=e85] [cursor=pointer]:
+ - /url: /settings
+ - generic [ref=e86]:
+ - img [ref=e87]
+ - generic [ref=e90]: Settings
+ - generic [ref=e92]:
+ - img [ref=e94]
+ - generic [ref=e101]: connecting
+ - generic [ref=e102]:
+ - banner [ref=e103]:
+ - generic [ref=e104]:
+ - generic [ref=e105]:
+ - img [ref=e106]
+ - text: default
+ - generic [ref=e110]:
+ - img [ref=e111]
+ - text: default
+ - main [ref=e115]:
+ - generic [ref=e116]:
+ - img [ref=e117]
+ - paragraph [ref=e119]: MCP Tools is not enabled.
+ - paragraph [ref=e120]: Enable it in Settings → Feature Switches → MCP Tools.
\ No newline at end of file
diff --git a/.playwright-mcp/page-2026-04-12T05-27-32-384Z.yml b/.playwright-mcp/page-2026-04-12T05-27-32-384Z.yml
new file mode 100644
index 00000000..07a512c8
--- /dev/null
+++ b/.playwright-mcp/page-2026-04-12T05-27-32-384Z.yml
@@ -0,0 +1,176 @@
+- generic [ref=e3]:
+ - link "Skip to content" [ref=e4] [cursor=pointer]:
+ - /url: "#main-content"
+ - complementary "Sidebar" [ref=e5]:
+ - generic [ref=e6]:
+ - img [ref=e7]
+ - generic [ref=e10]: TrustGraph
+ - generic [ref=e13]:
+ - generic [ref=e14]:
+ - generic [ref=e15]:
+ - img [ref=e16]
+ - text: Flow
+ - generic [ref=e20]:
+ - combobox "Flow" [ref=e21]:
+ - option "default" [selected]
+ - img
+ - generic [ref=e22]:
+ - img [ref=e23]
+ - generic [ref=e27]: default
+ - navigation "Main navigation" [ref=e29]:
+ - link "Chat" [ref=e30] [cursor=pointer]:
+ - /url: /chat
+ - generic [ref=e31]:
+ - img [ref=e32]
+ - generic [ref=e34]: Chat
+ - link "Library" [ref=e35] [cursor=pointer]:
+ - /url: /library
+ - generic [ref=e36]:
+ - img [ref=e37]
+ - generic [ref=e40]: Library
+ - link "Graph" [ref=e41] [cursor=pointer]:
+ - /url: /graph
+ - generic [ref=e42]:
+ - img [ref=e43]
+ - generic [ref=e47]: Graph
+ - link "Prompts" [ref=e48] [cursor=pointer]:
+ - /url: /prompts
+ - generic [ref=e49]:
+ - img [ref=e50]
+ - generic [ref=e54]: Prompts
+ - link "Token Cost" [ref=e55] [cursor=pointer]:
+ - /url: /token-cost
+ - generic [ref=e56]:
+ - img [ref=e57]
+ - generic [ref=e62]: Token Cost
+ - link "Knowledge Cores" [ref=e63] [cursor=pointer]:
+ - /url: /knowledge-cores
+ - generic [ref=e64]:
+ - img [ref=e65]
+ - generic [ref=e77]: Knowledge Cores
+ - link "Flows" [ref=e78] [cursor=pointer]:
+ - /url: /flows
+ - generic [ref=e79]:
+ - img [ref=e80]
+ - generic [ref=e84]: Flows
+ - link "Settings" [ref=e85] [cursor=pointer]:
+ - /url: /settings
+ - generic [ref=e86]:
+ - img [ref=e87]
+ - generic [ref=e90]: Settings
+ - generic [ref=e92]:
+ - img [ref=e94]
+ - generic [ref=e101]: connecting
+ - generic [ref=e102]:
+ - banner [ref=e103]:
+ - generic [ref=e104]:
+ - generic [ref=e105]:
+ - img [ref=e106]
+ - text: default
+ - generic [ref=e110]:
+ - img [ref=e111]
+ - text: default
+ - main [ref=e115]:
+ - generic [ref=e116]:
+ - generic [ref=e117]:
+ - img [ref=e118]
+ - heading "Settings" [level=1] [ref=e121]
+ - generic [ref=e122]:
+ - generic [ref=e123]:
+ - heading "Connection" [level=2] [ref=e124]:
+ - img [ref=e125]
+ - text: Connection
+ - generic [ref=e129]:
+ - generic [ref=e130]:
+ - generic [ref=e131]: "Status:"
+ - generic [ref=e132]:
+ - img [ref=e133]
+ - text: connecting
+ - generic [ref=e140]:
+ - generic [ref=e141]: Gateway URL
+ - textbox "Gateway URL" [ref=e142]:
+ - /placeholder: Leave blank to use the default proxy
+ - paragraph [ref=e143]: The WebSocket URL for the TrustGraph gateway.
+ - generic [ref=e144]:
+ - generic [ref=e145]: User ID
+ - textbox "User ID" [ref=e146]: user
+ - generic [ref=e147]:
+ - heading "Authentication" [level=2] [ref=e148]:
+ - img [ref=e149]
+ - text: Authentication
+ - generic [ref=e154]:
+ - generic [ref=e155]: API Key
+ - generic [ref=e156]:
+ - textbox "API Key" [ref=e157]:
+ - /placeholder: Leave blank for unauthenticated access
+ - button "Show API key" [ref=e158]:
+ - img [ref=e159]
+ - paragraph [ref=e162]: Changing the API key will reconnect the WebSocket.
+ - generic [ref=e163]:
+ - heading "Collection" [level=2] [ref=e164]:
+ - img [ref=e165]
+ - text: Collection
+ - generic [ref=e170]:
+ - generic [ref=e171]: Active Collection
+ - generic [ref=e172]:
+ - img [ref=e173]
+ - text: Loading collections...
+ - generic [ref=e175]:
+ - heading "Active Flow" [level=2] [ref=e176]:
+ - img [ref=e177]
+ - text: Active Flow
+ - generic [ref=e182]:
+ - generic [ref=e183]: Flow
+ - textbox "Flow" [ref=e184]:
+ - /placeholder: default
+ - text: default
+ - paragraph [ref=e185]: The flow ID used for chat, graph queries, and document processing.
+ - generic [ref=e186]:
+ - heading "Appearance" [level=2] [ref=e187]:
+ - img [ref=e188]
+ - text: Appearance
+ - generic [ref=e191]:
+ - generic [ref=e192]:
+ - paragraph [ref=e193]: Theme
+ - paragraph [ref=e194]: Toggle between dark and light mode.
+ - paragraph [ref=e195]: Currently using dark mode.
+ - switch "Dark mode" [checked] [ref=e196]
+ - generic [ref=e198]:
+ - heading "Feature Switches" [level=2] [ref=e199]:
+ - img [ref=e200]
+ - text: Feature Switches
+ - generic [ref=e203]:
+ - generic [ref=e204]:
+ - paragraph [ref=e206]: Flow Classes
+ - switch "Flow Classes" [ref=e207]
+ - generic [ref=e209]:
+ - paragraph [ref=e211]: Submissions
+ - switch "Submissions" [ref=e212]
+ - generic [ref=e214]:
+ - paragraph [ref=e216]: Token Cost
+ - switch "Token Cost" [ref=e217]
+ - generic [ref=e219]:
+ - paragraph [ref=e221]: Schemas
+ - switch "Schemas" [ref=e222]
+ - generic [ref=e224]:
+ - paragraph [ref=e226]: Structured Query
+ - switch "Structured Query" [ref=e227]
+ - generic [ref=e229]:
+ - paragraph [ref=e231]: Ontology Editor
+ - switch "Ontology Editor" [ref=e232]
+ - generic [ref=e234]:
+ - paragraph [ref=e236]: Agent Tools
+ - switch "Agent Tools" [ref=e237]
+ - generic [ref=e239]:
+ - paragraph [ref=e241]: MCP Tools
+ - switch "MCP Tools" [ref=e242]
+ - generic [ref=e244]:
+ - paragraph [ref=e246]: LLM Models
+ - switch "LLM Models" [ref=e247]
+ - generic [ref=e249]:
+ - heading "About" [level=2] [ref=e250]:
+ - img [ref=e251]
+ - text: About
+ - generic [ref=e254]:
+ - paragraph [ref=e255]: TrustGraph Workbench v0.1.0
+ - paragraph [ref=e256]: A web-based interface for interacting with the TrustGraph knowledge-graph system.
\ No newline at end of file
diff --git a/.playwright-mcp/page-2026-04-12T05-27-56-501Z.yml b/.playwright-mcp/page-2026-04-12T05-27-56-501Z.yml
new file mode 100644
index 00000000..b9c32352
--- /dev/null
+++ b/.playwright-mcp/page-2026-04-12T05-27-56-501Z.yml
@@ -0,0 +1,184 @@
+- generic [ref=e3]:
+ - link "Skip to content" [ref=e4] [cursor=pointer]:
+ - /url: "#main-content"
+ - complementary "Sidebar" [ref=e5]:
+ - generic [ref=e6]:
+ - img [ref=e7]
+ - generic [ref=e10]: TrustGraph
+ - generic [ref=e13]:
+ - generic [ref=e14]:
+ - generic [ref=e15]:
+ - img [ref=e16]
+ - text: Flow
+ - generic [ref=e20]:
+ - combobox "Flow" [ref=e21]:
+ - option "default" [selected]
+ - img
+ - generic [ref=e22]:
+ - img [ref=e23]
+ - generic [ref=e27]: default
+ - navigation "Main navigation" [ref=e29]:
+ - link "Chat" [ref=e30] [cursor=pointer]:
+ - /url: /chat
+ - generic [ref=e31]:
+ - img [ref=e32]
+ - generic [ref=e34]: Chat
+ - link "Library" [ref=e35] [cursor=pointer]:
+ - /url: /library
+ - generic [ref=e36]:
+ - img [ref=e37]
+ - generic [ref=e40]: Library
+ - link "Graph" [ref=e41] [cursor=pointer]:
+ - /url: /graph
+ - generic [ref=e42]:
+ - img [ref=e43]
+ - generic [ref=e47]: Graph
+ - link "Prompts" [ref=e48] [cursor=pointer]:
+ - /url: /prompts
+ - generic [ref=e49]:
+ - img [ref=e50]
+ - generic [ref=e54]: Prompts
+ - link "Token Cost" [ref=e55] [cursor=pointer]:
+ - /url: /token-cost
+ - generic [ref=e56]:
+ - img [ref=e57]
+ - generic [ref=e62]: Token Cost
+ - link "Knowledge Cores" [ref=e63] [cursor=pointer]:
+ - /url: /knowledge-cores
+ - generic [ref=e64]:
+ - img [ref=e65]
+ - generic [ref=e77]: Knowledge Cores
+ - link "Flows" [ref=e78] [cursor=pointer]:
+ - /url: /flows
+ - generic [ref=e79]:
+ - img [ref=e80]
+ - generic [ref=e84]: Flows
+ - link "MCP Tools" [ref=e266] [cursor=pointer]:
+ - /url: /mcp-tools
+ - generic [ref=e267]:
+ - img [ref=e268]
+ - generic [ref=e270]: MCP Tools
+ - link "Settings" [ref=e85] [cursor=pointer]:
+ - /url: /settings
+ - generic [ref=e86]:
+ - img [ref=e87]
+ - generic [ref=e90]: Settings
+ - generic [ref=e92]:
+ - img [ref=e94]
+ - generic [ref=e101]: reconnecting
+ - generic [ref=e102]:
+ - banner [ref=e103]:
+ - generic [ref=e104]:
+ - generic [ref=e105]:
+ - img [ref=e106]
+ - text: default
+ - generic [ref=e110]:
+ - img [ref=e111]
+ - text: default
+ - alert [ref=e257]:
+ - img [ref=e258]
+ - generic [ref=e265]: Connection lost. Attempting to reconnect...
+ - main [ref=e115]:
+ - generic [ref=e116]:
+ - generic [ref=e117]:
+ - img [ref=e118]
+ - heading "Settings" [level=1] [ref=e121]
+ - generic [ref=e122]:
+ - generic [ref=e123]:
+ - heading "Connection" [level=2] [ref=e124]:
+ - img [ref=e125]
+ - text: Connection
+ - generic [ref=e129]:
+ - generic [ref=e130]:
+ - generic [ref=e131]: "Status:"
+ - generic [ref=e132]:
+ - img [ref=e133]
+ - text: reconnecting
+ - generic [ref=e140]:
+ - generic [ref=e141]: Gateway URL
+ - textbox "Gateway URL" [ref=e142]:
+ - /placeholder: Leave blank to use the default proxy
+ - paragraph [ref=e143]: The WebSocket URL for the TrustGraph gateway.
+ - generic [ref=e144]:
+ - generic [ref=e145]: User ID
+ - textbox "User ID" [ref=e146]: user
+ - generic [ref=e147]:
+ - heading "Authentication" [level=2] [ref=e148]:
+ - img [ref=e149]
+ - text: Authentication
+ - generic [ref=e154]:
+ - generic [ref=e155]: API Key
+ - generic [ref=e156]:
+ - textbox "API Key" [ref=e157]:
+ - /placeholder: Leave blank for unauthenticated access
+ - button "Show API key" [ref=e158]:
+ - img [ref=e159]
+ - paragraph [ref=e162]: Changing the API key will reconnect the WebSocket.
+ - generic [ref=e163]:
+ - heading "Collection" [level=2] [ref=e164]:
+ - img [ref=e165]
+ - text: Collection
+ - generic [ref=e170]:
+ - generic [ref=e171]: Active Collection
+ - generic [ref=e172]:
+ - img [ref=e173]
+ - text: Loading collections...
+ - generic [ref=e175]:
+ - heading "Active Flow" [level=2] [ref=e176]:
+ - img [ref=e177]
+ - text: Active Flow
+ - generic [ref=e182]:
+ - generic [ref=e183]: Flow
+ - textbox "Flow" [ref=e184]:
+ - /placeholder: default
+ - text: default
+ - paragraph [ref=e185]: The flow ID used for chat, graph queries, and document processing.
+ - generic [ref=e186]:
+ - heading "Appearance" [level=2] [ref=e187]:
+ - img [ref=e188]
+ - text: Appearance
+ - generic [ref=e191]:
+ - generic [ref=e192]:
+ - paragraph [ref=e193]: Theme
+ - paragraph [ref=e194]: Toggle between dark and light mode.
+ - paragraph [ref=e195]: Currently using dark mode.
+ - switch "Dark mode" [checked] [ref=e196]
+ - generic [ref=e198]:
+ - heading "Feature Switches" [level=2] [ref=e199]:
+ - img [ref=e200]
+ - text: Feature Switches
+ - generic [ref=e203]:
+ - generic [ref=e204]:
+ - paragraph [ref=e206]: Flow Classes
+ - switch "Flow Classes" [ref=e207]
+ - generic [ref=e209]:
+ - paragraph [ref=e211]: Submissions
+ - switch "Submissions" [ref=e212]
+ - generic [ref=e214]:
+ - paragraph [ref=e216]: Token Cost
+ - switch "Token Cost" [ref=e217]
+ - generic [ref=e219]:
+ - paragraph [ref=e221]: Schemas
+ - switch "Schemas" [ref=e222]
+ - generic [ref=e224]:
+ - paragraph [ref=e226]: Structured Query
+ - switch "Structured Query" [ref=e227]
+ - generic [ref=e229]:
+ - paragraph [ref=e231]: Ontology Editor
+ - switch "Ontology Editor" [ref=e232]
+ - generic [ref=e234]:
+ - paragraph [ref=e236]: Agent Tools
+ - switch "Agent Tools" [ref=e237]
+ - generic [ref=e239]:
+ - paragraph [ref=e241]: MCP Tools
+ - switch "MCP Tools" [checked] [active] [ref=e242]
+ - generic [ref=e244]:
+ - paragraph [ref=e246]: LLM Models
+ - switch "LLM Models" [ref=e247]
+ - generic [ref=e249]:
+ - heading "About" [level=2] [ref=e250]:
+ - img [ref=e251]
+ - text: About
+ - generic [ref=e254]:
+ - paragraph [ref=e255]: TrustGraph Workbench v0.1.0
+ - paragraph [ref=e256]: A web-based interface for interacting with the TrustGraph knowledge-graph system.
\ No newline at end of file
diff --git a/.playwright-mcp/page-2026-04-12T05-28-05-234Z.yml b/.playwright-mcp/page-2026-04-12T05-28-05-234Z.yml
new file mode 100644
index 00000000..4af701d2
--- /dev/null
+++ b/.playwright-mcp/page-2026-04-12T05-28-05-234Z.yml
@@ -0,0 +1,106 @@
+- generic [ref=e3]:
+ - link "Skip to content" [ref=e4] [cursor=pointer]:
+ - /url: "#main-content"
+ - complementary "Sidebar" [ref=e5]:
+ - generic [ref=e6]:
+ - img [ref=e7]
+ - generic [ref=e10]: TrustGraph
+ - generic [ref=e13]:
+ - generic [ref=e14]:
+ - generic [ref=e15]:
+ - img [ref=e16]
+ - text: Flow
+ - generic [ref=e20]:
+ - combobox "Flow" [ref=e21]:
+ - option "default" [selected]
+ - img
+ - generic [ref=e22]:
+ - img [ref=e23]
+ - generic [ref=e27]: default
+ - navigation "Main navigation" [ref=e29]:
+ - link "Chat" [ref=e30] [cursor=pointer]:
+ - /url: /chat
+ - generic [ref=e31]:
+ - img [ref=e32]
+ - generic [ref=e34]: Chat
+ - link "Library" [ref=e35] [cursor=pointer]:
+ - /url: /library
+ - generic [ref=e36]:
+ - img [ref=e37]
+ - generic [ref=e40]: Library
+ - link "Graph" [ref=e41] [cursor=pointer]:
+ - /url: /graph
+ - generic [ref=e42]:
+ - img [ref=e43]
+ - generic [ref=e47]: Graph
+ - link "Prompts" [ref=e48] [cursor=pointer]:
+ - /url: /prompts
+ - generic [ref=e49]:
+ - img [ref=e50]
+ - generic [ref=e54]: Prompts
+ - link "Token Cost" [ref=e55] [cursor=pointer]:
+ - /url: /token-cost
+ - generic [ref=e56]:
+ - img [ref=e57]
+ - generic [ref=e62]: Token Cost
+ - link "Knowledge Cores" [ref=e63] [cursor=pointer]:
+ - /url: /knowledge-cores
+ - generic [ref=e64]:
+ - img [ref=e65]
+ - generic [ref=e77]: Knowledge Cores
+ - link "Flows" [ref=e78] [cursor=pointer]:
+ - /url: /flows
+ - generic [ref=e79]:
+ - img [ref=e80]
+ - generic [ref=e84]: Flows
+ - link "MCP Tools" [active] [ref=e266] [cursor=pointer]:
+ - /url: /mcp-tools
+ - generic [ref=e267]:
+ - img [ref=e268]
+ - generic [ref=e270]: MCP Tools
+ - link "Settings" [ref=e85] [cursor=pointer]:
+ - /url: /settings
+ - generic [ref=e86]:
+ - img [ref=e87]
+ - generic [ref=e90]: Settings
+ - generic [ref=e92]:
+ - img [ref=e94]
+ - generic [ref=e101]: reconnecting
+ - generic [ref=e102]:
+ - banner [ref=e103]:
+ - generic [ref=e104]:
+ - generic [ref=e105]:
+ - img [ref=e106]
+ - text: default
+ - generic [ref=e110]:
+ - img [ref=e111]
+ - text: default
+ - alert [ref=e257]:
+ - img [ref=e258]
+ - generic [ref=e265]: Connection lost. Attempting to reconnect...
+ - main [ref=e115]:
+ - generic [ref=e271]:
+ - generic [ref=e272]:
+ - generic [ref=e273]:
+ - img [ref=e274]
+ - heading "MCP Tools" [level=1] [ref=e276]
+ - generic [ref=e277]: 0 servers, 0 tools
+ - generic [ref=e278]:
+ - button "Refresh" [ref=e279]:
+ - img [ref=e280]
+ - text: Refresh
+ - button "Add Server" [ref=e285]:
+ - img [ref=e286]
+ - text: Add Server
+ - tablist "MCP sections" [ref=e287]:
+ - tab "Servers" [selected] [ref=e288]:
+ - img [ref=e289]
+ - text: Servers
+ - tab "Tools" [ref=e292]:
+ - img [ref=e293]
+ - text: Tools
+ - tabpanel "Servers" [ref=e295]:
+ - generic [ref=e296]:
+ - img [ref=e297]
+ - paragraph [ref=e300]: No MCP servers configured.
+ - paragraph [ref=e301]: Click "Add Server" to connect an external MCP server.
\ No newline at end of file
diff --git a/.playwright-mcp/page-2026-04-12T05-28-17-352Z.yml b/.playwright-mcp/page-2026-04-12T05-28-17-352Z.yml
new file mode 100644
index 00000000..cb222d0a
--- /dev/null
+++ b/.playwright-mcp/page-2026-04-12T05-28-17-352Z.yml
@@ -0,0 +1,106 @@
+- generic [ref=e3]:
+ - link "Skip to content" [ref=e4] [cursor=pointer]:
+ - /url: "#main-content"
+ - complementary "Sidebar" [ref=e5]:
+ - generic [ref=e6]:
+ - img [ref=e7]
+ - generic [ref=e10]: TrustGraph
+ - generic [ref=e13]:
+ - generic [ref=e14]:
+ - generic [ref=e15]:
+ - img [ref=e16]
+ - text: Flow
+ - generic [ref=e20]:
+ - combobox "Flow" [ref=e21]:
+ - option "default" [selected]
+ - img
+ - generic [ref=e22]:
+ - img [ref=e23]
+ - generic [ref=e27]: default
+ - navigation "Main navigation" [ref=e29]:
+ - link "Chat" [ref=e30] [cursor=pointer]:
+ - /url: /chat
+ - generic [ref=e31]:
+ - img [ref=e32]
+ - generic [ref=e34]: Chat
+ - link "Library" [ref=e35] [cursor=pointer]:
+ - /url: /library
+ - generic [ref=e36]:
+ - img [ref=e37]
+ - generic [ref=e40]: Library
+ - link "Graph" [ref=e41] [cursor=pointer]:
+ - /url: /graph
+ - generic [ref=e42]:
+ - img [ref=e43]
+ - generic [ref=e47]: Graph
+ - link "Prompts" [ref=e48] [cursor=pointer]:
+ - /url: /prompts
+ - generic [ref=e49]:
+ - img [ref=e50]
+ - generic [ref=e54]: Prompts
+ - link "Token Cost" [ref=e55] [cursor=pointer]:
+ - /url: /token-cost
+ - generic [ref=e56]:
+ - img [ref=e57]
+ - generic [ref=e62]: Token Cost
+ - link "Knowledge Cores" [ref=e63] [cursor=pointer]:
+ - /url: /knowledge-cores
+ - generic [ref=e64]:
+ - img [ref=e65]
+ - generic [ref=e77]: Knowledge Cores
+ - link "Flows" [ref=e78] [cursor=pointer]:
+ - /url: /flows
+ - generic [ref=e79]:
+ - img [ref=e80]
+ - generic [ref=e84]: Flows
+ - link "MCP Tools" [ref=e266] [cursor=pointer]:
+ - /url: /mcp-tools
+ - generic [ref=e267]:
+ - img [ref=e268]
+ - generic [ref=e270]: MCP Tools
+ - link "Settings" [ref=e85] [cursor=pointer]:
+ - /url: /settings
+ - generic [ref=e86]:
+ - img [ref=e87]
+ - generic [ref=e90]: Settings
+ - generic [ref=e92]:
+ - img [ref=e94]
+ - generic [ref=e101]: reconnecting
+ - generic [ref=e102]:
+ - banner [ref=e103]:
+ - generic [ref=e104]:
+ - generic [ref=e105]:
+ - img [ref=e106]
+ - text: default
+ - generic [ref=e110]:
+ - img [ref=e111]
+ - text: default
+ - alert [ref=e257]:
+ - img [ref=e258]
+ - generic [ref=e265]: Connection lost. Attempting to reconnect...
+ - main [ref=e115]:
+ - generic [ref=e271]:
+ - generic [ref=e272]:
+ - generic [ref=e273]:
+ - img [ref=e274]
+ - heading "MCP Tools" [level=1] [ref=e276]
+ - generic [ref=e277]: 0 servers, 0 tools
+ - generic [ref=e278]:
+ - button "Refresh" [ref=e279]:
+ - img [ref=e280]
+ - text: Refresh
+ - button "Add Tool" [ref=e302]:
+ - img [ref=e286]
+ - text: Add Tool
+ - tablist "MCP sections" [ref=e287]:
+ - tab "Servers" [ref=e288]:
+ - img [ref=e289]
+ - text: Servers
+ - tab "Tools" [active] [selected] [ref=e292]:
+ - img [ref=e293]
+ - text: Tools
+ - tabpanel "Tools" [ref=e303]:
+ - generic [ref=e304]:
+ - img [ref=e305]
+ - paragraph [ref=e307]: No tools configured.
+ - paragraph [ref=e308]: Click "Add Tool" to create an MCP tool.
\ No newline at end of file
diff --git a/.playwright-mcp/page-2026-04-12T05-28-36-861Z.yml b/.playwright-mcp/page-2026-04-12T05-28-36-861Z.yml
new file mode 100644
index 00000000..ee26e755
--- /dev/null
+++ b/.playwright-mcp/page-2026-04-12T05-28-36-861Z.yml
@@ -0,0 +1,106 @@
+- generic [ref=e3]:
+ - link "Skip to content" [ref=e4] [cursor=pointer]:
+ - /url: "#main-content"
+ - complementary "Sidebar" [ref=e5]:
+ - generic [ref=e6]:
+ - img [ref=e7]
+ - generic [ref=e10]: TrustGraph
+ - generic [ref=e13]:
+ - generic [ref=e14]:
+ - generic [ref=e15]:
+ - img [ref=e16]
+ - text: Flow
+ - generic [ref=e20]:
+ - combobox "Flow" [ref=e21]:
+ - option "default" [selected]
+ - img
+ - generic [ref=e22]:
+ - img [ref=e23]
+ - generic [ref=e27]: default
+ - navigation "Main navigation" [ref=e29]:
+ - link "Chat" [ref=e30] [cursor=pointer]:
+ - /url: /chat
+ - generic [ref=e31]:
+ - img [ref=e32]
+ - generic [ref=e34]: Chat
+ - link "Library" [ref=e35] [cursor=pointer]:
+ - /url: /library
+ - generic [ref=e36]:
+ - img [ref=e37]
+ - generic [ref=e40]: Library
+ - link "Graph" [ref=e41] [cursor=pointer]:
+ - /url: /graph
+ - generic [ref=e42]:
+ - img [ref=e43]
+ - generic [ref=e47]: Graph
+ - link "Prompts" [ref=e48] [cursor=pointer]:
+ - /url: /prompts
+ - generic [ref=e49]:
+ - img [ref=e50]
+ - generic [ref=e54]: Prompts
+ - link "Token Cost" [ref=e55] [cursor=pointer]:
+ - /url: /token-cost
+ - generic [ref=e56]:
+ - img [ref=e57]
+ - generic [ref=e62]: Token Cost
+ - link "Knowledge Cores" [ref=e63] [cursor=pointer]:
+ - /url: /knowledge-cores
+ - generic [ref=e64]:
+ - img [ref=e65]
+ - generic [ref=e77]: Knowledge Cores
+ - link "Flows" [ref=e78] [cursor=pointer]:
+ - /url: /flows
+ - generic [ref=e79]:
+ - img [ref=e80]
+ - generic [ref=e84]: Flows
+ - link "MCP Tools" [ref=e266] [cursor=pointer]:
+ - /url: /mcp-tools
+ - generic [ref=e267]:
+ - img [ref=e268]
+ - generic [ref=e270]: MCP Tools
+ - link "Settings" [ref=e85] [cursor=pointer]:
+ - /url: /settings
+ - generic [ref=e86]:
+ - img [ref=e87]
+ - generic [ref=e90]: Settings
+ - generic [ref=e92]:
+ - img [ref=e94]
+ - generic [ref=e101]: reconnecting
+ - generic [ref=e102]:
+ - banner [ref=e103]:
+ - generic [ref=e104]:
+ - generic [ref=e105]:
+ - img [ref=e106]
+ - text: default
+ - generic [ref=e110]:
+ - img [ref=e111]
+ - text: default
+ - alert [ref=e257]:
+ - img [ref=e258]
+ - generic [ref=e265]: Connection lost. Attempting to reconnect...
+ - main [ref=e115]:
+ - generic [ref=e271]:
+ - generic [ref=e272]:
+ - generic [ref=e273]:
+ - img [ref=e274]
+ - heading "MCP Tools" [level=1] [ref=e276]
+ - generic [ref=e277]: 0 servers, 0 tools
+ - generic [ref=e278]:
+ - button "Refresh" [ref=e279]:
+ - img [ref=e280]
+ - text: Refresh
+ - button "Add Server" [ref=e309]:
+ - img [ref=e286]
+ - text: Add Server
+ - tablist "MCP sections" [ref=e287]:
+ - tab "Servers" [active] [selected] [ref=e288]:
+ - img [ref=e289]
+ - text: Servers
+ - tab "Tools" [ref=e292]:
+ - img [ref=e293]
+ - text: Tools
+ - tabpanel "Servers" [ref=e310]:
+ - generic [ref=e311]:
+ - img [ref=e312]
+ - paragraph [ref=e315]: No MCP servers configured.
+ - paragraph [ref=e316]: Click "Add Server" to connect an external MCP server.
\ No newline at end of file
diff --git a/.playwright-mcp/page-2026-04-12T05-28-42-962Z.yml b/.playwright-mcp/page-2026-04-12T05-28-42-962Z.yml
new file mode 100644
index 00000000..54b9b6e1
--- /dev/null
+++ b/.playwright-mcp/page-2026-04-12T05-28-42-962Z.yml
@@ -0,0 +1,134 @@
+- generic [ref=e3]:
+ - link "Skip to content" [ref=e4] [cursor=pointer]:
+ - /url: "#main-content"
+ - complementary "Sidebar" [ref=e5]:
+ - generic [ref=e6]:
+ - img [ref=e7]
+ - generic [ref=e10]: TrustGraph
+ - generic [ref=e13]:
+ - generic [ref=e14]:
+ - generic [ref=e15]:
+ - img [ref=e16]
+ - text: Flow
+ - generic [ref=e20]:
+ - combobox "Flow" [ref=e21]:
+ - option "default" [selected]
+ - img
+ - generic [ref=e22]:
+ - img [ref=e23]
+ - generic [ref=e27]: default
+ - navigation "Main navigation" [ref=e29]:
+ - link "Chat" [ref=e30] [cursor=pointer]:
+ - /url: /chat
+ - generic [ref=e31]:
+ - img [ref=e32]
+ - generic [ref=e34]: Chat
+ - link "Library" [ref=e35] [cursor=pointer]:
+ - /url: /library
+ - generic [ref=e36]:
+ - img [ref=e37]
+ - generic [ref=e40]: Library
+ - link "Graph" [ref=e41] [cursor=pointer]:
+ - /url: /graph
+ - generic [ref=e42]:
+ - img [ref=e43]
+ - generic [ref=e47]: Graph
+ - link "Prompts" [ref=e48] [cursor=pointer]:
+ - /url: /prompts
+ - generic [ref=e49]:
+ - img [ref=e50]
+ - generic [ref=e54]: Prompts
+ - link "Token Cost" [ref=e55] [cursor=pointer]:
+ - /url: /token-cost
+ - generic [ref=e56]:
+ - img [ref=e57]
+ - generic [ref=e62]: Token Cost
+ - link "Knowledge Cores" [ref=e63] [cursor=pointer]:
+ - /url: /knowledge-cores
+ - generic [ref=e64]:
+ - img [ref=e65]
+ - generic [ref=e77]: Knowledge Cores
+ - link "Flows" [ref=e78] [cursor=pointer]:
+ - /url: /flows
+ - generic [ref=e79]:
+ - img [ref=e80]
+ - generic [ref=e84]: Flows
+ - link "MCP Tools" [ref=e266] [cursor=pointer]:
+ - /url: /mcp-tools
+ - generic [ref=e267]:
+ - img [ref=e268]
+ - generic [ref=e270]: MCP Tools
+ - link "Settings" [ref=e85] [cursor=pointer]:
+ - /url: /settings
+ - generic [ref=e86]:
+ - img [ref=e87]
+ - generic [ref=e90]: Settings
+ - generic [ref=e92]:
+ - img [ref=e94]
+ - generic [ref=e101]: reconnecting
+ - generic [ref=e102]:
+ - banner [ref=e103]:
+ - generic [ref=e104]:
+ - generic [ref=e105]:
+ - img [ref=e106]
+ - text: default
+ - generic [ref=e110]:
+ - img [ref=e111]
+ - text: default
+ - alert [ref=e257]:
+ - img [ref=e258]
+ - generic [ref=e265]: Connection lost. Attempting to reconnect...
+ - main [ref=e115]:
+ - generic [ref=e271]:
+ - generic [ref=e272]:
+ - generic [ref=e273]:
+ - img [ref=e274]
+ - heading "MCP Tools" [level=1] [ref=e276]
+ - generic [ref=e277]: 0 servers, 0 tools
+ - generic [ref=e278]:
+ - button "Refresh" [ref=e279]:
+ - img [ref=e280]
+ - text: Refresh
+ - button "Add Server" [ref=e309]:
+ - img [ref=e286]
+ - text: Add Server
+ - tablist "MCP sections" [ref=e287]:
+ - tab "Servers" [selected] [ref=e288]:
+ - img [ref=e289]
+ - text: Servers
+ - tab "Tools" [ref=e292]:
+ - img [ref=e293]
+ - text: Tools
+ - tabpanel "Servers" [ref=e310]:
+ - generic [ref=e311]:
+ - img [ref=e312]
+ - paragraph [ref=e315]: No MCP servers configured.
+ - paragraph [ref=e316]: Click "Add Server" to connect an external MCP server.
+ - dialog "Add MCP Server" [ref=e318]:
+ - generic [ref=e319]:
+ - heading "Add MCP Server" [level=2] [ref=e320]
+ - button "Close dialog" [ref=e321]:
+ - img [ref=e322]
+ - generic [ref=e326]:
+ - generic [ref=e327]:
+ - generic [ref=e328]: Key
+ - textbox "Key" [active] [ref=e329]:
+ - /placeholder: brave-search
+ - generic [ref=e330]:
+ - generic [ref=e331]: URL
+ - textbox "URL" [ref=e332]:
+ - /placeholder: http://localhost:8383/mcp
+ - generic [ref=e333]:
+ - generic [ref=e334]: Remote Name (optional)
+ - textbox "Remote Name (optional)" [ref=e335]:
+ - /placeholder: Tool name at the remote server
+ - generic [ref=e336]:
+ - generic [ref=e337]: Auth Token (optional)
+ - generic [ref=e338]:
+ - textbox "Auth Token (optional)" [ref=e339]:
+ - /placeholder: Bearer token for authentication
+ - button "Show auth token" [ref=e340]:
+ - img [ref=e341]
+ - generic [ref=e344]:
+ - button "Cancel" [ref=e345]
+ - button "Save" [disabled] [ref=e346]
\ No newline at end of file
diff --git a/.playwright-mcp/page-2026-04-12T05-29-03-429Z.yml b/.playwright-mcp/page-2026-04-12T05-29-03-429Z.yml
new file mode 100644
index 00000000..cb8af391
--- /dev/null
+++ b/.playwright-mcp/page-2026-04-12T05-29-03-429Z.yml
@@ -0,0 +1,106 @@
+- generic [ref=e3]:
+ - link "Skip to content" [ref=e4] [cursor=pointer]:
+ - /url: "#main-content"
+ - complementary "Sidebar" [ref=e5]:
+ - generic [ref=e6]:
+ - img [ref=e7]
+ - generic [ref=e10]: TrustGraph
+ - generic [ref=e13]:
+ - generic [ref=e14]:
+ - generic [ref=e15]:
+ - img [ref=e16]
+ - text: Flow
+ - generic [ref=e20]:
+ - combobox "Flow" [ref=e21]:
+ - option "default" [selected]
+ - img
+ - generic [ref=e22]:
+ - img [ref=e23]
+ - generic [ref=e27]: default
+ - navigation "Main navigation" [ref=e29]:
+ - link "Chat" [ref=e30] [cursor=pointer]:
+ - /url: /chat
+ - generic [ref=e31]:
+ - img [ref=e32]
+ - generic [ref=e34]: Chat
+ - link "Library" [ref=e35] [cursor=pointer]:
+ - /url: /library
+ - generic [ref=e36]:
+ - img [ref=e37]
+ - generic [ref=e40]: Library
+ - link "Graph" [ref=e41] [cursor=pointer]:
+ - /url: /graph
+ - generic [ref=e42]:
+ - img [ref=e43]
+ - generic [ref=e47]: Graph
+ - link "Prompts" [ref=e48] [cursor=pointer]:
+ - /url: /prompts
+ - generic [ref=e49]:
+ - img [ref=e50]
+ - generic [ref=e54]: Prompts
+ - link "Token Cost" [ref=e55] [cursor=pointer]:
+ - /url: /token-cost
+ - generic [ref=e56]:
+ - img [ref=e57]
+ - generic [ref=e62]: Token Cost
+ - link "Knowledge Cores" [ref=e63] [cursor=pointer]:
+ - /url: /knowledge-cores
+ - generic [ref=e64]:
+ - img [ref=e65]
+ - generic [ref=e77]: Knowledge Cores
+ - link "Flows" [ref=e78] [cursor=pointer]:
+ - /url: /flows
+ - generic [ref=e79]:
+ - img [ref=e80]
+ - generic [ref=e84]: Flows
+ - link "MCP Tools" [ref=e266] [cursor=pointer]:
+ - /url: /mcp-tools
+ - generic [ref=e267]:
+ - img [ref=e268]
+ - generic [ref=e270]: MCP Tools
+ - link "Settings" [ref=e85] [cursor=pointer]:
+ - /url: /settings
+ - generic [ref=e86]:
+ - img [ref=e87]
+ - generic [ref=e90]: Settings
+ - generic [ref=e92]:
+ - img [ref=e94]
+ - generic [ref=e101]: reconnecting
+ - generic [ref=e102]:
+ - banner [ref=e103]:
+ - generic [ref=e104]:
+ - generic [ref=e105]:
+ - img [ref=e106]
+ - text: default
+ - generic [ref=e110]:
+ - img [ref=e111]
+ - text: default
+ - alert [ref=e257]:
+ - img [ref=e258]
+ - generic [ref=e265]: Connection lost. Attempting to reconnect...
+ - main [ref=e115]:
+ - generic [ref=e271]:
+ - generic [ref=e272]:
+ - generic [ref=e273]:
+ - img [ref=e274]
+ - heading "MCP Tools" [level=1] [ref=e276]
+ - generic [ref=e277]: 0 servers, 0 tools
+ - generic [ref=e278]:
+ - button "Refresh" [ref=e279]:
+ - img [ref=e280]
+ - text: Refresh
+ - button "Add Tool" [ref=e347]:
+ - img [ref=e286]
+ - text: Add Tool
+ - tablist "MCP sections" [ref=e287]:
+ - tab "Servers" [ref=e288]:
+ - img [ref=e289]
+ - text: Servers
+ - tab "Tools" [active] [selected] [ref=e292]:
+ - img [ref=e293]
+ - text: Tools
+ - tabpanel "Tools" [ref=e348]:
+ - generic [ref=e349]:
+ - img [ref=e350]
+ - paragraph [ref=e352]: No tools configured.
+ - paragraph [ref=e353]: Click "Add Tool" to create an MCP tool.
\ No newline at end of file
diff --git a/.playwright-mcp/page-2026-04-12T05-29-09-123Z.yml b/.playwright-mcp/page-2026-04-12T05-29-09-123Z.yml
new file mode 100644
index 00000000..87008900
--- /dev/null
+++ b/.playwright-mcp/page-2026-04-12T05-29-09-123Z.yml
@@ -0,0 +1,145 @@
+- generic [ref=e3]:
+ - link "Skip to content" [ref=e4] [cursor=pointer]:
+ - /url: "#main-content"
+ - complementary "Sidebar" [ref=e5]:
+ - generic [ref=e6]:
+ - img [ref=e7]
+ - generic [ref=e10]: TrustGraph
+ - generic [ref=e13]:
+ - generic [ref=e14]:
+ - generic [ref=e15]:
+ - img [ref=e16]
+ - text: Flow
+ - generic [ref=e20]:
+ - combobox "Flow" [ref=e21]:
+ - option "default" [selected]
+ - img
+ - generic [ref=e22]:
+ - img [ref=e23]
+ - generic [ref=e27]: default
+ - navigation "Main navigation" [ref=e29]:
+ - link "Chat" [ref=e30] [cursor=pointer]:
+ - /url: /chat
+ - generic [ref=e31]:
+ - img [ref=e32]
+ - generic [ref=e34]: Chat
+ - link "Library" [ref=e35] [cursor=pointer]:
+ - /url: /library
+ - generic [ref=e36]:
+ - img [ref=e37]
+ - generic [ref=e40]: Library
+ - link "Graph" [ref=e41] [cursor=pointer]:
+ - /url: /graph
+ - generic [ref=e42]:
+ - img [ref=e43]
+ - generic [ref=e47]: Graph
+ - link "Prompts" [ref=e48] [cursor=pointer]:
+ - /url: /prompts
+ - generic [ref=e49]:
+ - img [ref=e50]
+ - generic [ref=e54]: Prompts
+ - link "Token Cost" [ref=e55] [cursor=pointer]:
+ - /url: /token-cost
+ - generic [ref=e56]:
+ - img [ref=e57]
+ - generic [ref=e62]: Token Cost
+ - link "Knowledge Cores" [ref=e63] [cursor=pointer]:
+ - /url: /knowledge-cores
+ - generic [ref=e64]:
+ - img [ref=e65]
+ - generic [ref=e77]: Knowledge Cores
+ - link "Flows" [ref=e78] [cursor=pointer]:
+ - /url: /flows
+ - generic [ref=e79]:
+ - img [ref=e80]
+ - generic [ref=e84]: Flows
+ - link "MCP Tools" [ref=e266] [cursor=pointer]:
+ - /url: /mcp-tools
+ - generic [ref=e267]:
+ - img [ref=e268]
+ - generic [ref=e270]: MCP Tools
+ - link "Settings" [ref=e85] [cursor=pointer]:
+ - /url: /settings
+ - generic [ref=e86]:
+ - img [ref=e87]
+ - generic [ref=e90]: Settings
+ - generic [ref=e92]:
+ - img [ref=e94]
+ - generic [ref=e101]: reconnecting
+ - generic [ref=e102]:
+ - banner [ref=e103]:
+ - generic [ref=e104]:
+ - generic [ref=e105]:
+ - img [ref=e106]
+ - text: default
+ - generic [ref=e110]:
+ - img [ref=e111]
+ - text: default
+ - alert [ref=e257]:
+ - img [ref=e258]
+ - generic [ref=e265]: Connection lost. Attempting to reconnect...
+ - main [ref=e115]:
+ - generic [ref=e271]:
+ - generic [ref=e272]:
+ - generic [ref=e273]:
+ - img [ref=e274]
+ - heading "MCP Tools" [level=1] [ref=e276]
+ - generic [ref=e277]: 0 servers, 0 tools
+ - generic [ref=e278]:
+ - button "Refresh" [ref=e279]:
+ - img [ref=e280]
+ - text: Refresh
+ - button "Add Tool" [ref=e347]:
+ - img [ref=e286]
+ - text: Add Tool
+ - tablist "MCP sections" [ref=e287]:
+ - tab "Servers" [ref=e288]:
+ - img [ref=e289]
+ - text: Servers
+ - tab "Tools" [selected] [ref=e292]:
+ - img [ref=e293]
+ - text: Tools
+ - tabpanel "Tools" [ref=e348]:
+ - generic [ref=e349]:
+ - img [ref=e350]
+ - paragraph [ref=e352]: No tools configured.
+ - paragraph [ref=e353]: Click "Add Tool" to create an MCP tool.
+ - dialog "Add MCP Tool" [ref=e355]:
+ - generic [ref=e356]:
+ - heading "Add MCP Tool" [level=2] [ref=e357]
+ - button "Close dialog" [ref=e358]:
+ - img [ref=e359]
+ - generic [ref=e363]:
+ - generic [ref=e364]:
+ - generic [ref=e365]:
+ - generic [ref=e366]: Key
+ - textbox "Key" [active] [ref=e367]:
+ - /placeholder: brave-search
+ - generic [ref=e368]:
+ - generic [ref=e369]: Name
+ - textbox "Name" [ref=e370]:
+ - /placeholder: brave-search
+ - generic [ref=e371]:
+ - generic [ref=e372]: Description
+ - textbox "Description" [ref=e373]:
+ - /placeholder: What this tool does...
+ - generic [ref=e374]:
+ - generic [ref=e375]:
+ - generic [ref=e376]: MCP Server
+ - textbox "MCP Server" [ref=e377]:
+ - /placeholder: MCP server key
+ - generic [ref=e378]:
+ - generic [ref=e379]: Groups (comma-separated)
+ - textbox "Groups (comma-separated)" [ref=e380]:
+ - /placeholder: default
+ - text: default
+ - generic [ref=e381]:
+ - generic [ref=e382]:
+ - generic [ref=e383]: Arguments
+ - button "Add" [ref=e384]:
+ - img [ref=e385]
+ - text: Add
+ - paragraph [ref=e386]: No arguments defined. Click "Add" to add one.
+ - generic [ref=e387]:
+ - button "Cancel" [ref=e388]
+ - button "Save" [disabled] [ref=e389]
\ No newline at end of file
diff --git a/.playwright-mcp/page-2026-04-12T05-29-17-218Z.yml b/.playwright-mcp/page-2026-04-12T05-29-17-218Z.yml
new file mode 100644
index 00000000..b3369a6e
--- /dev/null
+++ b/.playwright-mcp/page-2026-04-12T05-29-17-218Z.yml
@@ -0,0 +1,156 @@
+- generic [ref=e3]:
+ - link "Skip to content" [ref=e4] [cursor=pointer]:
+ - /url: "#main-content"
+ - complementary "Sidebar" [ref=e5]:
+ - generic [ref=e6]:
+ - img [ref=e7]
+ - generic [ref=e10]: TrustGraph
+ - generic [ref=e13]:
+ - generic [ref=e14]:
+ - generic [ref=e15]:
+ - img [ref=e16]
+ - text: Flow
+ - generic [ref=e20]:
+ - combobox "Flow" [ref=e21]:
+ - option "default" [selected]
+ - img
+ - generic [ref=e22]:
+ - img [ref=e23]
+ - generic [ref=e27]: default
+ - navigation "Main navigation" [ref=e29]:
+ - link "Chat" [ref=e30] [cursor=pointer]:
+ - /url: /chat
+ - generic [ref=e31]:
+ - img [ref=e32]
+ - generic [ref=e34]: Chat
+ - link "Library" [ref=e35] [cursor=pointer]:
+ - /url: /library
+ - generic [ref=e36]:
+ - img [ref=e37]
+ - generic [ref=e40]: Library
+ - link "Graph" [ref=e41] [cursor=pointer]:
+ - /url: /graph
+ - generic [ref=e42]:
+ - img [ref=e43]
+ - generic [ref=e47]: Graph
+ - link "Prompts" [ref=e48] [cursor=pointer]:
+ - /url: /prompts
+ - generic [ref=e49]:
+ - img [ref=e50]
+ - generic [ref=e54]: Prompts
+ - link "Token Cost" [ref=e55] [cursor=pointer]:
+ - /url: /token-cost
+ - generic [ref=e56]:
+ - img [ref=e57]
+ - generic [ref=e62]: Token Cost
+ - link "Knowledge Cores" [ref=e63] [cursor=pointer]:
+ - /url: /knowledge-cores
+ - generic [ref=e64]:
+ - img [ref=e65]
+ - generic [ref=e77]: Knowledge Cores
+ - link "Flows" [ref=e78] [cursor=pointer]:
+ - /url: /flows
+ - generic [ref=e79]:
+ - img [ref=e80]
+ - generic [ref=e84]: Flows
+ - link "MCP Tools" [ref=e266] [cursor=pointer]:
+ - /url: /mcp-tools
+ - generic [ref=e267]:
+ - img [ref=e268]
+ - generic [ref=e270]: MCP Tools
+ - link "Settings" [ref=e85] [cursor=pointer]:
+ - /url: /settings
+ - generic [ref=e86]:
+ - img [ref=e87]
+ - generic [ref=e90]: Settings
+ - generic [ref=e92]:
+ - img [ref=e94]
+ - generic [ref=e101]: reconnecting
+ - generic [ref=e102]:
+ - banner [ref=e103]:
+ - generic [ref=e104]:
+ - generic [ref=e105]:
+ - img [ref=e106]
+ - text: default
+ - generic [ref=e110]:
+ - img [ref=e111]
+ - text: default
+ - alert [ref=e257]:
+ - img [ref=e258]
+ - generic [ref=e265]: Connection lost. Attempting to reconnect...
+ - main [ref=e115]:
+ - generic [ref=e271]:
+ - generic [ref=e272]:
+ - generic [ref=e273]:
+ - img [ref=e274]
+ - heading "MCP Tools" [level=1] [ref=e276]
+ - generic [ref=e277]: 0 servers, 0 tools
+ - generic [ref=e278]:
+ - button "Refresh" [ref=e279]:
+ - img [ref=e280]
+ - text: Refresh
+ - button "Add Tool" [ref=e347]:
+ - img [ref=e286]
+ - text: Add Tool
+ - tablist "MCP sections" [ref=e287]:
+ - tab "Servers" [ref=e288]:
+ - img [ref=e289]
+ - text: Servers
+ - tab "Tools" [selected] [ref=e292]:
+ - img [ref=e293]
+ - text: Tools
+ - tabpanel "Tools" [ref=e348]:
+ - generic [ref=e349]:
+ - img [ref=e350]
+ - paragraph [ref=e352]: No tools configured.
+ - paragraph [ref=e353]: Click "Add Tool" to create an MCP tool.
+ - dialog "Add MCP Tool" [ref=e355]:
+ - generic [ref=e356]:
+ - heading "Add MCP Tool" [level=2] [ref=e357]
+ - button "Close dialog" [ref=e358]:
+ - img [ref=e359]
+ - generic [ref=e363]:
+ - generic [ref=e364]:
+ - generic [ref=e365]:
+ - generic [ref=e366]: Key
+ - textbox "Key" [ref=e367]:
+ - /placeholder: brave-search
+ - generic [ref=e368]:
+ - generic [ref=e369]: Name
+ - textbox "Name" [ref=e370]:
+ - /placeholder: brave-search
+ - generic [ref=e371]:
+ - generic [ref=e372]: Description
+ - textbox "Description" [ref=e373]:
+ - /placeholder: What this tool does...
+ - generic [ref=e374]:
+ - generic [ref=e375]:
+ - generic [ref=e376]: MCP Server
+ - textbox "MCP Server" [ref=e377]:
+ - /placeholder: MCP server key
+ - generic [ref=e378]:
+ - generic [ref=e379]: Groups (comma-separated)
+ - textbox "Groups (comma-separated)" [ref=e380]:
+ - /placeholder: default
+ - text: default
+ - generic [ref=e381]:
+ - generic [ref=e382]:
+ - generic [ref=e383]: Arguments
+ - button "Add" [active] [ref=e384]:
+ - img [ref=e385]
+ - text: Add
+ - generic [ref=e391]:
+ - textbox "Argument 1 name" [ref=e392]:
+ - /placeholder: name
+ - combobox "Argument 1 type" [ref=e393]:
+ - option "string" [selected]
+ - option "number"
+ - option "boolean"
+ - option "object"
+ - textbox "Argument 1 description" [ref=e394]:
+ - /placeholder: description
+ - button "Remove argument 1" [ref=e395]:
+ - img [ref=e396]
+ - generic [ref=e387]:
+ - button "Cancel" [ref=e388]
+ - button "Save" [disabled] [ref=e389]
\ No newline at end of file
diff --git a/.playwright-mcp/page-2026-04-12T05-29-50-589Z.yml b/.playwright-mcp/page-2026-04-12T05-29-50-589Z.yml
new file mode 100644
index 00000000..b692d031
--- /dev/null
+++ b/.playwright-mcp/page-2026-04-12T05-29-50-589Z.yml
@@ -0,0 +1,106 @@
+- generic [ref=e3]:
+ - link "Skip to content" [ref=e4] [cursor=pointer]:
+ - /url: "#main-content"
+ - complementary "Sidebar" [ref=e5]:
+ - generic [ref=e6]:
+ - img [ref=e7]
+ - generic [ref=e10]: TrustGraph
+ - generic [ref=e13]:
+ - generic [ref=e14]:
+ - generic [ref=e15]:
+ - img [ref=e16]
+ - text: Flow
+ - generic [ref=e20]:
+ - combobox "Flow" [ref=e21]:
+ - option "default" [selected]
+ - img
+ - generic [ref=e22]:
+ - img [ref=e23]
+ - generic [ref=e27]: default
+ - navigation "Main navigation" [ref=e29]:
+ - link "Chat" [ref=e30] [cursor=pointer]:
+ - /url: /chat
+ - generic [ref=e31]:
+ - img [ref=e32]
+ - generic [ref=e34]: Chat
+ - link "Library" [ref=e35] [cursor=pointer]:
+ - /url: /library
+ - generic [ref=e36]:
+ - img [ref=e37]
+ - generic [ref=e40]: Library
+ - link "Graph" [ref=e41] [cursor=pointer]:
+ - /url: /graph
+ - generic [ref=e42]:
+ - img [ref=e43]
+ - generic [ref=e47]: Graph
+ - link "Prompts" [ref=e48] [cursor=pointer]:
+ - /url: /prompts
+ - generic [ref=e49]:
+ - img [ref=e50]
+ - generic [ref=e54]: Prompts
+ - link "Token Cost" [ref=e55] [cursor=pointer]:
+ - /url: /token-cost
+ - generic [ref=e56]:
+ - img [ref=e57]
+ - generic [ref=e62]: Token Cost
+ - link "Knowledge Cores" [ref=e63] [cursor=pointer]:
+ - /url: /knowledge-cores
+ - generic [ref=e64]:
+ - img [ref=e65]
+ - generic [ref=e77]: Knowledge Cores
+ - link "Flows" [ref=e78] [cursor=pointer]:
+ - /url: /flows
+ - generic [ref=e79]:
+ - img [ref=e80]
+ - generic [ref=e84]: Flows
+ - link "MCP Tools" [ref=e266] [cursor=pointer]:
+ - /url: /mcp-tools
+ - generic [ref=e267]:
+ - img [ref=e268]
+ - generic [ref=e270]: MCP Tools
+ - link "Settings" [ref=e85] [cursor=pointer]:
+ - /url: /settings
+ - generic [ref=e86]:
+ - img [ref=e87]
+ - generic [ref=e90]: Settings
+ - generic [ref=e92]:
+ - img [ref=e94]
+ - generic [ref=e101]: reconnecting
+ - generic [ref=e102]:
+ - banner [ref=e103]:
+ - generic [ref=e104]:
+ - generic [ref=e105]:
+ - img [ref=e106]
+ - text: default
+ - generic [ref=e110]:
+ - img [ref=e111]
+ - text: default
+ - alert [ref=e257]:
+ - img [ref=e258]
+ - generic [ref=e265]: Connection lost. Attempting to reconnect...
+ - main [ref=e115]:
+ - generic [ref=e271]:
+ - generic [ref=e272]:
+ - generic [ref=e273]:
+ - img [ref=e274]
+ - heading "MCP Tools" [level=1] [ref=e276]
+ - generic [ref=e277]: 0 servers, 0 tools
+ - generic [ref=e278]:
+ - button "Refresh" [ref=e279]:
+ - img [ref=e280]
+ - text: Refresh
+ - button "Add Server" [ref=e399]:
+ - img [ref=e286]
+ - text: Add Server
+ - tablist "MCP sections" [ref=e287]:
+ - tab "Servers" [active] [selected] [ref=e288]:
+ - img [ref=e289]
+ - text: Servers
+ - tab "Tools" [ref=e292]:
+ - img [ref=e293]
+ - text: Tools
+ - tabpanel "Servers" [ref=e400]:
+ - generic [ref=e401]:
+ - img [ref=e402]
+ - paragraph [ref=e405]: No MCP servers configured.
+ - paragraph [ref=e406]: Click "Add Server" to connect an external MCP server.
\ No newline at end of file
diff --git a/.playwright-mcp/page-2026-04-12T05-29-55-753Z.yml b/.playwright-mcp/page-2026-04-12T05-29-55-753Z.yml
new file mode 100644
index 00000000..277293b8
--- /dev/null
+++ b/.playwright-mcp/page-2026-04-12T05-29-55-753Z.yml
@@ -0,0 +1,134 @@
+- generic [ref=e3]:
+ - link "Skip to content" [ref=e4] [cursor=pointer]:
+ - /url: "#main-content"
+ - complementary "Sidebar" [ref=e5]:
+ - generic [ref=e6]:
+ - img [ref=e7]
+ - generic [ref=e10]: TrustGraph
+ - generic [ref=e13]:
+ - generic [ref=e14]:
+ - generic [ref=e15]:
+ - img [ref=e16]
+ - text: Flow
+ - generic [ref=e20]:
+ - combobox "Flow" [ref=e21]:
+ - option "default" [selected]
+ - img
+ - generic [ref=e22]:
+ - img [ref=e23]
+ - generic [ref=e27]: default
+ - navigation "Main navigation" [ref=e29]:
+ - link "Chat" [ref=e30] [cursor=pointer]:
+ - /url: /chat
+ - generic [ref=e31]:
+ - img [ref=e32]
+ - generic [ref=e34]: Chat
+ - link "Library" [ref=e35] [cursor=pointer]:
+ - /url: /library
+ - generic [ref=e36]:
+ - img [ref=e37]
+ - generic [ref=e40]: Library
+ - link "Graph" [ref=e41] [cursor=pointer]:
+ - /url: /graph
+ - generic [ref=e42]:
+ - img [ref=e43]
+ - generic [ref=e47]: Graph
+ - link "Prompts" [ref=e48] [cursor=pointer]:
+ - /url: /prompts
+ - generic [ref=e49]:
+ - img [ref=e50]
+ - generic [ref=e54]: Prompts
+ - link "Token Cost" [ref=e55] [cursor=pointer]:
+ - /url: /token-cost
+ - generic [ref=e56]:
+ - img [ref=e57]
+ - generic [ref=e62]: Token Cost
+ - link "Knowledge Cores" [ref=e63] [cursor=pointer]:
+ - /url: /knowledge-cores
+ - generic [ref=e64]:
+ - img [ref=e65]
+ - generic [ref=e77]: Knowledge Cores
+ - link "Flows" [ref=e78] [cursor=pointer]:
+ - /url: /flows
+ - generic [ref=e79]:
+ - img [ref=e80]
+ - generic [ref=e84]: Flows
+ - link "MCP Tools" [ref=e266] [cursor=pointer]:
+ - /url: /mcp-tools
+ - generic [ref=e267]:
+ - img [ref=e268]
+ - generic [ref=e270]: MCP Tools
+ - link "Settings" [ref=e85] [cursor=pointer]:
+ - /url: /settings
+ - generic [ref=e86]:
+ - img [ref=e87]
+ - generic [ref=e90]: Settings
+ - generic [ref=e92]:
+ - img [ref=e94]
+ - generic [ref=e101]: reconnecting
+ - generic [ref=e102]:
+ - banner [ref=e103]:
+ - generic [ref=e104]:
+ - generic [ref=e105]:
+ - img [ref=e106]
+ - text: default
+ - generic [ref=e110]:
+ - img [ref=e111]
+ - text: default
+ - alert [ref=e257]:
+ - img [ref=e258]
+ - generic [ref=e265]: Connection lost. Attempting to reconnect...
+ - main [ref=e115]:
+ - generic [ref=e271]:
+ - generic [ref=e272]:
+ - generic [ref=e273]:
+ - img [ref=e274]
+ - heading "MCP Tools" [level=1] [ref=e276]
+ - generic [ref=e277]: 0 servers, 0 tools
+ - generic [ref=e278]:
+ - button "Refresh" [ref=e279]:
+ - img [ref=e280]
+ - text: Refresh
+ - button "Add Server" [ref=e399]:
+ - img [ref=e286]
+ - text: Add Server
+ - tablist "MCP sections" [ref=e287]:
+ - tab "Servers" [selected] [ref=e288]:
+ - img [ref=e289]
+ - text: Servers
+ - tab "Tools" [ref=e292]:
+ - img [ref=e293]
+ - text: Tools
+ - tabpanel "Servers" [ref=e400]:
+ - generic [ref=e401]:
+ - img [ref=e402]
+ - paragraph [ref=e405]: No MCP servers configured.
+ - paragraph [ref=e406]: Click "Add Server" to connect an external MCP server.
+ - dialog "Add MCP Server" [ref=e408]:
+ - generic [ref=e409]:
+ - heading "Add MCP Server" [level=2] [ref=e410]
+ - button "Close dialog" [ref=e411]:
+ - img [ref=e412]
+ - generic [ref=e416]:
+ - generic [ref=e417]:
+ - generic [ref=e418]: Key
+ - textbox "Key" [active] [ref=e419]:
+ - /placeholder: brave-search
+ - generic [ref=e420]:
+ - generic [ref=e421]: URL
+ - textbox "URL" [ref=e422]:
+ - /placeholder: http://localhost:8383/mcp
+ - generic [ref=e423]:
+ - generic [ref=e424]: Remote Name (optional)
+ - textbox "Remote Name (optional)" [ref=e425]:
+ - /placeholder: Tool name at the remote server
+ - generic [ref=e426]:
+ - generic [ref=e427]: Auth Token (optional)
+ - generic [ref=e428]:
+ - textbox "Auth Token (optional)" [ref=e429]:
+ - /placeholder: Bearer token for authentication
+ - button "Show auth token" [ref=e430]:
+ - img [ref=e431]
+ - generic [ref=e434]:
+ - button "Cancel" [ref=e435]
+ - button "Save" [disabled] [ref=e436]
\ No newline at end of file
diff --git a/.playwright-mcp/page-2026-04-12T05-31-12-362Z.yml b/.playwright-mcp/page-2026-04-12T05-31-12-362Z.yml
new file mode 100644
index 00000000..569d5d1f
--- /dev/null
+++ b/.playwright-mcp/page-2026-04-12T05-31-12-362Z.yml
@@ -0,0 +1,134 @@
+- generic [ref=e3]:
+ - link "Skip to content" [ref=e4] [cursor=pointer]:
+ - /url: "#main-content"
+ - complementary "Sidebar" [ref=e5]:
+ - generic [ref=e6]:
+ - img [ref=e7]
+ - generic [ref=e10]: TrustGraph
+ - generic [ref=e13]:
+ - generic [ref=e14]:
+ - generic [ref=e15]:
+ - img [ref=e16]
+ - text: Flow
+ - generic [ref=e20]:
+ - combobox "Flow" [ref=e21]:
+ - option "default" [selected]
+ - img
+ - generic [ref=e22]:
+ - img [ref=e23]
+ - generic [ref=e27]: default
+ - navigation "Main navigation" [ref=e29]:
+ - link "Chat" [ref=e30] [cursor=pointer]:
+ - /url: /chat
+ - generic [ref=e31]:
+ - img [ref=e32]
+ - generic [ref=e34]: Chat
+ - link "Library" [ref=e35] [cursor=pointer]:
+ - /url: /library
+ - generic [ref=e36]:
+ - img [ref=e37]
+ - generic [ref=e40]: Library
+ - link "Graph" [ref=e41] [cursor=pointer]:
+ - /url: /graph
+ - generic [ref=e42]:
+ - img [ref=e43]
+ - generic [ref=e47]: Graph
+ - link "Prompts" [ref=e48] [cursor=pointer]:
+ - /url: /prompts
+ - generic [ref=e49]:
+ - img [ref=e50]
+ - generic [ref=e54]: Prompts
+ - link "Token Cost" [ref=e55] [cursor=pointer]:
+ - /url: /token-cost
+ - generic [ref=e56]:
+ - img [ref=e57]
+ - generic [ref=e62]: Token Cost
+ - link "Knowledge Cores" [ref=e63] [cursor=pointer]:
+ - /url: /knowledge-cores
+ - generic [ref=e64]:
+ - img [ref=e65]
+ - generic [ref=e77]: Knowledge Cores
+ - link "Flows" [ref=e78] [cursor=pointer]:
+ - /url: /flows
+ - generic [ref=e79]:
+ - img [ref=e80]
+ - generic [ref=e84]: Flows
+ - link "MCP Tools" [ref=e266] [cursor=pointer]:
+ - /url: /mcp-tools
+ - generic [ref=e267]:
+ - img [ref=e268]
+ - generic [ref=e270]: MCP Tools
+ - link "Settings" [ref=e85] [cursor=pointer]:
+ - /url: /settings
+ - generic [ref=e86]:
+ - img [ref=e87]
+ - generic [ref=e90]: Settings
+ - generic [ref=e92]:
+ - img [ref=e94]
+ - generic [ref=e101]: reconnecting
+ - generic [ref=e102]:
+ - banner [ref=e103]:
+ - generic [ref=e104]:
+ - generic [ref=e105]:
+ - img [ref=e106]
+ - text: default
+ - generic [ref=e110]:
+ - img [ref=e111]
+ - text: default
+ - alert [ref=e257]:
+ - img [ref=e258]
+ - generic [ref=e265]: Connection lost. Attempting to reconnect...
+ - main [ref=e115]:
+ - generic [ref=e271]:
+ - generic [ref=e272]:
+ - generic [ref=e273]:
+ - img [ref=e274]
+ - heading "MCP Tools" [level=1] [ref=e276]
+ - generic [ref=e277]: 0 servers, 0 tools
+ - generic [ref=e278]:
+ - button "Refresh" [ref=e279]:
+ - img [ref=e280]
+ - text: Refresh
+ - button "Add Server" [ref=e399]:
+ - img [ref=e286]
+ - text: Add Server
+ - tablist "MCP sections" [ref=e287]:
+ - tab "Servers" [selected] [ref=e288]:
+ - img [ref=e289]
+ - text: Servers
+ - tab "Tools" [ref=e292]:
+ - img [ref=e293]
+ - text: Tools
+ - tabpanel "Servers" [ref=e400]:
+ - generic [ref=e401]:
+ - img [ref=e402]
+ - paragraph [ref=e405]: No MCP servers configured.
+ - paragraph [ref=e406]: Click "Add Server" to connect an external MCP server.
+ - dialog "Add MCP Server" [ref=e438]:
+ - generic [ref=e439]:
+ - heading "Add MCP Server" [level=2] [ref=e440]
+ - button "Close dialog" [ref=e441]:
+ - img [ref=e442]
+ - generic [ref=e446]:
+ - generic [ref=e447]:
+ - generic [ref=e448]: Key
+ - textbox "Key" [active] [ref=e449]:
+ - /placeholder: brave-search
+ - generic [ref=e450]:
+ - generic [ref=e451]: URL
+ - textbox "URL" [ref=e452]:
+ - /placeholder: http://localhost:8383/mcp
+ - generic [ref=e453]:
+ - generic [ref=e454]: Remote Name (optional)
+ - textbox "Remote Name (optional)" [ref=e455]:
+ - /placeholder: Tool name at the remote server
+ - generic [ref=e456]:
+ - generic [ref=e457]: Auth Token (optional)
+ - generic [ref=e458]:
+ - textbox "Auth Token (optional)" [ref=e459]:
+ - /placeholder: Bearer token for authentication
+ - button "Show auth token" [ref=e460]:
+ - img [ref=e461]
+ - generic [ref=e464]:
+ - button "Cancel" [ref=e465]
+ - button "Save" [disabled] [ref=e466]
\ No newline at end of file
diff --git a/.playwright-mcp/page-2026-04-12T05-31-25-532Z.yml b/.playwright-mcp/page-2026-04-12T05-31-25-532Z.yml
new file mode 100644
index 00000000..af739735
--- /dev/null
+++ b/.playwright-mcp/page-2026-04-12T05-31-25-532Z.yml
@@ -0,0 +1,106 @@
+- generic [ref=e3]:
+ - link "Skip to content" [ref=e4] [cursor=pointer]:
+ - /url: "#main-content"
+ - complementary "Sidebar" [ref=e5]:
+ - generic [ref=e6]:
+ - img [ref=e7]
+ - generic [ref=e10]: TrustGraph
+ - generic [ref=e13]:
+ - generic [ref=e14]:
+ - generic [ref=e15]:
+ - img [ref=e16]
+ - text: Flow
+ - generic [ref=e20]:
+ - combobox "Flow" [ref=e21]:
+ - option "default" [selected]
+ - img
+ - generic [ref=e22]:
+ - img [ref=e23]
+ - generic [ref=e27]: default
+ - navigation "Main navigation" [ref=e29]:
+ - link "Chat" [ref=e30] [cursor=pointer]:
+ - /url: /chat
+ - generic [ref=e31]:
+ - img [ref=e32]
+ - generic [ref=e34]: Chat
+ - link "Library" [ref=e35] [cursor=pointer]:
+ - /url: /library
+ - generic [ref=e36]:
+ - img [ref=e37]
+ - generic [ref=e40]: Library
+ - link "Graph" [ref=e41] [cursor=pointer]:
+ - /url: /graph
+ - generic [ref=e42]:
+ - img [ref=e43]
+ - generic [ref=e47]: Graph
+ - link "Prompts" [ref=e48] [cursor=pointer]:
+ - /url: /prompts
+ - generic [ref=e49]:
+ - img [ref=e50]
+ - generic [ref=e54]: Prompts
+ - link "Token Cost" [ref=e55] [cursor=pointer]:
+ - /url: /token-cost
+ - generic [ref=e56]:
+ - img [ref=e57]
+ - generic [ref=e62]: Token Cost
+ - link "Knowledge Cores" [ref=e63] [cursor=pointer]:
+ - /url: /knowledge-cores
+ - generic [ref=e64]:
+ - img [ref=e65]
+ - generic [ref=e77]: Knowledge Cores
+ - link "Flows" [ref=e78] [cursor=pointer]:
+ - /url: /flows
+ - generic [ref=e79]:
+ - img [ref=e80]
+ - generic [ref=e84]: Flows
+ - link "MCP Tools" [ref=e266] [cursor=pointer]:
+ - /url: /mcp-tools
+ - generic [ref=e267]:
+ - img [ref=e268]
+ - generic [ref=e270]: MCP Tools
+ - link "Settings" [ref=e85] [cursor=pointer]:
+ - /url: /settings
+ - generic [ref=e86]:
+ - img [ref=e87]
+ - generic [ref=e90]: Settings
+ - generic [ref=e92]:
+ - img [ref=e94]
+ - generic [ref=e101]: reconnecting
+ - generic [ref=e102]:
+ - banner [ref=e103]:
+ - generic [ref=e104]:
+ - generic [ref=e105]:
+ - img [ref=e106]
+ - text: default
+ - generic [ref=e110]:
+ - img [ref=e111]
+ - text: default
+ - alert [ref=e257]:
+ - img [ref=e258]
+ - generic [ref=e265]: Connection lost. Attempting to reconnect...
+ - main [ref=e115]:
+ - generic [ref=e271]:
+ - generic [ref=e272]:
+ - generic [ref=e273]:
+ - img [ref=e274]
+ - heading "MCP Tools" [level=1] [ref=e276]
+ - generic [ref=e277]: 0 servers, 0 tools
+ - generic [ref=e278]:
+ - button "Refresh" [ref=e279]:
+ - img [ref=e280]
+ - text: Refresh
+ - button "Add Server" [active] [ref=e399]:
+ - img [ref=e286]
+ - text: Add Server
+ - tablist "MCP sections" [ref=e287]:
+ - tab "Servers" [selected] [ref=e288]:
+ - img [ref=e289]
+ - text: Servers
+ - tab "Tools" [ref=e292]:
+ - img [ref=e293]
+ - text: Tools
+ - tabpanel "Servers" [ref=e400]:
+ - generic [ref=e401]:
+ - img [ref=e402]
+ - paragraph [ref=e405]: No MCP servers configured.
+ - paragraph [ref=e406]: Click "Add Server" to connect an external MCP server.
\ No newline at end of file
diff --git a/.playwright-mcp/page-2026-04-12T05-31-31-420Z.yml b/.playwright-mcp/page-2026-04-12T05-31-31-420Z.yml
new file mode 100644
index 00000000..96ab4056
--- /dev/null
+++ b/.playwright-mcp/page-2026-04-12T05-31-31-420Z.yml
@@ -0,0 +1,136 @@
+- generic [ref=e3]:
+ - link "Skip to content" [ref=e4] [cursor=pointer]:
+ - /url: "#main-content"
+ - complementary "Sidebar" [ref=e5]:
+ - generic [ref=e6]:
+ - img [ref=e7]
+ - generic [ref=e10]: TrustGraph
+ - generic [ref=e13]:
+ - generic [ref=e14]:
+ - generic [ref=e15]:
+ - img [ref=e16]
+ - text: Flow
+ - generic [ref=e20]:
+ - combobox "Flow" [ref=e21]:
+ - option "default" [selected]
+ - img
+ - generic [ref=e22]:
+ - img [ref=e23]
+ - generic [ref=e27]: default
+ - navigation "Main navigation" [ref=e29]:
+ - link "Chat" [ref=e30] [cursor=pointer]:
+ - /url: /chat
+ - generic [ref=e31]:
+ - img [ref=e32]
+ - generic [ref=e34]: Chat
+ - link "Library" [ref=e35] [cursor=pointer]:
+ - /url: /library
+ - generic [ref=e36]:
+ - img [ref=e37]
+ - generic [ref=e40]: Library
+ - link "Graph" [ref=e41] [cursor=pointer]:
+ - /url: /graph
+ - generic [ref=e42]:
+ - img [ref=e43]
+ - generic [ref=e47]: Graph
+ - link "Prompts" [ref=e48] [cursor=pointer]:
+ - /url: /prompts
+ - generic [ref=e49]:
+ - img [ref=e50]
+ - generic [ref=e54]: Prompts
+ - link "Token Cost" [ref=e55] [cursor=pointer]:
+ - /url: /token-cost
+ - generic [ref=e56]:
+ - img [ref=e57]
+ - generic [ref=e62]: Token Cost
+ - link "Knowledge Cores" [ref=e63] [cursor=pointer]:
+ - /url: /knowledge-cores
+ - generic [ref=e64]:
+ - img [ref=e65]
+ - generic [ref=e77]: Knowledge Cores
+ - link "Flows" [ref=e78] [cursor=pointer]:
+ - /url: /flows
+ - generic [ref=e79]:
+ - img [ref=e80]
+ - generic [ref=e84]: Flows
+ - link "MCP Tools" [ref=e266] [cursor=pointer]:
+ - /url: /mcp-tools
+ - generic [ref=e267]:
+ - img [ref=e268]
+ - generic [ref=e270]: MCP Tools
+ - link "Settings" [ref=e85] [cursor=pointer]:
+ - /url: /settings
+ - generic [ref=e86]:
+ - img [ref=e87]
+ - generic [ref=e90]: Settings
+ - generic [ref=e92]:
+ - img [ref=e94]
+ - generic [ref=e101]: reconnecting
+ - generic [ref=e102]:
+ - banner [ref=e103]:
+ - generic [ref=e104]:
+ - generic [ref=e105]:
+ - img [ref=e106]
+ - text: default
+ - generic [ref=e110]:
+ - img [ref=e111]
+ - text: default
+ - alert [ref=e257]:
+ - img [ref=e258]
+ - generic [ref=e265]: Connection lost. Attempting to reconnect...
+ - main [ref=e115]:
+ - generic [ref=e271]:
+ - generic [ref=e272]:
+ - generic [ref=e273]:
+ - img [ref=e274]
+ - heading "MCP Tools" [level=1] [ref=e276]
+ - generic [ref=e277]: 0 servers, 0 tools
+ - generic [ref=e278]:
+ - button "Refresh" [ref=e279]:
+ - img [ref=e280]
+ - text: Refresh
+ - button "Add Server" [ref=e399]:
+ - img [ref=e286]
+ - text: Add Server
+ - tablist "MCP sections" [ref=e287]:
+ - tab "Servers" [selected] [ref=e288]:
+ - img [ref=e289]
+ - text: Servers
+ - tab "Tools" [ref=e292]:
+ - img [ref=e293]
+ - text: Tools
+ - tabpanel "Servers" [ref=e400]:
+ - generic [ref=e401]:
+ - img [ref=e402]
+ - paragraph [ref=e405]: No MCP servers configured.
+ - paragraph [ref=e406]: Click "Add Server" to connect an external MCP server.
+ - dialog "Add MCP Server" [ref=e468]:
+ - generic [ref=e469]:
+ - heading "Add MCP Server" [level=2] [ref=e470]
+ - button "Close dialog" [ref=e471]:
+ - img [ref=e472]
+ - generic [ref=e476]:
+ - generic [ref=e477]:
+ - generic [ref=e478]: Key
+ - textbox "Key" [active] [ref=e479]:
+ - /placeholder: brave-search
+ - text: test-stale-state
+ - generic [ref=e480]:
+ - generic [ref=e481]: URL
+ - textbox "URL" [ref=e482]:
+ - /placeholder: http://localhost:8383/mcp
+ - text: http://example.com
+ - generic [ref=e483]:
+ - generic [ref=e484]: Remote Name (optional)
+ - textbox "Remote Name (optional)" [ref=e485]:
+ - /placeholder: Tool name at the remote server
+ - generic [ref=e486]:
+ - generic [ref=e487]: Auth Token (optional)
+ - generic [ref=e488]:
+ - textbox "Auth Token (optional)" [ref=e489]:
+ - /placeholder: Bearer token for authentication
+ - button "Show auth token" [ref=e490]:
+ - img [ref=e491]
+ - generic [ref=e494]:
+ - button "Cancel" [ref=e495]
+ - button "Save" [ref=e496]
\ No newline at end of file
diff --git a/.playwright-mcp/page-2026-04-12T05-31-48-160Z.yml b/.playwright-mcp/page-2026-04-12T05-31-48-160Z.yml
new file mode 100644
index 00000000..fcbf942b
--- /dev/null
+++ b/.playwright-mcp/page-2026-04-12T05-31-48-160Z.yml
@@ -0,0 +1,136 @@
+- generic [ref=e3]:
+ - link "Skip to content" [ref=e4] [cursor=pointer]:
+ - /url: "#main-content"
+ - complementary "Sidebar" [ref=e5]:
+ - generic [ref=e6]:
+ - img [ref=e7]
+ - generic [ref=e10]: TrustGraph
+ - generic [ref=e13]:
+ - generic [ref=e14]:
+ - generic [ref=e15]:
+ - img [ref=e16]
+ - text: Flow
+ - generic [ref=e20]:
+ - combobox "Flow" [ref=e21]:
+ - option "default" [selected]
+ - img
+ - generic [ref=e22]:
+ - img [ref=e23]
+ - generic [ref=e27]: default
+ - navigation "Main navigation" [ref=e29]:
+ - link "Chat" [ref=e30] [cursor=pointer]:
+ - /url: /chat
+ - generic [ref=e31]:
+ - img [ref=e32]
+ - generic [ref=e34]: Chat
+ - link "Library" [ref=e35] [cursor=pointer]:
+ - /url: /library
+ - generic [ref=e36]:
+ - img [ref=e37]
+ - generic [ref=e40]: Library
+ - link "Graph" [ref=e41] [cursor=pointer]:
+ - /url: /graph
+ - generic [ref=e42]:
+ - img [ref=e43]
+ - generic [ref=e47]: Graph
+ - link "Prompts" [ref=e48] [cursor=pointer]:
+ - /url: /prompts
+ - generic [ref=e49]:
+ - img [ref=e50]
+ - generic [ref=e54]: Prompts
+ - link "Token Cost" [ref=e55] [cursor=pointer]:
+ - /url: /token-cost
+ - generic [ref=e56]:
+ - img [ref=e57]
+ - generic [ref=e62]: Token Cost
+ - link "Knowledge Cores" [ref=e63] [cursor=pointer]:
+ - /url: /knowledge-cores
+ - generic [ref=e64]:
+ - img [ref=e65]
+ - generic [ref=e77]: Knowledge Cores
+ - link "Flows" [ref=e78] [cursor=pointer]:
+ - /url: /flows
+ - generic [ref=e79]:
+ - img [ref=e80]
+ - generic [ref=e84]: Flows
+ - link "MCP Tools" [ref=e266] [cursor=pointer]:
+ - /url: /mcp-tools
+ - generic [ref=e267]:
+ - img [ref=e268]
+ - generic [ref=e270]: MCP Tools
+ - link "Settings" [ref=e85] [cursor=pointer]:
+ - /url: /settings
+ - generic [ref=e86]:
+ - img [ref=e87]
+ - generic [ref=e90]: Settings
+ - generic [ref=e92]:
+ - img [ref=e94]
+ - generic [ref=e101]: reconnecting
+ - generic [ref=e102]:
+ - banner [ref=e103]:
+ - generic [ref=e104]:
+ - generic [ref=e105]:
+ - img [ref=e106]
+ - text: default
+ - generic [ref=e110]:
+ - img [ref=e111]
+ - text: default
+ - alert [ref=e257]:
+ - img [ref=e258]
+ - generic [ref=e265]: Connection lost. Attempting to reconnect...
+ - main [ref=e115]:
+ - generic [ref=e271]:
+ - generic [ref=e272]:
+ - generic [ref=e273]:
+ - img [ref=e274]
+ - heading "MCP Tools" [level=1] [ref=e276]
+ - generic [ref=e277]: 0 servers, 0 tools
+ - generic [ref=e278]:
+ - button "Refresh" [ref=e279]:
+ - img [ref=e280]
+ - text: Refresh
+ - button "Add Server" [ref=e399]:
+ - img [ref=e286]
+ - text: Add Server
+ - tablist "MCP sections" [ref=e287]:
+ - tab "Servers" [selected] [ref=e288]:
+ - img [ref=e289]
+ - text: Servers
+ - tab "Tools" [ref=e292]:
+ - img [ref=e293]
+ - text: Tools
+ - tabpanel "Servers" [ref=e400]:
+ - generic [ref=e401]:
+ - img [ref=e402]
+ - paragraph [ref=e405]: No MCP servers configured.
+ - paragraph [ref=e406]: Click "Add Server" to connect an external MCP server.
+ - dialog "Add MCP Server" [ref=e498]:
+ - generic [ref=e499]:
+ - heading "Add MCP Server" [level=2] [ref=e500]
+ - button "Close dialog" [ref=e501]:
+ - img [ref=e502]
+ - generic [ref=e506]:
+ - generic [ref=e507]:
+ - generic [ref=e508]: Key
+ - textbox "Key" [active] [ref=e509]:
+ - /placeholder: brave-search
+ - text: test-stale-state
+ - generic [ref=e510]:
+ - generic [ref=e511]: URL
+ - textbox "URL" [ref=e512]:
+ - /placeholder: http://localhost:8383/mcp
+ - text: http://example.com
+ - generic [ref=e513]:
+ - generic [ref=e514]: Remote Name (optional)
+ - textbox "Remote Name (optional)" [ref=e515]:
+ - /placeholder: Tool name at the remote server
+ - generic [ref=e516]:
+ - generic [ref=e517]: Auth Token (optional)
+ - generic [ref=e518]:
+ - textbox "Auth Token (optional)" [ref=e519]:
+ - /placeholder: Bearer token for authentication
+ - button "Show auth token" [ref=e520]:
+ - img [ref=e521]
+ - generic [ref=e524]:
+ - button "Cancel" [ref=e525]
+ - button "Save" [ref=e526]
\ No newline at end of file
diff --git a/ai-context/context-graph-demo/.gitignore b/ai-context/context-graph-demo/.gitignore
new file mode 100644
index 00000000..99e677ff
--- /dev/null
+++ b/ai-context/context-graph-demo/.gitignore
@@ -0,0 +1,25 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+*~
diff --git a/ai-context/context-graph-demo/LICENSE b/ai-context/context-graph-demo/LICENSE
new file mode 100644
index 00000000..7e31bd3e
--- /dev/null
+++ b/ai-context/context-graph-demo/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to the Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by the Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding any notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. Please also get an approval
+ from the project maintainers before applying the Apache License
+ to your project.
+
+ Copyright 2026 Knownext Inc.
+ Copyright 2026 Knownext Limited
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/ai-context/context-graph-demo/README.md b/ai-context/context-graph-demo/README.md
new file mode 100644
index 00000000..7e94af46
--- /dev/null
+++ b/ai-context/context-graph-demo/README.md
@@ -0,0 +1,67 @@
+
+# Context Graph Demo
+
+A React application that demonstrates
+[TrustGraph](https://trustgraph.ai/) context graph capabilities
+The demo provides an interactive graph visualisation, natural-language
+querying, explainability views, and ontology browsing — all powered by
+a TrustGraph backend. Load your own data to explore.
+
+See it in action: [Context Graph demo video](https://www.youtube.com/watch?v=sWc7mkhITIo)
+
+## Features
+
+- **Graph view** — interactive force-directed graph of entities and
+ relationships, with domain-based filtering
+- **Query view** — natural-language questions answered by the TrustGraph
+ knowledge graph
+- **Explain view** — step-by-step explainability traces showing how
+ answers were derived
+- **Data view** — browse the raw documents loaded into TrustGraph
+- **Ontology view** — explore the ontology (types and predicates)
+ extracted from the dataset
+
+## Prerequisites
+
+- Node.js (v18+)
+- A running [TrustGraph](https://trustgraph.ai/) instance (tested with
+ TrustGraph 2.1)
+
+## Preparing TrustGraph
+
+This demo requires TrustGraph to be running in ontology mode:
+
+1. Launch a flow using the `ontology` flow blueprint.
+2. Load an OWL ontology into the workbench.
+3. Process your data using the new flow.
+
+## Getting started
+
+Install dependencies:
+
+```bash
+npm install
+```
+
+Start the development server:
+
+```bash
+npm run dev
+```
+
+The Vite dev server proxies `/api/socket` (WebSocket) and other API
+routes to the TrustGraph API gateway at `localhost:8088`. If your
+TrustGraph instance is running on a different host or port, edit the
+proxy targets in `vite.config.js`.
+
+Build for production:
+
+```bash
+npm run build
+```
+
+## License
+
+Copyright 2026 Knownext Inc. and Knownext Limited.
+Licensed under the Apache License 2.0 — see [LICENSE](LICENSE) for
+details.
diff --git a/ai-context/context-graph-demo/eslint.config.js b/ai-context/context-graph-demo/eslint.config.js
new file mode 100644
index 00000000..4fa125da
--- /dev/null
+++ b/ai-context/context-graph-demo/eslint.config.js
@@ -0,0 +1,29 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{js,jsx}'],
+ extends: [
+ js.configs.recommended,
+ reactHooks.configs.flat.recommended,
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ parserOptions: {
+ ecmaVersion: 'latest',
+ ecmaFeatures: { jsx: true },
+ sourceType: 'module',
+ },
+ },
+ rules: {
+ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
+ },
+ },
+])
diff --git a/ai-context/context-graph-demo/index.html b/ai-context/context-graph-demo/index.html
new file mode 100644
index 00000000..3e6d7b10
--- /dev/null
+++ b/ai-context/context-graph-demo/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ TrustGraph Context Graph Demo
+
+
+
+
+
+
+
+
+
diff --git a/ai-context/context-graph-demo/package-lock.json b/ai-context/context-graph-demo/package-lock.json
new file mode 100644
index 00000000..b2309e56
--- /dev/null
+++ b/ai-context/context-graph-demo/package-lock.json
@@ -0,0 +1,3080 @@
+{
+ "name": "retail-intelligence-demo",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "retail-intelligence-demo",
+ "version": "0.0.0",
+ "dependencies": {
+ "@tanstack/react-query": "^5.90.21",
+ "@trustgraph/react-provider": "^1.4.0",
+ "@trustgraph/react-state": "^1.4.6",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.1",
+ "@types/react": "^19.2.7",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^5.1.1",
+ "eslint": "^9.39.1",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.4.24",
+ "globals": "^16.5.0",
+ "typescript": "~5.9.3",
+ "vite": "^7.3.1"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+ "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz",
+ "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz",
+ "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz",
+ "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz",
+ "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz",
+ "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz",
+ "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz",
+ "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz",
+ "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz",
+ "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz",
+ "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz",
+ "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz",
+ "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz",
+ "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz",
+ "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz",
+ "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz",
+ "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz",
+ "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz",
+ "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz",
+ "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz",
+ "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz",
+ "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz",
+ "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz",
+ "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz",
+ "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz",
+ "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz",
+ "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz",
+ "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.7",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.5"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz",
+ "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.14.0",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.1",
+ "minimatch": "^3.1.5",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.39.4",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz",
+ "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.7",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
+ "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
+ "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
+ "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
+ "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
+ "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
+ "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
+ "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
+ "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
+ "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
+ "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
+ "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
+ "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
+ "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
+ "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
+ "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
+ "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
+ "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
+ "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
+ "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
+ "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
+ "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
+ "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
+ "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
+ "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
+ "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@tanstack/query-core": {
+ "version": "5.91.0",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.91.0.tgz",
+ "integrity": "sha512-FYXN8Kk9Q5VKuV6AIVaNwMThSi0nvAtR4X7HQoigf6ePOtFcavJYVIzgFhOVdtbBQtCJE3KimDIMMJM2DR1hjw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-query": {
+ "version": "5.91.0",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.91.0.tgz",
+ "integrity": "sha512-S8FODsDTNv0Ym+o/JVBvA6EWiWVhg6K2Q4qFehZyFKk6uW4H9OPbXl4kyiN9hAly0uHJ/1GEbR6kAI4MZWfjEA==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-core": "5.91.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19"
+ }
+ },
+ "node_modules/@trustgraph/client": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@trustgraph/client/-/client-1.6.0.tgz",
+ "integrity": "sha512-z09X1TNmaRrmZXt4b5aXtJr/D7XjF7Lm7/lpYIPUO0NTJ4QYm2ZDPE/z1bJxpJf29y/Q2A65bWY6+TzPoO9kZQ==",
+ "license": "Apache-2.0",
+ "peer": true
+ },
+ "node_modules/@trustgraph/react-provider": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@trustgraph/react-provider/-/react-provider-1.4.0.tgz",
+ "integrity": "sha512-CRtwrbzEOiKoAekXpjIhz8jbvNbaw6Fm0E2EgLnVp5Jf/6GVfPJ6v3eeqG4Z0AlO23cq8k0j88i+nxGI6llQIQ==",
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "@trustgraph/client": "^1.4.0",
+ "react": "^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@trustgraph/react-state": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@trustgraph/react-state/-/react-state-1.6.0.tgz",
+ "integrity": "sha512-NsTfmbNE0zTz/H0/aMTOYjGvJ89bpuoZP56/n9Jtc/35Bx5sqtRi+ECCCkWvPV1nHMQC7MWtlMtmAfhuZm4Bgw==",
+ "license": "MIT",
+ "dependencies": {
+ "@trustgraph/react-provider": "^1.4.0",
+ "compute-cosine-similarity": "^1.1.0",
+ "uuid": "^11.0.3"
+ },
+ "peerDependencies": {
+ "@tanstack/react-query": "^5.0.0",
+ "react": "^18.0.0 || ^19.0.0",
+ "zustand": "^4.0.0 || ^5.0.0"
+ }
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.14",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
+ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz",
+ "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.29.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-rc.3",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.18.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
+ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.8",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.8.tgz",
+ "integrity": "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001780",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz",
+ "integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/compute-cosine-similarity": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/compute-cosine-similarity/-/compute-cosine-similarity-1.1.0.tgz",
+ "integrity": "sha512-FXhNx0ILLjGi9Z9+lglLzM12+0uoTnYkHm7GiadXDAr0HGVLm25OivUS1B/LPkbzzvlcXz/1EvWg9ZYyJSdhTw==",
+ "dependencies": {
+ "compute-dot": "^1.1.0",
+ "compute-l2norm": "^1.1.0",
+ "validate.io-array": "^1.0.5",
+ "validate.io-function": "^1.0.2"
+ }
+ },
+ "node_modules/compute-dot": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/compute-dot/-/compute-dot-1.1.0.tgz",
+ "integrity": "sha512-L5Ocet4DdMrXboss13K59OK23GXjiSia7+7Ukc7q4Bl+RVpIXK2W9IHMbWDZkh+JUEvJAwOKRaJDiFUa1LTnJg==",
+ "dependencies": {
+ "validate.io-array": "^1.0.3",
+ "validate.io-function": "^1.0.2"
+ }
+ },
+ "node_modules/compute-l2norm": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/compute-l2norm/-/compute-l2norm-1.1.0.tgz",
+ "integrity": "sha512-6EHh1Elj90eU28SXi+h2PLnTQvZmkkHWySpoFz+WOlVNLz3DQoC4ISUHSV9n5jMxPHtKGJ01F4uu2PsXBB8sSg==",
+ "dependencies": {
+ "validate.io-array": "^1.0.3",
+ "validate.io-function": "^1.0.2"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.321",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz",
+ "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/esbuild": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz",
+ "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.27.4",
+ "@esbuild/android-arm": "0.27.4",
+ "@esbuild/android-arm64": "0.27.4",
+ "@esbuild/android-x64": "0.27.4",
+ "@esbuild/darwin-arm64": "0.27.4",
+ "@esbuild/darwin-x64": "0.27.4",
+ "@esbuild/freebsd-arm64": "0.27.4",
+ "@esbuild/freebsd-x64": "0.27.4",
+ "@esbuild/linux-arm": "0.27.4",
+ "@esbuild/linux-arm64": "0.27.4",
+ "@esbuild/linux-ia32": "0.27.4",
+ "@esbuild/linux-loong64": "0.27.4",
+ "@esbuild/linux-mips64el": "0.27.4",
+ "@esbuild/linux-ppc64": "0.27.4",
+ "@esbuild/linux-riscv64": "0.27.4",
+ "@esbuild/linux-s390x": "0.27.4",
+ "@esbuild/linux-x64": "0.27.4",
+ "@esbuild/netbsd-arm64": "0.27.4",
+ "@esbuild/netbsd-x64": "0.27.4",
+ "@esbuild/openbsd-arm64": "0.27.4",
+ "@esbuild/openbsd-x64": "0.27.4",
+ "@esbuild/openharmony-arm64": "0.27.4",
+ "@esbuild/sunos-x64": "0.27.4",
+ "@esbuild/win32-arm64": "0.27.4",
+ "@esbuild/win32-ia32": "0.27.4",
+ "@esbuild/win32-x64": "0.27.4"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.39.4",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz",
+ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.2",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
+ "@eslint/eslintrc": "^3.3.5",
+ "@eslint/js": "9.39.4",
+ "@eslint/plugin-kit": "^0.4.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.5",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz",
+ "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.24.4",
+ "@babel/parser": "^7.24.4",
+ "hermes-parser": "^0.25.1",
+ "zod": "^3.25.0 || ^4.0.0",
+ "zod-validation-error": "^3.5.0 || ^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.4.26",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz",
+ "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": ">=8.40"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "16.5.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz",
+ "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/hermes-estree": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
+ "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/hermes-parser": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
+ "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hermes-estree": "0.25.1"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.36",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz",
+ "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.8",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
+ "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.4",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
+ "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.4",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
+ "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.4"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
+ "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
+ "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.59.0",
+ "@rollup/rollup-android-arm64": "4.59.0",
+ "@rollup/rollup-darwin-arm64": "4.59.0",
+ "@rollup/rollup-darwin-x64": "4.59.0",
+ "@rollup/rollup-freebsd-arm64": "4.59.0",
+ "@rollup/rollup-freebsd-x64": "4.59.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.59.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.59.0",
+ "@rollup/rollup-linux-arm64-musl": "4.59.0",
+ "@rollup/rollup-linux-loong64-gnu": "4.59.0",
+ "@rollup/rollup-linux-loong64-musl": "4.59.0",
+ "@rollup/rollup-linux-ppc64-gnu": "4.59.0",
+ "@rollup/rollup-linux-ppc64-musl": "4.59.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.59.0",
+ "@rollup/rollup-linux-riscv64-musl": "4.59.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-musl": "4.59.0",
+ "@rollup/rollup-openbsd-x64": "4.59.0",
+ "@rollup/rollup-openharmony-arm64": "4.59.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.59.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.59.0",
+ "@rollup/rollup-win32-x64-gnu": "4.59.0",
+ "@rollup/rollup-win32-x64-msvc": "4.59.0",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
+ "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/esm/bin/uuid"
+ }
+ },
+ "node_modules/validate.io-array": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/validate.io-array/-/validate.io-array-1.0.6.tgz",
+ "integrity": "sha512-DeOy7CnPEziggrOO5CZhVKJw6S3Yi7e9e65R1Nl/RTN1vTQKnzjfvks0/8kQ40FP/dsjRAOd4hxmJ7uLa6vxkg==",
+ "license": "MIT"
+ },
+ "node_modules/validate.io-function": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/validate.io-function/-/validate.io-function-1.0.2.tgz",
+ "integrity": "sha512-LlFybRJEriSuBnUhQyG5bwglhh50EpTL2ul23MPIuR1odjO7XaMLFV8vHGwp7AZciFxtYOeiSCT5st+XSPONiQ=="
+ },
+ "node_modules/vite": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
+ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.27.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
+ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-validation-error": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
+ "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.0 || ^4.0.0"
+ }
+ },
+ "node_modules/zustand": {
+ "version": "5.0.12",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz",
+ "integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18.0.0",
+ "immer": ">=9.0.6",
+ "react": ">=18.0.0",
+ "use-sync-external-store": ">=1.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "use-sync-external-store": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/ai-context/context-graph-demo/package.json b/ai-context/context-graph-demo/package.json
new file mode 100644
index 00000000..cc484076
--- /dev/null
+++ b/ai-context/context-graph-demo/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "retail-intelligence-demo",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@tanstack/react-query": "^5.90.21",
+ "@trustgraph/react-provider": "^1.4.0",
+ "@trustgraph/react-state": "^1.4.6",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.1",
+ "@types/react": "^19.2.7",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^5.1.1",
+ "eslint": "^9.39.1",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.4.24",
+ "globals": "^16.5.0",
+ "typescript": "~5.9.3",
+ "vite": "^7.3.1"
+ }
+}
diff --git a/ai-context/context-graph-demo/public/tg.svg b/ai-context/context-graph-demo/public/tg.svg
new file mode 100644
index 00000000..7123ae45
--- /dev/null
+++ b/ai-context/context-graph-demo/public/tg.svg
@@ -0,0 +1,83 @@
+
+
diff --git a/ai-context/context-graph-demo/retail.json b/ai-context/context-graph-demo/retail.json
new file mode 100644
index 00000000..565ab45c
--- /dev/null
+++ b/ai-context/context-graph-demo/retail.json
@@ -0,0 +1 @@
+{"metadata":{"name":"TrustGraph Retail Intelligence Ontology","description":"Ontology for retail ecosystem modeling: consumers, brands, retail channels, and AI agents","version":"1.0","created":"2026-02-25T13:38:28.636Z","modified":"2026-02-25T13:38:28.636Z","creator":"","namespace":"http://trustgraph.ai/retail#","namespaces":{"owl":"http://www.w3.org/2002/07/owl#","rdf":"http://www.w3.org/1999/02/22-rdf-syntax-ns#","rdfs":"http://www.w3.org/2000/01/rdf-schema#","xsd":"http://www.w3.org/2001/XMLSchema#","":"http://trustgraph.ai/retail#"}},"classes":{"retail#Consumer":{"uri":"http://trustgraph.ai/retail#Consumer","type":"owl:Class","rdfs:label":[{"value":"Consumer","lang":"en"}],"rdfs:comment":"Individuals and segments interacting with brands through retail channels"},"retail#Brand":{"uri":"http://trustgraph.ai/retail#Brand","type":"owl:Class","rdfs:label":[{"value":"Brand","lang":"en"}],"rdfs:comment":"Product brands seeking to connect with consumers through retail experiences"},"retail#Retail":{"uri":"http://trustgraph.ai/retail#Retail","type":"owl:Class","rdfs:label":[{"value":"Retail","lang":"en"}],"rdfs:comment":"Channels, touchpoints, and experiences where brands meet consumers"},"retail#Agent":{"uri":"http://trustgraph.ai/retail#Agent","type":"owl:Class","rdfs:label":[{"value":"Agent","lang":"en"}],"rdfs:comment":"AI agents that orchestrate personalized brand-consumer connections"}},"objectProperties":{"retail#hasAffinityFor":{"uri":"http://trustgraph.ai/retail#hasAffinityFor","type":"owl:ObjectProperty","rdfs:label":[{"value":"has affinity for","lang":"en"}],"rdfs:domain":"retail#Consumer","rdfs:range":"retail#Brand"},"retail#frequents":{"uri":"http://trustgraph.ai/retail#frequents","type":"owl:ObjectProperty","rdfs:label":[{"value":"frequents","lang":"en"}],"rdfs:domain":"retail#Consumer","rdfs:range":"retail#Brand"},"retail#purchasesFrom":{"uri":"http://trustgraph.ai/retail#purchasesFrom","type":"owl:ObjectProperty","rdfs:label":[{"value":"purchases from","lang":"en"}],"rdfs:domain":"retail#Consumer","rdfs:range":"retail#Brand"},"retail#advocatesFor":{"uri":"http://trustgraph.ai/retail#advocatesFor","type":"owl:ObjectProperty","rdfs:label":[{"value":"advocates for","lang":"en"}],"rdfs:domain":"retail#Consumer","rdfs:range":"retail#Brand"},"retail#loyalTo":{"uri":"http://trustgraph.ai/retail#loyalTo","type":"owl:ObjectProperty","rdfs:label":[{"value":"loyal to","lang":"en"}],"rdfs:domain":"retail#Consumer","rdfs:range":"retail#Brand"},"retail#shopsVia":{"uri":"http://trustgraph.ai/retail#shopsVia","type":"owl:ObjectProperty","rdfs:label":[{"value":"shops via","lang":"en"}],"rdfs:domain":"retail#Consumer","rdfs:range":"retail#Retail"},"retail#discoversThrough":{"uri":"http://trustgraph.ai/retail#discoversThrough","type":"owl:ObjectProperty","rdfs:label":[{"value":"discovers through","lang":"en"}],"rdfs:domain":"retail#Consumer","rdfs:range":"retail#Retail"},"retail#experiences":{"uri":"http://trustgraph.ai/retail#experiences","type":"owl:ObjectProperty","rdfs:label":[{"value":"experiences","lang":"en"}],"rdfs:domain":"retail#Consumer","rdfs:range":"retail#Retail"},"retail#memberOf":{"uri":"http://trustgraph.ai/retail#memberOf","type":"owl:ObjectProperty","rdfs:label":[{"value":"member of","lang":"en"}],"rdfs:domain":"retail#Consumer","rdfs:range":"retail#Retail"},"retail#merchandisesIn":{"uri":"http://trustgraph.ai/retail#merchandisesIn","type":"owl:ObjectProperty","rdfs:label":[{"value":"merchandises in","lang":"en"}],"rdfs:domain":"retail#Brand","rdfs:range":"retail#Retail"},"retail#activatesVia":{"uri":"http://trustgraph.ai/retail#activatesVia","type":"owl:ObjectProperty","rdfs:label":[{"value":"activates via","lang":"en"}],"rdfs:domain":"retail#Brand","rdfs:range":"retail#Retail"},"retail#promotesOn":{"uri":"http://trustgraph.ai/retail#promotesOn","type":"owl:ObjectProperty","rdfs:label":[{"value":"promotes on","lang":"en"}],"rdfs:domain":"retail#Brand","rdfs:range":"retail#Retail"},"retail#sellsThrough":{"uri":"http://trustgraph.ai/retail#sellsThrough","type":"owl:ObjectProperty","rdfs:label":[{"value":"sells through","lang":"en"}],"rdfs:domain":"retail#Brand","rdfs:range":"retail#Retail"},"retail#rewardsVia":{"uri":"http://trustgraph.ai/retail#rewardsVia","type":"owl:ObjectProperty","rdfs:label":[{"value":"rewards via","lang":"en"}],"rdfs:domain":"retail#Brand","rdfs:range":"retail#Retail"},"retail#recommendsTo":{"uri":"http://trustgraph.ai/retail#recommendsTo","type":"owl:ObjectProperty","rdfs:label":[{"value":"recommends to","lang":"en"}],"rdfs:domain":"retail#Agent","rdfs:range":"retail#Consumer"},"retail#personalizesFor":{"uri":"http://trustgraph.ai/retail#personalizesFor","type":"owl:ObjectProperty","rdfs:label":[{"value":"personalizes for","lang":"en"}],"rdfs:domain":"retail#Agent","rdfs:range":"retail#Consumer"},"retail#monitorsSentimentOf":{"uri":"http://trustgraph.ai/retail#monitorsSentimentOf","type":"owl:ObjectProperty","rdfs:label":[{"value":"monitors sentiment of","lang":"en"}],"rdfs:domain":"retail#Agent","rdfs:range":"retail#Consumer"},"retail#optimizesJourneyFor":{"uri":"http://trustgraph.ai/retail#optimizesJourneyFor","type":"owl:ObjectProperty","rdfs:label":[{"value":"optimizes journey for","lang":"en"}],"rdfs:domain":"retail#Agent","rdfs:range":"retail#Consumer"},"retail#orchestratesCampaignFor":{"uri":"http://trustgraph.ai/retail#orchestratesCampaignFor","type":"owl:ObjectProperty","rdfs:label":[{"value":"orchestrates campaign for","lang":"en"}],"rdfs:domain":"retail#Agent","rdfs:range":"retail#Brand"},"retail#analyzesPerceptionOf":{"uri":"http://trustgraph.ai/retail#analyzesPerceptionOf","type":"owl:ObjectProperty","rdfs:label":[{"value":"analyzes perception of","lang":"en"}],"rdfs:domain":"retail#Agent","rdfs:range":"retail#Brand"},"retail#curatesProductsFor":{"uri":"http://trustgraph.ai/retail#curatesProductsFor","type":"owl:ObjectProperty","rdfs:label":[{"value":"curates products for","lang":"en"}],"rdfs:domain":"retail#Agent","rdfs:range":"retail#Brand"},"retail#tailorsExperienceAt":{"uri":"http://trustgraph.ai/retail#tailorsExperienceAt","type":"owl:ObjectProperty","rdfs:label":[{"value":"tailors experience at","lang":"en"}],"rdfs:domain":"retail#Agent","rdfs:range":"retail#Retail"},"retail#deploysCampaignAt":{"uri":"http://trustgraph.ai/retail#deploysCampaignAt","type":"owl:ObjectProperty","rdfs:label":[{"value":"deploys campaign at","lang":"en"}],"rdfs:domain":"retail#Agent","rdfs:range":"retail#Retail"},"retail#optimizesFlowAt":{"uri":"http://trustgraph.ai/retail#optimizesFlowAt","type":"owl:ObjectProperty","rdfs:label":[{"value":"optimizes flow at","lang":"en"}],"rdfs:domain":"retail#Agent","rdfs:range":"retail#Retail"}},"datatypeProperties":{"retail#segment":{"uri":"http://trustgraph.ai/retail#segment","type":"owl:DatatypeProperty","rdfs:range":"xsd:string","rdfs:label":[{"value":"segment","lang":"en"}],"rdfs:domain":"retail#Consumer"},"retail#preferences":{"uri":"http://trustgraph.ai/retail#preferences","type":"owl:DatatypeProperty","rdfs:range":"xsd:string","rdfs:label":[{"value":"preferences","lang":"en"}],"rdfs:domain":"retail#Consumer"},"retail#journeyStage":{"uri":"http://trustgraph.ai/retail#journeyStage","type":"owl:DatatypeProperty","rdfs:range":"xsd:string","rdfs:label":[{"value":"journey stage","lang":"en"}],"rdfs:domain":"retail#Consumer"},"retail#lifetimeValue":{"uri":"http://trustgraph.ai/retail#lifetimeValue","type":"owl:DatatypeProperty","rdfs:range":"xsd:decimal","rdfs:label":[{"value":"lifetime value","lang":"en"}],"rdfs:domain":"retail#Consumer"},"retail#sentiment":{"uri":"http://trustgraph.ai/retail#sentiment","type":"owl:DatatypeProperty","rdfs:range":"xsd:decimal","rdfs:label":[{"value":"sentiment","lang":"en"}],"rdfs:domain":"retail#Consumer"},"retail#size":{"uri":"http://trustgraph.ai/retail#size","type":"owl:DatatypeProperty","rdfs:range":"xsd:string","rdfs:label":[{"value":"size","lang":"en"}],"rdfs:domain":"retail#Consumer"},"retail#avgSpend":{"uri":"http://trustgraph.ai/retail#avgSpend","type":"owl:DatatypeProperty","rdfs:range":"xsd:string","rdfs:label":[{"value":"average spend","lang":"en"}],"rdfs:domain":"retail#Consumer"},"retail#loyalty":{"uri":"http://trustgraph.ai/retail#loyalty","type":"owl:DatatypeProperty","rdfs:range":"xsd:decimal","rdfs:label":[{"value":"loyalty","lang":"en"}],"rdfs:domain":"retail#Consumer"},"retail#identity":{"uri":"http://trustgraph.ai/retail#identity","type":"owl:DatatypeProperty","rdfs:range":"xsd:string","rdfs:label":[{"value":"identity","lang":"en"}],"rdfs:domain":"retail#Brand"},"retail#positioning":{"uri":"http://trustgraph.ai/retail#positioning","type":"owl:DatatypeProperty","rdfs:range":"xsd:string","rdfs:label":[{"value":"positioning","lang":"en"}],"rdfs:domain":"retail#Brand"},"retail#campaigns":{"uri":"http://trustgraph.ai/retail#campaigns","type":"owl:DatatypeProperty","rdfs:range":"xsd:integer","rdfs:label":[{"value":"campaigns","lang":"en"}],"rdfs:domain":"retail#Brand"},"retail#products":{"uri":"http://trustgraph.ai/retail#products","type":"owl:DatatypeProperty","rdfs:range":"xsd:string","rdfs:label":[{"value":"products","lang":"en"}],"rdfs:domain":"retail#Brand"},"retail#partnerships":{"uri":"http://trustgraph.ai/retail#partnerships","type":"owl:DatatypeProperty","rdfs:range":"xsd:string","rdfs:label":[{"value":"partnerships","lang":"en"}],"rdfs:domain":"retail#Brand"},"retail#category":{"uri":"http://trustgraph.ai/retail#category","type":"owl:DatatypeProperty","rdfs:range":"xsd:string","rdfs:label":[{"value":"category","lang":"en"}],"rdfs:domain":"retail#Brand"},"retail#channel":{"uri":"http://trustgraph.ai/retail#channel","type":"owl:DatatypeProperty","rdfs:range":"xsd:string","rdfs:label":[{"value":"channel","lang":"en"}],"rdfs:domain":"retail#Retail"},"retail#location":{"uri":"http://trustgraph.ai/retail#location","type":"owl:DatatypeProperty","rdfs:range":"xsd:string","rdfs:label":[{"value":"location","lang":"en"}],"rdfs:domain":"retail#Retail"},"retail#traffic":{"uri":"http://trustgraph.ai/retail#traffic","type":"owl:DatatypeProperty","rdfs:range":"xsd:string","rdfs:label":[{"value":"traffic","lang":"en"}],"rdfs:domain":"retail#Retail"},"retail#conversionRate":{"uri":"http://trustgraph.ai/retail#conversionRate","type":"owl:DatatypeProperty","rdfs:range":"xsd:string","rdfs:label":[{"value":"conversion rate","lang":"en"}],"rdfs:domain":"retail#Retail"},"retail#experienceScore":{"uri":"http://trustgraph.ai/retail#experienceScore","type":"owl:DatatypeProperty","rdfs:range":"xsd:decimal","rdfs:label":[{"value":"experience score","lang":"en"}],"rdfs:domain":"retail#Retail"},"retail#capability":{"uri":"http://trustgraph.ai/retail#capability","type":"owl:DatatypeProperty","rdfs:range":"xsd:string","rdfs:label":[{"value":"capability","lang":"en"}],"rdfs:domain":"retail#Agent"},"retail#contextSources":{"uri":"http://trustgraph.ai/retail#contextSources","type":"owl:DatatypeProperty","rdfs:range":"xsd:string","rdfs:label":[{"value":"context sources","lang":"en"}],"rdfs:domain":"retail#Agent"},"retail#accuracy":{"uri":"http://trustgraph.ai/retail#accuracy","type":"owl:DatatypeProperty","rdfs:range":"xsd:string","rdfs:label":[{"value":"accuracy","lang":"en"}],"rdfs:domain":"retail#Agent"},"retail#latency":{"uri":"http://trustgraph.ai/retail#latency","type":"owl:DatatypeProperty","rdfs:range":"xsd:string","rdfs:label":[{"value":"latency","lang":"en"}],"rdfs:domain":"retail#Agent"},"retail#decisionsPerDay":{"uri":"http://trustgraph.ai/retail#decisionsPerDay","type":"owl:DatatypeProperty","rdfs:range":"xsd:string","rdfs:label":[{"value":"decisions per day","lang":"en"}],"rdfs:domain":"retail#Agent"}}}
diff --git a/ai-context/context-graph-demo/retail.ttl b/ai-context/context-graph-demo/retail.ttl
new file mode 100644
index 00000000..165f3568
--- /dev/null
+++ b/ai-context/context-graph-demo/retail.ttl
@@ -0,0 +1,310 @@
+@prefix owl: .
+@prefix rdf: .
+@prefix rdfs: .
+@prefix xsd: .
+@prefix : .
+
+# Ontology declaration
+ a owl:Ontology ;
+ rdfs:label "TrustGraph Retail Intelligence Ontology" ;
+ rdfs:comment "Ontology for retail ecosystem modeling: consumers, brands, retail channels, and AI agents" .
+
+# =============================================================================
+# Classes
+# =============================================================================
+
+:Consumer a owl:Class ;
+ rdfs:label "Consumer" ;
+ rdfs:comment "Individuals and segments interacting with brands through retail channels" .
+
+:Brand a owl:Class ;
+ rdfs:label "Brand" ;
+ rdfs:comment "Product brands seeking to connect with consumers through retail experiences" .
+
+:Retail a owl:Class ;
+ rdfs:label "Retail" ;
+ rdfs:comment "Channels, touchpoints, and experiences where brands meet consumers" .
+
+:Agent a owl:Class ;
+ rdfs:label "Agent" ;
+ rdfs:comment "AI agents that orchestrate personalized brand-consumer connections" .
+
+# =============================================================================
+# Datatype Properties - Consumer
+# =============================================================================
+
+:segment a owl:DatatypeProperty ;
+ rdfs:label "segment" ;
+ rdfs:domain :Consumer ;
+ rdfs:range xsd:string .
+
+:preferences a owl:DatatypeProperty ;
+ rdfs:label "preferences" ;
+ rdfs:domain :Consumer ;
+ rdfs:range xsd:string .
+
+:journeyStage a owl:DatatypeProperty ;
+ rdfs:label "journey stage" ;
+ rdfs:domain :Consumer ;
+ rdfs:range xsd:string .
+
+:lifetimeValue a owl:DatatypeProperty ;
+ rdfs:label "lifetime value" ;
+ rdfs:domain :Consumer ;
+ rdfs:range xsd:decimal .
+
+:sentiment a owl:DatatypeProperty ;
+ rdfs:label "sentiment" ;
+ rdfs:domain :Consumer ;
+ rdfs:range xsd:decimal .
+
+:size a owl:DatatypeProperty ;
+ rdfs:label "size" ;
+ rdfs:domain :Consumer ;
+ rdfs:range xsd:string .
+
+:avgSpend a owl:DatatypeProperty ;
+ rdfs:label "average spend" ;
+ rdfs:domain :Consumer ;
+ rdfs:range xsd:string .
+
+:loyalty a owl:DatatypeProperty ;
+ rdfs:label "loyalty" ;
+ rdfs:domain :Consumer ;
+ rdfs:range xsd:decimal .
+
+# =============================================================================
+# Datatype Properties - Brand
+# =============================================================================
+
+:identity a owl:DatatypeProperty ;
+ rdfs:label "identity" ;
+ rdfs:domain :Brand ;
+ rdfs:range xsd:string .
+
+:positioning a owl:DatatypeProperty ;
+ rdfs:label "positioning" ;
+ rdfs:domain :Brand ;
+ rdfs:range xsd:string .
+
+:campaigns a owl:DatatypeProperty ;
+ rdfs:label "campaigns" ;
+ rdfs:domain :Brand ;
+ rdfs:range xsd:integer .
+
+:products a owl:DatatypeProperty ;
+ rdfs:label "products" ;
+ rdfs:domain :Brand ;
+ rdfs:range xsd:string .
+
+:partnerships a owl:DatatypeProperty ;
+ rdfs:label "partnerships" ;
+ rdfs:domain :Brand ;
+ rdfs:range xsd:string .
+
+:category a owl:DatatypeProperty ;
+ rdfs:label "category" ;
+ rdfs:domain :Brand ;
+ rdfs:range xsd:string .
+
+# =============================================================================
+# Datatype Properties - Retail
+# =============================================================================
+
+:channel a owl:DatatypeProperty ;
+ rdfs:label "channel" ;
+ rdfs:domain :Retail ;
+ rdfs:range xsd:string .
+
+:location a owl:DatatypeProperty ;
+ rdfs:label "location" ;
+ rdfs:domain :Retail ;
+ rdfs:range xsd:string .
+
+:traffic a owl:DatatypeProperty ;
+ rdfs:label "traffic" ;
+ rdfs:domain :Retail ;
+ rdfs:range xsd:string .
+
+:conversionRate a owl:DatatypeProperty ;
+ rdfs:label "conversion rate" ;
+ rdfs:domain :Retail ;
+ rdfs:range xsd:string .
+
+:experienceScore a owl:DatatypeProperty ;
+ rdfs:label "experience score" ;
+ rdfs:domain :Retail ;
+ rdfs:range xsd:decimal .
+
+# =============================================================================
+# Datatype Properties - Agent
+# =============================================================================
+
+:capability a owl:DatatypeProperty ;
+ rdfs:label "capability" ;
+ rdfs:domain :Agent ;
+ rdfs:range xsd:string .
+
+:contextSources a owl:DatatypeProperty ;
+ rdfs:label "context sources" ;
+ rdfs:domain :Agent ;
+ rdfs:range xsd:string .
+
+:accuracy a owl:DatatypeProperty ;
+ rdfs:label "accuracy" ;
+ rdfs:domain :Agent ;
+ rdfs:range xsd:string .
+
+:latency a owl:DatatypeProperty ;
+ rdfs:label "latency" ;
+ rdfs:domain :Agent ;
+ rdfs:range xsd:string .
+
+:decisionsPerDay a owl:DatatypeProperty ;
+ rdfs:label "decisions per day" ;
+ rdfs:domain :Agent ;
+ rdfs:range xsd:string .
+
+# =============================================================================
+# Object Properties - Consumer <-> Brand
+# =============================================================================
+
+:hasAffinityFor a owl:ObjectProperty ;
+ rdfs:label "has affinity for" ;
+ rdfs:domain :Consumer ;
+ rdfs:range :Brand .
+
+:frequents a owl:ObjectProperty ;
+ rdfs:label "frequents" ;
+ rdfs:domain :Consumer ;
+ rdfs:range :Brand .
+
+:purchasesFrom a owl:ObjectProperty ;
+ rdfs:label "purchases from" ;
+ rdfs:domain :Consumer ;
+ rdfs:range :Brand .
+
+:advocatesFor a owl:ObjectProperty ;
+ rdfs:label "advocates for" ;
+ rdfs:domain :Consumer ;
+ rdfs:range :Brand .
+
+:loyalTo a owl:ObjectProperty ;
+ rdfs:label "loyal to" ;
+ rdfs:domain :Consumer ;
+ rdfs:range :Brand .
+
+# =============================================================================
+# Object Properties - Consumer <-> Retail
+# =============================================================================
+
+:shopsVia a owl:ObjectProperty ;
+ rdfs:label "shops via" ;
+ rdfs:domain :Consumer ;
+ rdfs:range :Retail .
+
+:discoversThrough a owl:ObjectProperty ;
+ rdfs:label "discovers through" ;
+ rdfs:domain :Consumer ;
+ rdfs:range :Retail .
+
+:experiences a owl:ObjectProperty ;
+ rdfs:label "experiences" ;
+ rdfs:domain :Consumer ;
+ rdfs:range :Retail .
+
+:memberOf a owl:ObjectProperty ;
+ rdfs:label "member of" ;
+ rdfs:domain :Consumer ;
+ rdfs:range :Retail .
+
+# =============================================================================
+# Object Properties - Brand <-> Retail
+# =============================================================================
+
+:merchandisesIn a owl:ObjectProperty ;
+ rdfs:label "merchandises in" ;
+ rdfs:domain :Brand ;
+ rdfs:range :Retail .
+
+:activatesVia a owl:ObjectProperty ;
+ rdfs:label "activates via" ;
+ rdfs:domain :Brand ;
+ rdfs:range :Retail .
+
+:promotesOn a owl:ObjectProperty ;
+ rdfs:label "promotes on" ;
+ rdfs:domain :Brand ;
+ rdfs:range :Retail .
+
+:sellsThrough a owl:ObjectProperty ;
+ rdfs:label "sells through" ;
+ rdfs:domain :Brand ;
+ rdfs:range :Retail .
+
+:rewardsVia a owl:ObjectProperty ;
+ rdfs:label "rewards via" ;
+ rdfs:domain :Brand ;
+ rdfs:range :Retail .
+
+# =============================================================================
+# Object Properties - Agent <-> Consumer
+# =============================================================================
+
+:recommendsTo a owl:ObjectProperty ;
+ rdfs:label "recommends to" ;
+ rdfs:domain :Agent ;
+ rdfs:range :Consumer .
+
+:personalizesFor a owl:ObjectProperty ;
+ rdfs:label "personalizes for" ;
+ rdfs:domain :Agent ;
+ rdfs:range :Consumer .
+
+:monitorsSentimentOf a owl:ObjectProperty ;
+ rdfs:label "monitors sentiment of" ;
+ rdfs:domain :Agent ;
+ rdfs:range :Consumer .
+
+:optimizesJourneyFor a owl:ObjectProperty ;
+ rdfs:label "optimizes journey for" ;
+ rdfs:domain :Agent ;
+ rdfs:range :Consumer .
+
+# =============================================================================
+# Object Properties - Agent <-> Brand
+# =============================================================================
+
+:orchestratesCampaignFor a owl:ObjectProperty ;
+ rdfs:label "orchestrates campaign for" ;
+ rdfs:domain :Agent ;
+ rdfs:range :Brand .
+
+:analyzesPerceptionOf a owl:ObjectProperty ;
+ rdfs:label "analyzes perception of" ;
+ rdfs:domain :Agent ;
+ rdfs:range :Brand .
+
+:curatesProductsFor a owl:ObjectProperty ;
+ rdfs:label "curates products for" ;
+ rdfs:domain :Agent ;
+ rdfs:range :Brand .
+
+# =============================================================================
+# Object Properties - Agent <-> Retail
+# =============================================================================
+
+:tailorsExperienceAt a owl:ObjectProperty ;
+ rdfs:label "tailors experience at" ;
+ rdfs:domain :Agent ;
+ rdfs:range :Retail .
+
+:deploysCampaignAt a owl:ObjectProperty ;
+ rdfs:label "deploys campaign at" ;
+ rdfs:domain :Agent ;
+ rdfs:range :Retail .
+
+:optimizesFlowAt a owl:ObjectProperty ;
+ rdfs:label "optimizes flow at" ;
+ rdfs:domain :Agent ;
+ rdfs:range :Retail .
diff --git a/ai-context/context-graph-demo/src/App.tsx b/ai-context/context-graph-demo/src/App.tsx
new file mode 100644
index 00000000..891f8fb1
--- /dev/null
+++ b/ai-context/context-graph-demo/src/App.tsx
@@ -0,0 +1,56 @@
+import { useState, useEffect } from "react";
+import type { TabKey, DomainKey, Entity } from "./types";
+import { Header, StatusBar, Toaster } from "./components";
+import { GraphView, QueryView, ExplainView, DataView, OntologyView } from "./pages";
+import { useGraphData, toast } from "./state";
+
+export default function App() {
+ const [activeTab, setActiveTab] = useState("graph");
+ const [activeFilter, setActiveFilter] = useState(null);
+ const [selectedNode, setSelectedNode] = useState(null);
+ const { entities, isLoading } = useGraphData();
+
+ // Notification when graph loads
+ useEffect(() => {
+ if (!isLoading && entities.length > 0) {
+ toast.success(`Graph loaded: ${entities.length} entities`);
+ }
+ }, [isLoading, entities.length]);
+
+ const handleTabChange = (tab: TabKey) => {
+ setActiveTab(tab);
+ if (tab !== "graph") {
+ setSelectedNode(null);
+ }
+ };
+
+ return (
+
+
+
+ {activeTab === "graph" && (
+
+ )}
+
+ {activeTab === "query" && }
+
+ {activeTab === "explain" && }
+
+ {activeTab === "data" && }
+
+ {activeTab === "ontology" && }
+
+
+
+
+ );
+}
diff --git a/ai-context/context-graph-demo/src/assets/react.svg b/ai-context/context-graph-demo/src/assets/react.svg
new file mode 100644
index 00000000..6c87de9b
--- /dev/null
+++ b/ai-context/context-graph-demo/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ai-context/context-graph-demo/src/components/common/Badge.tsx b/ai-context/context-graph-demo/src/components/common/Badge.tsx
new file mode 100644
index 00000000..d5a0836e
--- /dev/null
+++ b/ai-context/context-graph-demo/src/components/common/Badge.tsx
@@ -0,0 +1,39 @@
+interface BadgeProps {
+ children: React.ReactNode;
+ color: string;
+ size?: "small" | "medium";
+ selected?: boolean;
+ onClick?: () => void;
+}
+
+export function Badge({
+ children,
+ color,
+ size = "medium",
+ selected = false,
+ onClick,
+}: BadgeProps) {
+ const isSmall = size === "small";
+
+ return (
+
+ );
+}
diff --git a/ai-context/context-graph-demo/src/components/common/Card.tsx b/ai-context/context-graph-demo/src/components/common/Card.tsx
new file mode 100644
index 00000000..89b17156
--- /dev/null
+++ b/ai-context/context-graph-demo/src/components/common/Card.tsx
@@ -0,0 +1,35 @@
+import { surface, border } from "../../theme";
+
+interface CardProps {
+ children: React.ReactNode;
+ padding?: number | string;
+ borderRadius?: number;
+ borderColor?: string;
+ onClick?: () => void;
+ style?: React.CSSProperties;
+}
+
+export function Card({
+ children,
+ padding = 24,
+ borderRadius = 12,
+ borderColor = border.subtle,
+ onClick,
+ style,
+}: CardProps) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/ai-context/context-graph-demo/src/components/common/FilterBar.tsx b/ai-context/context-graph-demo/src/components/common/FilterBar.tsx
new file mode 100644
index 00000000..01a7f63c
--- /dev/null
+++ b/ai-context/context-graph-demo/src/components/common/FilterBar.tsx
@@ -0,0 +1,78 @@
+import { FilterButton } from "./FilterButton";
+import { text, border } from "../../theme";
+
+export interface FilterItem {
+ key: string;
+ label: string;
+ icon?: string;
+ color?: string;
+}
+
+interface FilterBarProps {
+ items: FilterItem[];
+ selectedKey: string | null;
+ onSelect: (key: string | null) => void;
+ stats?: string;
+ showAll?: boolean;
+ allLabel?: string;
+ emptyMessage?: string;
+ maxItems?: number;
+}
+
+export function FilterBar({
+ items,
+ selectedKey,
+ onSelect,
+ stats,
+ showAll = true,
+ allLabel = "All",
+ emptyMessage,
+ maxItems = 10,
+}: FilterBarProps) {
+ const displayItems = items.slice(0, maxItems);
+
+ return (
+
+
+ FILTER:
+
+
+ {emptyMessage && items.length === 0 ? (
+
{emptyMessage}
+ ) : (
+ <>
+ {showAll && (
+
onSelect(null)}
+ />
+ )}
+ {displayItems.map((item) => (
+ onSelect(selectedKey === item.key ? null : item.key)}
+ />
+ ))}
+ >
+ )}
+
+ {stats && (
+
+ {stats}
+
+ )}
+
+ );
+}
diff --git a/ai-context/context-graph-demo/src/components/common/FilterButton.tsx b/ai-context/context-graph-demo/src/components/common/FilterButton.tsx
new file mode 100644
index 00000000..7b0ea0a9
--- /dev/null
+++ b/ai-context/context-graph-demo/src/components/common/FilterButton.tsx
@@ -0,0 +1,31 @@
+import { text, border } from "../../theme";
+
+interface FilterButtonProps {
+ label: string;
+ icon?: string;
+ color?: string;
+ isActive: boolean;
+ onClick: () => void;
+}
+
+export function FilterButton({ label, icon, color, isActive, onClick }: FilterButtonProps) {
+ const activeColor = color || "#fff";
+
+ return (
+
+ );
+}
diff --git a/ai-context/context-graph-demo/src/components/common/Header.tsx b/ai-context/context-graph-demo/src/components/common/Header.tsx
new file mode 100644
index 00000000..bcb968a9
--- /dev/null
+++ b/ai-context/context-graph-demo/src/components/common/Header.tsx
@@ -0,0 +1,58 @@
+import type { TabKey } from "../../types";
+
+interface HeaderProps {
+ activeTab: TabKey;
+ onTabChange: (tab: TabKey) => void;
+}
+
+export function Header({ activeTab, onTabChange }: HeaderProps) {
+ return (
+
+
+

+
+
+ TrustGraph
+
+
+ CONTEXT GRAPH DEMO
+
+
+
+
+ {(["graph", "query", "explain", "data", "ontology"] as const).map((tab) => {
+ const labels: Record = {
+ graph: "◈ Context Graph",
+ query: "⚡ Agent Query",
+ explain: "◉ Explain",
+ data: "▤ Table Explorer",
+ ontology: "◇ Ontology",
+ };
+ return (
+
+ );
+ })}
+
+
+ );
+}
diff --git a/ai-context/context-graph-demo/src/components/common/LoadingState.tsx b/ai-context/context-graph-demo/src/components/common/LoadingState.tsx
new file mode 100644
index 00000000..2f129d3d
--- /dev/null
+++ b/ai-context/context-graph-demo/src/components/common/LoadingState.tsx
@@ -0,0 +1,23 @@
+import { semantic, text } from "../../theme";
+
+interface LoadingStateProps {
+ message?: string;
+ variant?: "loading" | "error";
+}
+
+export function LoadingState({ message, variant = "loading" }: LoadingStateProps) {
+ const isError = variant === "error";
+ const defaultMessage = isError ? "Error loading data" : "Loading...";
+
+ return (
+
+ {message || defaultMessage}
+
+ );
+}
diff --git a/ai-context/context-graph-demo/src/components/common/MessageBubble.tsx b/ai-context/context-graph-demo/src/components/common/MessageBubble.tsx
new file mode 100644
index 00000000..bccb3c13
--- /dev/null
+++ b/ai-context/context-graph-demo/src/components/common/MessageBubble.tsx
@@ -0,0 +1,97 @@
+import { semantic, text, surface, border, withGlow } from "../../theme";
+
+export interface Message {
+ role: string;
+ text: string;
+ type?: string;
+}
+
+interface MessageBubbleProps {
+ message: Message;
+}
+
+export function MessageBubble({ message }: MessageBubbleProps) {
+ const isUser = message.role === "human";
+ const messageType = message.type;
+
+ const getTypeStyles = () => {
+ switch (messageType) {
+ case "thinking":
+ return {
+ bg: withGlow(semantic.thinking, 0.08),
+ border: withGlow(semantic.thinking, 0.2),
+ icon: "◈",
+ label: "THINKING",
+ color: semantic.thinking,
+ };
+ case "observation":
+ return {
+ bg: withGlow(semantic.observation, 0.08),
+ border: withGlow(semantic.observation, 0.2),
+ icon: "◉",
+ label: "OBSERVATION",
+ color: semantic.observation,
+ };
+ case "answer":
+ return {
+ bg: withGlow(semantic.answer, 0.08),
+ border: withGlow(semantic.answer, 0.2),
+ icon: "✓",
+ label: "ANSWER",
+ color: semantic.answer,
+ };
+ default:
+ return null;
+ }
+ };
+
+ const typeStyles = getTypeStyles();
+
+ if (isUser) {
+ return (
+
+
+ YOU
+
+
+ {message.text}
+
+
+ );
+ }
+
+ return (
+
+ {typeStyles && (
+
+ {typeStyles.icon}
+ {typeStyles.label}
+
+ )}
+
+ {message.text}
+
+
+ );
+}
diff --git a/ai-context/context-graph-demo/src/components/common/SearchInput.tsx b/ai-context/context-graph-demo/src/components/common/SearchInput.tsx
new file mode 100644
index 00000000..d822910e
--- /dev/null
+++ b/ai-context/context-graph-demo/src/components/common/SearchInput.tsx
@@ -0,0 +1,73 @@
+import { text, surface, border, palette } from "../../theme";
+
+interface SearchInputProps {
+ value: string;
+ onChange: (value: string) => void;
+ onSubmit: () => void;
+ placeholder?: string;
+ buttonText?: string;
+ isLoading?: boolean;
+ buttonColor?: string;
+ disabled?: boolean;
+}
+
+export function SearchInput({
+ value,
+ onChange,
+ onSubmit,
+ placeholder = "Search...",
+ buttonText = "Search",
+ isLoading = false,
+ buttonColor = palette.blue,
+ disabled = false,
+}: SearchInputProps) {
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault();
+ onSubmit();
+ }
+ };
+
+ const isDisabled = disabled || isLoading || !value.trim();
+
+ return (
+
+ onChange(e.target.value)}
+ onKeyDown={handleKeyDown}
+ placeholder={placeholder}
+ disabled={isLoading}
+ style={{
+ flex: 1,
+ padding: "12px 16px",
+ borderRadius: 8,
+ border: `1px solid ${border.medium}`,
+ background: surface.card,
+ color: text.primary,
+ fontSize: 14,
+ fontFamily: "'IBM Plex Sans', sans-serif",
+ outline: "none",
+ }}
+ />
+
+
+ );
+}
diff --git a/ai-context/context-graph-demo/src/components/common/SectionLabel.tsx b/ai-context/context-graph-demo/src/components/common/SectionLabel.tsx
new file mode 100644
index 00000000..56deb1b9
--- /dev/null
+++ b/ai-context/context-graph-demo/src/components/common/SectionLabel.tsx
@@ -0,0 +1,22 @@
+import { text } from "../../theme";
+
+interface SectionLabelProps {
+ children: React.ReactNode;
+ marginBottom?: number;
+ marginTop?: number;
+}
+
+export function SectionLabel({ children, marginBottom = 10, marginTop }: SectionLabelProps) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/ai-context/context-graph-demo/src/components/common/StatusBar.tsx b/ai-context/context-graph-demo/src/components/common/StatusBar.tsx
new file mode 100644
index 00000000..c3f23e09
--- /dev/null
+++ b/ai-context/context-graph-demo/src/components/common/StatusBar.tsx
@@ -0,0 +1,60 @@
+import { useConnectionState } from "@trustgraph/react-provider";
+import { useProgressStateStore } from "@trustgraph/react-state";
+import { semantic, palette, text, border } from "../../theme";
+
+export function StatusBar() {
+ const connectionState = useConnectionState();
+ const activity = useProgressStateStore((state) => state.activity);
+
+ const getStatusDisplay = () => {
+ if (!connectionState) return { color: text.subtle, text: "Initializing..." };
+ switch (connectionState.status) {
+ case "authenticated":
+ return { color: semantic.success, text: "Authenticated" };
+ case "connected":
+ return { color: semantic.success, text: "Connected" };
+ case "unauthenticated":
+ return { color: semantic.info, text: "Connected" };
+ case "connecting":
+ return { color: palette.amber, text: "Connecting..." };
+ case "reconnecting":
+ return { color: semantic.warning, text: `Reconnecting (${connectionState.reconnectAttempt}/${connectionState.maxAttempts})...` };
+ case "failed":
+ return { color: semantic.error, text: "Connection failed" };
+ default:
+ return { color: text.subtle, text: connectionState.status };
+ }
+ };
+
+ const status = getStatusDisplay();
+ const activeActivity = activity.size > 0 ? Array.from(activity)[0] : null;
+
+ return (
+
+
+ {activeActivity ? (
+ <>
+ ◌
+ {activeActivity}...
+ >
+ ) : (
+ <>
+ ◈
+ Ready
+ >
+ )}
+
+
+ ● {status.text}
+ |
+ trustgraph.ai
+
+
+ );
+}
diff --git a/ai-context/context-graph-demo/src/components/common/Toaster.tsx b/ai-context/context-graph-demo/src/components/common/Toaster.tsx
new file mode 100644
index 00000000..45c87d2f
--- /dev/null
+++ b/ai-context/context-graph-demo/src/components/common/Toaster.tsx
@@ -0,0 +1,120 @@
+import { useToastStore, Toast, ToastType } from "../../state/toastStore";
+import { semantic, surface, text } from "../../theme";
+
+const typeStyles: Record = {
+ success: { color: semantic.success, icon: "✓" },
+ error: { color: semantic.error, icon: "✕" },
+ warning: { color: semantic.warning, icon: "!" },
+ info: { color: semantic.info, icon: "i" },
+};
+
+function ToastItem({ toast, onDismiss }: { toast: Toast; onDismiss: () => void }) {
+ const style = typeStyles[toast.type];
+
+ return (
+
+
+ {style.icon}
+
+
+ {toast.message}
+
+
+
+ );
+}
+
+export function Toaster() {
+ const toasts = useToastStore((state) => state.toasts);
+ const removeToast = useToastStore((state) => state.removeToast);
+
+ if (toasts.length === 0) return null;
+
+ return (
+ <>
+
+
+ {toasts.map((toast) => (
+ removeToast(toast.id)}
+ />
+ ))}
+
+ >
+ );
+}
diff --git a/ai-context/context-graph-demo/src/components/common/Typewriter.tsx b/ai-context/context-graph-demo/src/components/common/Typewriter.tsx
new file mode 100644
index 00000000..8503deec
--- /dev/null
+++ b/ai-context/context-graph-demo/src/components/common/Typewriter.tsx
@@ -0,0 +1,37 @@
+import { useState, useEffect, useRef } from "react";
+
+interface TypewriterProps {
+ text: string;
+ speed?: number;
+ onDone?: () => void;
+}
+
+export function Typewriter({ text, speed = 12, onDone }: TypewriterProps) {
+ const [displayed, setDisplayed] = useState("");
+ const idx = useRef(0);
+
+ useEffect(() => {
+ idx.current = 0;
+ setDisplayed("");
+ const interval = setInterval(() => {
+ idx.current++;
+ if (idx.current >= text.length) {
+ setDisplayed(text);
+ clearInterval(interval);
+ onDone?.();
+ } else {
+ setDisplayed(text.slice(0, idx.current));
+ }
+ }, speed);
+ return () => clearInterval(interval);
+ }, [text, speed, onDone]);
+
+ return (
+
+ {displayed}
+
+ ▌
+
+
+ );
+}
diff --git a/ai-context/context-graph-demo/src/components/common/index.ts b/ai-context/context-graph-demo/src/components/common/index.ts
new file mode 100644
index 00000000..540ae1f9
--- /dev/null
+++ b/ai-context/context-graph-demo/src/components/common/index.ts
@@ -0,0 +1,14 @@
+export { SectionLabel } from "./SectionLabel";
+export { FilterButton } from "./FilterButton";
+export { Header } from "./Header";
+export { StatusBar } from "./StatusBar";
+export { Typewriter } from "./Typewriter";
+export { Card } from "./Card";
+export { Badge } from "./Badge";
+export { LoadingState } from "./LoadingState";
+export { Toaster } from "./Toaster";
+export { SearchInput } from "./SearchInput";
+export { FilterBar } from "./FilterBar";
+export type { FilterItem } from "./FilterBar";
+export { MessageBubble } from "./MessageBubble";
+export type { Message } from "./MessageBubble";
diff --git a/ai-context/context-graph-demo/src/components/graph/ExplainGraph.tsx b/ai-context/context-graph-demo/src/components/graph/ExplainGraph.tsx
new file mode 100644
index 00000000..8d24510c
--- /dev/null
+++ b/ai-context/context-graph-demo/src/components/graph/ExplainGraph.tsx
@@ -0,0 +1,451 @@
+import { useEffect, useRef, useState, useCallback, useMemo } from "react";
+import { ZoomControls } from "./ZoomControls";
+import { border, palette, text, withGlow } from "../../theme";
+
+// ── Types ───────────────────────────────────────────────────────────
+
+export interface ExplainGraphNode {
+ id: string;
+ label: string;
+ color?: string;
+}
+
+export interface ExplainGraphEdge {
+ id: string;
+ from: string;
+ to: string;
+ label: string;
+ reasoning?: string;
+}
+
+interface LayoutNode extends ExplainGraphNode {
+ x: number;
+ y: number;
+ vx: number;
+ vy: number;
+}
+
+interface ExplainGraphProps {
+ nodes: ExplainGraphNode[];
+ edges: ExplainGraphEdge[];
+ highlightedNodeIds: string[];
+ highlightedEdgeIds: string[];
+ onNodeClick?: (nodeId: string) => void;
+ onEdgeClick?: (edgeId: string) => void;
+}
+
+// ── Simple force layout ─────────────────────────────────────────────
+
+function computeLayout(
+ nodes: ExplainGraphNode[],
+ edges: ExplainGraphEdge[],
+ width: number,
+ height: number,
+): LayoutNode[] {
+ if (nodes.length === 0 || width === 0) return [];
+
+ const cx = width / 2;
+ const cy = height / 2;
+
+ // Initial positions: circle layout
+ const layoutNodes: LayoutNode[] = nodes.map((n, i) => {
+ const angle = (Math.PI * 2 * i) / nodes.length - Math.PI / 2;
+ const radius = Math.min(cx, cy) * 0.55;
+ return {
+ ...n,
+ x: cx + Math.cos(angle) * radius,
+ y: cy + Math.sin(angle) * radius,
+ vx: 0,
+ vy: 0,
+ };
+ });
+
+ // Run simple force simulation
+ const iterations = 120;
+ const repulsion = 2000;
+ const attraction = 0.005;
+ const damping = 0.85;
+ const centerPull = 0.01;
+
+ const nodeMap = new Map(layoutNodes.map((n, i) => [n.id, i]));
+
+ for (let iter = 0; iter < iterations; iter++) {
+ // Repulsion between all pairs
+ for (let i = 0; i < layoutNodes.length; i++) {
+ for (let j = i + 1; j < layoutNodes.length; j++) {
+ const a = layoutNodes[i];
+ const b = layoutNodes[j];
+ let dx = a.x - b.x;
+ let dy = a.y - b.y;
+ const dist = Math.sqrt(dx * dx + dy * dy) || 1;
+ const force = repulsion / (dist * dist);
+ dx = (dx / dist) * force;
+ dy = (dy / dist) * force;
+ a.vx += dx;
+ a.vy += dy;
+ b.vx -= dx;
+ b.vy -= dy;
+ }
+ }
+
+ // Attraction along edges
+ for (const edge of edges) {
+ const ai = nodeMap.get(edge.from);
+ const bi = nodeMap.get(edge.to);
+ if (ai === undefined || bi === undefined) continue;
+ const a = layoutNodes[ai];
+ const b = layoutNodes[bi];
+ const dx = b.x - a.x;
+ const dy = b.y - a.y;
+ const fx = dx * attraction;
+ const fy = dy * attraction;
+ a.vx += fx;
+ a.vy += fy;
+ b.vx -= fx;
+ b.vy -= fy;
+ }
+
+ // Center pull
+ for (const n of layoutNodes) {
+ n.vx += (cx - n.x) * centerPull;
+ n.vy += (cy - n.y) * centerPull;
+ }
+
+ // Apply velocity
+ for (const n of layoutNodes) {
+ n.vx *= damping;
+ n.vy *= damping;
+ n.x += n.vx;
+ n.y += n.vy;
+
+ // Keep in bounds with padding
+ const pad = 40;
+ n.x = Math.max(pad, Math.min(width - pad, n.x));
+ n.y = Math.max(pad, Math.min(height - pad, n.y));
+ }
+ }
+
+ return layoutNodes;
+}
+
+// ── Component ───────────────────────────────────────────────────────
+
+export function ExplainGraph({
+ nodes,
+ edges,
+ highlightedNodeIds,
+ highlightedEdgeIds,
+ onNodeClick,
+ onEdgeClick,
+}: ExplainGraphProps) {
+ const containerRef = useRef(null);
+ const svgRef = useRef(null);
+ const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
+ const [hovered, setHovered] = useState(null);
+
+ // Zoom and pan
+ const [zoom, setZoom] = useState(1);
+ const [pan, setPan] = useState({ x: 0, y: 0 });
+ const isPanningRef = useRef(false);
+ const lastPanPosRef = useRef({ x: 0, y: 0 });
+
+ // Track container size
+ useEffect(() => {
+ const container = containerRef.current;
+ if (!container) return;
+ const ro = new ResizeObserver((entries) => {
+ const entry = entries[0];
+ if (entry) {
+ setContainerSize({ width: entry.contentRect.width, height: entry.contentRect.height });
+ }
+ });
+ ro.observe(container);
+ return () => ro.disconnect();
+ }, []);
+
+ // Layout
+ const layoutNodes = useMemo(
+ () => computeLayout(nodes, edges, containerSize.width, containerSize.height),
+ [nodes, edges, containerSize],
+ );
+
+ const nodeMap = useMemo(
+ () => new Map(layoutNodes.map(n => [n.id, n])),
+ [layoutNodes],
+ );
+
+ // Grid lines
+ const gridLines = useMemo(() => {
+ const lines: React.ReactElement[] = [];
+ const { width, height } = containerSize;
+ if (width === 0) return lines;
+ for (let x = 0; x < width; x += 30) {
+ lines.push();
+ }
+ for (let y = 0; y < height; y += 30) {
+ lines.push();
+ }
+ return lines;
+ }, [containerSize]);
+
+ // Zoom
+ const handleWheel = useCallback((e: React.WheelEvent) => {
+ e.preventDefault();
+ const delta = e.deltaY > 0 ? 0.9 : 1.1;
+ const newZoom = Math.min(4, Math.max(0.25, zoom * delta));
+ const svg = svgRef.current;
+ if (!svg) return;
+ const rect = svg.getBoundingClientRect();
+ const cursorX = e.clientX - rect.left;
+ const cursorY = e.clientY - rect.top;
+ const zoomRatio = newZoom / zoom;
+ setPan(p => ({ x: cursorX - (cursorX - p.x) * zoomRatio, y: cursorY - (cursorY - p.y) * zoomRatio }));
+ setZoom(newZoom);
+ }, [zoom]);
+
+ // Pan
+ const handleMouseDown = useCallback((e: React.MouseEvent) => {
+ if (e.button === 1 || (e.button === 0 && e.shiftKey)) {
+ e.preventDefault();
+ isPanningRef.current = true;
+ lastPanPosRef.current = { x: e.clientX, y: e.clientY };
+ }
+ }, []);
+
+ const handleMouseMove = useCallback((e: React.MouseEvent) => {
+ if (!isPanningRef.current) return;
+ const dx = e.clientX - lastPanPosRef.current.x;
+ const dy = e.clientY - lastPanPosRef.current.y;
+ lastPanPosRef.current = { x: e.clientX, y: e.clientY };
+ setPan(p => ({ x: p.x + dx, y: p.y + dy }));
+ }, []);
+
+ const handleMouseUp = useCallback(() => { isPanningRef.current = false; }, []);
+
+ const handleResetView = useCallback(() => { setZoom(1); setPan({ x: 0, y: 0 }); }, []);
+
+ const hasHighlights = highlightedNodeIds.length > 0 || highlightedEdgeIds.length > 0;
+ const NODE_R = 10;
+
+ if (containerSize.width === 0) {
+ return ;
+ }
+
+ return (
+
+
+
+
setZoom(z => Math.min(4, z * 1.2))}
+ onZoomOut={() => setZoom(z => Math.max(0.25, z / 1.2))}
+ onReset={handleResetView}
+ />
+
+ {/* Empty state */}
+ {nodes.length === 0 && (
+
+ Graph will populate as explain events arrive
+
+ )}
+
+ {/* Tooltip */}
+ {hovered && !hovered.startsWith("edge-") && (() => {
+ const node = nodeMap.get(hovered);
+ if (!node) return null;
+ return (
+
+ );
+ })()}
+
+ {/* Edge tooltip */}
+ {hovered?.startsWith("edge-") && (() => {
+ const edgeId = hovered.slice(5);
+ const edge = edges.find(e => e.id === edgeId);
+ if (!edge) return null;
+ const from = nodeMap.get(edge.from);
+ const to = nodeMap.get(edge.to);
+ if (!from || !to) return null;
+ const mx = ((from.x + to.x) / 2) * zoom + pan.x;
+ const my = ((from.y + to.y) / 2) * zoom + pan.y;
+ return (
+
+
+ {edge.label}
+
+ {edge.reasoning && (
+
+ {edge.reasoning.length > 150 ? edge.reasoning.slice(0, 150) + "..." : edge.reasoning}
+
+ )}
+
+ );
+ })()}
+
+ );
+}
diff --git a/ai-context/context-graph-demo/src/components/graph/GraphCanvas.tsx b/ai-context/context-graph-demo/src/components/graph/GraphCanvas.tsx
new file mode 100644
index 00000000..b75f36fc
--- /dev/null
+++ b/ai-context/context-graph-demo/src/components/graph/GraphCanvas.tsx
@@ -0,0 +1,596 @@
+import { useEffect, useRef, useCallback, useState, MouseEvent } from "react";
+import type { DomainKey, Entity, GraphNode, OntologyType, Relationship } from "../../types";
+import { ZoomControls } from "./ZoomControls";
+import { border } from "../../theme";
+
+interface GraphCanvasProps {
+ entities: Entity[];
+ relationships: Relationship[];
+ ontology: OntologyType;
+ highlightedEntities: string[];
+ onNodeClick: (node: GraphNode) => void;
+ activeFilter: DomainKey | null;
+}
+
+const SETTLE_TIME = 10000; // 10 seconds until nodes settle
+const FRAME_INTERVAL = 1000 / 30; // 30fps
+
+export function GraphCanvas({ entities, relationships, ontology, highlightedEntities, onNodeClick, activeFilter }: GraphCanvasProps) {
+ const containerRef = useRef(null);
+ const staticCanvasRef = useRef(null);
+ const nodesCanvasRef = useRef(null);
+ const edgesCanvasRef = useRef(null);
+ const nodesRef = useRef([]);
+ const animRef = useRef(0);
+ const hoveredRef = useRef(null);
+ const settledRef = useRef(false);
+ const startTimeRef = useRef(0);
+ const timeRef = useRef(0);
+ const lastFrameTimeRef = useRef(0);
+
+ // Store view state in refs to avoid triggering resets
+ const highlightedRef = useRef(highlightedEntities);
+ const activeFilterRef = useRef(activeFilter);
+ const relationshipsRef = useRef(relationships);
+ const ontologyRef = useRef(ontology);
+
+ const [hovered, setHovered] = useState(null);
+ const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
+
+ // Zoom and pan state
+ const [zoom, setZoom] = useState(1);
+ const [pan, setPan] = useState({ x: 0, y: 0 });
+ const zoomRef = useRef(1);
+ const panRef = useRef({ x: 0, y: 0 });
+ const isPanningRef = useRef(false);
+ const lastPanPosRef = useRef({ x: 0, y: 0 });
+
+ // Keep zoom/pan refs in sync
+ zoomRef.current = zoom;
+ panRef.current = pan;
+
+ // Keep refs in sync with props
+ useEffect(() => {
+ highlightedRef.current = highlightedEntities;
+ }, [highlightedEntities]);
+
+ useEffect(() => {
+ activeFilterRef.current = activeFilter;
+ }, [activeFilter]);
+
+ useEffect(() => {
+ relationshipsRef.current = relationships;
+ }, [relationships]);
+
+ useEffect(() => {
+ ontologyRef.current = ontology;
+ }, [ontology]);
+
+ // Track container size changes
+ useEffect(() => {
+ const container = containerRef.current;
+ if (!container) return;
+
+ const resizeObserver = new ResizeObserver((entries) => {
+ const entry = entries[0];
+ if (entry) {
+ setContainerSize({
+ width: entry.contentRect.width,
+ height: entry.contentRect.height,
+ });
+ }
+ });
+
+ resizeObserver.observe(container);
+ return () => resizeObserver.disconnect();
+ }, []);
+
+ // Draw static layer (grid + domain labels)
+ const drawStaticLayer = useCallback((ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, domainPositions: Record) => {
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+ // Grid stays fixed (no transform)
+ ctx.strokeStyle = border.grid;
+ ctx.lineWidth = 1;
+ for (let x = 0; x < canvas.width; x += 60) {
+ ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke();
+ }
+ for (let y = 0; y < canvas.height; y += 60) {
+ ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke();
+ }
+
+ // Domain labels with zoom/pan transform
+ ctx.save();
+ ctx.translate(panRef.current.x, panRef.current.y);
+ ctx.scale(zoomRef.current, zoomRef.current);
+
+ const currentOntology = ontologyRef.current;
+ (Object.entries(domainPositions) as [DomainKey, { x: number; y: number }][]).forEach(([domain, pos]) => {
+ const data = currentOntology[domain];
+ ctx.font = "bold 22px 'IBM Plex Mono', monospace";
+ ctx.fillStyle = data.color + "44";
+ ctx.textAlign = "center";
+ ctx.fillText(data.label.toUpperCase(), pos.x, pos.y - Math.min(canvas.width, canvas.height) * 0.14);
+ });
+
+ ctx.restore();
+ }, []);
+
+ // Draw nodes layer - reads from refs
+ const drawNodesLayer = useCallback((ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, time: number) => {
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+ // Apply zoom/pan transform
+ ctx.save();
+ ctx.translate(panRef.current.x, panRef.current.y);
+ ctx.scale(zoomRef.current, zoomRef.current);
+
+ const nodes = nodesRef.current;
+ const settled = settledRef.current;
+ const highlighted = highlightedRef.current;
+ const filter = activeFilterRef.current;
+ const rels = relationshipsRef.current;
+
+ nodes.forEach((node) => {
+ const isHighlighted = highlighted && highlighted.includes(node.id);
+ const isHovered = hoveredRef.current === node.id;
+ const isDimmed = highlighted && highlighted.length > 0 && !isHighlighted;
+ const isFiltered = filter && node.domain !== filter && !rels.some(
+ r => r.domain.includes(filter) && (r.from === node.id || r.to === node.id)
+ );
+
+ const alpha = isFiltered ? 0.15 : isDimmed ? 0.3 : 1;
+ const r = isHighlighted || isHovered ? node.r * 1.4 : node.r;
+ const pulseR = isHighlighted && !settled ? Math.sin(time * 3) * 3 : 0;
+
+ // Glow
+ if ((isHighlighted || isHovered) && !isFiltered) {
+ ctx.beginPath();
+ ctx.arc(node.x, node.y, r + 12 + pulseR, 0, Math.PI * 2);
+ const grd = ctx.createRadialGradient(node.x, node.y, r, node.x, node.y, r + 12 + pulseR);
+ grd.addColorStop(0, node.glow);
+ grd.addColorStop(1, "rgba(0,0,0,0)");
+ ctx.fillStyle = grd;
+ ctx.fill();
+ }
+
+ // Node circle
+ ctx.beginPath();
+ ctx.arc(node.x, node.y, r, 0, Math.PI * 2);
+ ctx.fillStyle = node.color + Math.round(alpha * 255 * 0.2).toString(16).padStart(2, "0");
+ ctx.fill();
+ ctx.strokeStyle = node.color + Math.round(alpha * 255).toString(16).padStart(2, "0");
+ ctx.lineWidth = isHighlighted ? 2.5 : 1.5;
+ ctx.stroke();
+
+ // Label
+ ctx.font = `${isHighlighted ? "bold " : ""}${isHovered ? 17 : 14}px 'IBM Plex Sans', sans-serif`;
+ ctx.fillStyle = `rgba(255,255,255,${alpha * (isHighlighted ? 1 : 0.75)})`;
+ ctx.textAlign = "center";
+ ctx.fillText(node.label, node.x, node.y + r + 18);
+
+ // Update node positions (spring physics + drift) - only if not settled
+ if (!settled) {
+ node.x += (node.targetX - node.x) * 0.02;
+ node.y += (node.targetY - node.y) * 0.02;
+ node.x += Math.sin(time + node.targetX * 0.01) * 0.3;
+ node.y += Math.cos(time + node.targetY * 0.01) * 0.3;
+ }
+ });
+
+ ctx.restore();
+ }, []);
+
+ // Draw edges layer - reads from refs
+ const drawEdgesLayer = useCallback((ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, time: number) => {
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+ // Apply zoom/pan transform
+ ctx.save();
+ ctx.translate(panRef.current.x, panRef.current.y);
+ ctx.scale(zoomRef.current, zoomRef.current);
+
+ const nodes = nodesRef.current;
+ const highlighted = highlightedRef.current;
+ const filter = activeFilterRef.current;
+ const rels = relationshipsRef.current;
+
+ const filteredRels = filter
+ ? rels.filter((r) => r.domain.includes(filter))
+ : rels;
+
+ filteredRels.forEach((rel) => {
+ const fromNode = nodes.find((n) => n.id === rel.from);
+ const toNode = nodes.find((n) => n.id === rel.to);
+ if (!fromNode || !toNode) return;
+
+ const isHighlighted =
+ highlighted &&
+ highlighted.includes(rel.from) &&
+ highlighted.includes(rel.to);
+
+ const baseAlpha = isHighlighted ? 0.7 : 0.12;
+ const pulse = isHighlighted ? Math.sin(time * 4) * 0.15 + 0.15 : 0;
+
+ ctx.beginPath();
+ ctx.moveTo(fromNode.x, fromNode.y);
+ // Curved edges
+ const mx = (fromNode.x + toNode.x) / 2 + (fromNode.y - toNode.y) * 0.1;
+ const my = (fromNode.y + toNode.y) / 2 + (toNode.x - fromNode.x) * 0.1;
+ ctx.quadraticCurveTo(mx, my, toNode.x, toNode.y);
+
+ const gradient = ctx.createLinearGradient(fromNode.x, fromNode.y, toNode.x, toNode.y);
+ gradient.addColorStop(0, fromNode.color + Math.round((baseAlpha + pulse) * 255).toString(16).padStart(2, "0"));
+ gradient.addColorStop(1, toNode.color + Math.round((baseAlpha + pulse) * 255).toString(16).padStart(2, "0"));
+ ctx.strokeStyle = gradient;
+ ctx.lineWidth = isHighlighted ? 3 : 1.5;
+ ctx.stroke();
+
+ // Animated particles on highlighted edges
+ if (isHighlighted) {
+ const t = (time * 2) % 1;
+ const px = (1 - t) * (1 - t) * fromNode.x + 2 * (1 - t) * t * mx + t * t * toNode.x;
+ const py = (1 - t) * (1 - t) * fromNode.y + 2 * (1 - t) * t * my + t * t * toNode.y;
+ ctx.beginPath();
+ ctx.arc(px, py, 3, 0, Math.PI * 2);
+ ctx.fillStyle = "#fff";
+ ctx.fill();
+ }
+ });
+
+ ctx.restore();
+ }, []);
+
+ // Animation loop function - separate from setup
+ const runAnimation = useCallback(() => {
+ const nodesCanvas = nodesCanvasRef.current;
+ const edgesCanvas = edgesCanvasRef.current;
+ const nodesCtx = nodesCanvas?.getContext("2d");
+ const edgesCtx = edgesCanvas?.getContext("2d");
+
+ if (!nodesCtx || !nodesCanvas || !edgesCtx || !edgesCanvas) return;
+
+ // Capture validated references for the closure
+ const validNodesCtx = nodesCtx;
+ const validNodesCanvas = nodesCanvas;
+ const validEdgesCtx = edgesCtx;
+ const validEdgesCanvas = edgesCanvas;
+
+ function animate(currentTime: number) {
+ // Throttle to target fps
+ if (currentTime - lastFrameTimeRef.current < FRAME_INTERVAL) {
+ animRef.current = requestAnimationFrame(animate);
+ return;
+ }
+ lastFrameTimeRef.current = currentTime;
+ timeRef.current += 0.01;
+
+ // Check if we should settle
+ if (!settledRef.current && currentTime - startTimeRef.current > SETTLE_TIME) {
+ settledRef.current = true;
+ }
+
+ const hasHighlights = highlightedRef.current && highlightedRef.current.length > 0;
+ const isSettled = settledRef.current;
+
+ // Draw edges layer
+ drawEdgesLayer(validEdgesCtx, validEdgesCanvas, timeRef.current);
+
+ // Draw nodes layer
+ if (!isSettled || hasHighlights || hoveredRef.current) {
+ drawNodesLayer(validNodesCtx, validNodesCanvas, timeRef.current);
+ }
+
+ // Continue animation if not settled, or if there are highlights
+ if (!isSettled || hasHighlights) {
+ animRef.current = requestAnimationFrame(animate);
+ } else {
+ // Settled with no highlights - do one final draw and stop
+ drawNodesLayer(validNodesCtx, validNodesCanvas, timeRef.current);
+ drawEdgesLayer(validEdgesCtx, validEdgesCanvas, timeRef.current);
+ animRef.current = 0;
+ }
+ }
+
+ animRef.current = requestAnimationFrame(animate);
+ }, [drawNodesLayer, drawEdgesLayer]);
+
+ // Main setup - only runs when data or size changes
+ useEffect(() => {
+ const staticCanvas = staticCanvasRef.current;
+ const nodesCanvas = nodesCanvasRef.current;
+ const edgesCanvas = edgesCanvasRef.current;
+ if (!staticCanvas || !nodesCanvas || !edgesCanvas || containerSize.width === 0) return;
+
+ // Cancel any existing animation
+ if (animRef.current) {
+ cancelAnimationFrame(animRef.current);
+ animRef.current = 0;
+ }
+
+ // Setup all canvases
+ [staticCanvas, nodesCanvas, edgesCanvas].forEach(canvas => {
+ canvas.width = containerSize.width * 2;
+ canvas.height = containerSize.height * 2;
+ canvas.style.width = containerSize.width + "px";
+ canvas.style.height = containerSize.height + "px";
+ });
+
+ const cx = staticCanvas.width / 2;
+ const cy = staticCanvas.height / 2;
+
+ // Position nodes in domain clusters
+ const domainKeys = Object.keys(ontology);
+ const domainPositions: Record = {};
+ domainKeys.forEach((domain, i) => {
+ const angle = (Math.PI * 2 * i) / domainKeys.length - Math.PI / 2;
+ const radius = Math.min(cx, cy) * 0.45;
+ domainPositions[domain] = {
+ x: cx + Math.cos(angle) * radius,
+ y: cy + Math.sin(angle) * radius,
+ };
+ });
+
+ nodesRef.current = entities.map((e) => {
+ const dp = domainPositions[e.domain];
+ const subIdx = ontology[e.domain].subclasses.findIndex((s) => s.id === e.id);
+ const total = ontology[e.domain].subclasses.length;
+ const angle = ((Math.PI * 2) / total) * subIdx - Math.PI / 2;
+ const radius = Math.min(staticCanvas.width, staticCanvas.height) * 0.1;
+ return {
+ ...e,
+ x: dp.x + Math.cos(angle) * radius,
+ y: dp.y + Math.sin(angle) * radius,
+ vx: 0,
+ vy: 0,
+ targetX: dp.x + Math.cos(angle) * radius,
+ targetY: dp.y + Math.sin(angle) * radius,
+ r: 18,
+ };
+ });
+
+ const staticCtx = staticCanvas.getContext("2d");
+ if (!staticCtx) return;
+
+ // Draw static layer once
+ drawStaticLayer(staticCtx, staticCanvas, domainPositions);
+
+ // Reset animation state
+ settledRef.current = false;
+ startTimeRef.current = performance.now();
+ timeRef.current = 0;
+ lastFrameTimeRef.current = 0;
+
+ // Start animation
+ runAnimation();
+
+ return () => {
+ if (animRef.current) {
+ cancelAnimationFrame(animRef.current);
+ animRef.current = 0;
+ }
+ };
+ }, [entities, ontology, containerSize, drawStaticLayer, runAnimation]);
+
+ // Restart animation when highlights change (without resetting positions)
+ useEffect(() => {
+ const hasHighlights = highlightedEntities && highlightedEntities.length > 0;
+
+ // If we have highlights and animation isn't running, restart it
+ if (hasHighlights && animRef.current === 0) {
+ runAnimation();
+ }
+ }, [highlightedEntities, runAnimation]);
+
+ // Redraw on filter change (without resetting)
+ useEffect(() => {
+ const nodesCanvas = nodesCanvasRef.current;
+ const edgesCanvas = edgesCanvasRef.current;
+ const nodesCtx = nodesCanvas?.getContext("2d");
+ const edgesCtx = edgesCanvas?.getContext("2d");
+
+ if (nodesCtx && nodesCanvas && edgesCtx && edgesCanvas && settledRef.current && animRef.current === 0) {
+ drawNodesLayer(nodesCtx, nodesCanvas, timeRef.current);
+ drawEdgesLayer(edgesCtx, edgesCanvas, timeRef.current);
+ }
+ }, [activeFilter, drawNodesLayer, drawEdgesLayer]);
+
+ const handleMouseMove = useCallback((e: MouseEvent) => {
+ // Handle panning first
+ if (isPanningRef.current) {
+ const dx = (e.clientX - lastPanPosRef.current.x) * 2;
+ const dy = (e.clientY - lastPanPosRef.current.y) * 2;
+ lastPanPosRef.current = { x: e.clientX, y: e.clientY };
+ setPan(p => ({ x: p.x + dx, y: p.y + dy }));
+ return;
+ }
+
+ const canvas = nodesCanvasRef.current;
+ if (!canvas) return;
+ const rect = canvas.getBoundingClientRect();
+
+ // Transform screen coordinates to world coordinates (accounting for zoom/pan)
+ const screenX = (e.clientX - rect.left) * 2;
+ const screenY = (e.clientY - rect.top) * 2;
+ const x = (screenX - panRef.current.x) / zoomRef.current;
+ const y = (screenY - panRef.current.y) / zoomRef.current;
+
+ const nodes = nodesRef.current;
+ let found: string | null = null;
+ for (const node of nodes) {
+ const dx = node.x - x;
+ const dy = node.y - y;
+ if (Math.sqrt(dx * dx + dy * dy) < node.r * 1.5) {
+ found = node.id;
+ break;
+ }
+ }
+ const wasHovered = hoveredRef.current;
+ hoveredRef.current = found;
+ setHovered(found);
+ canvas.style.cursor = isPanningRef.current ? "grabbing" : (found ? "pointer" : "default");
+
+ // Redraw if hover state changed and we're settled
+ if (wasHovered !== found && settledRef.current) {
+ const nodesCanvas = nodesCanvasRef.current;
+ const edgesCanvas = edgesCanvasRef.current;
+ const nodesCtx = nodesCanvas?.getContext("2d");
+ const edgesCtx = edgesCanvas?.getContext("2d");
+
+ if (nodesCtx && nodesCanvas && edgesCtx && edgesCanvas) {
+ drawNodesLayer(nodesCtx, nodesCanvas, timeRef.current);
+ drawEdgesLayer(edgesCtx, edgesCanvas, timeRef.current);
+ }
+ }
+ }, [drawNodesLayer, drawEdgesLayer]);
+
+ const handleClick = useCallback((e: MouseEvent) => {
+ // Don't trigger click if we were panning
+ if (e.shiftKey) return;
+ if (hoveredRef.current && onNodeClick) {
+ const node = nodesRef.current.find((n) => n.id === hoveredRef.current);
+ if (node) onNodeClick(node);
+ }
+ }, [onNodeClick]);
+
+ // Redraw all layers (used when zoom/pan changes)
+ const redrawAllLayers = useCallback(() => {
+ const staticCanvas = staticCanvasRef.current;
+ const nodesCanvas = nodesCanvasRef.current;
+ const edgesCanvas = edgesCanvasRef.current;
+ const staticCtx = staticCanvas?.getContext("2d");
+ const nodesCtx = nodesCanvas?.getContext("2d");
+ const edgesCtx = edgesCanvas?.getContext("2d");
+
+ if (!staticCtx || !staticCanvas || !nodesCtx || !nodesCanvas || !edgesCtx || !edgesCanvas) return;
+
+ // Recalculate domain positions for static layer redraw
+ const cx = staticCanvas.width / 2;
+ const cy = staticCanvas.height / 2;
+ const domainKeys = Object.keys(ontologyRef.current);
+ const domainPositions: Record = {};
+ domainKeys.forEach((domain, i) => {
+ const angle = (Math.PI * 2 * i) / domainKeys.length - Math.PI / 2;
+ const radius = Math.min(cx, cy) * 0.45;
+ domainPositions[domain] = {
+ x: cx + Math.cos(angle) * radius,
+ y: cy + Math.sin(angle) * radius,
+ };
+ });
+
+ drawStaticLayer(staticCtx, staticCanvas, domainPositions);
+ drawEdgesLayer(edgesCtx, edgesCanvas, timeRef.current);
+ drawNodesLayer(nodesCtx, nodesCanvas, timeRef.current);
+ }, [drawStaticLayer, drawEdgesLayer, drawNodesLayer]);
+
+ // Zoom handler - zoom towards cursor position
+ const handleWheel = useCallback((e: React.WheelEvent) => {
+ e.preventDefault();
+ const delta = e.deltaY > 0 ? 0.9 : 1.1;
+ const newZoom = Math.min(4, Math.max(0.25, zoomRef.current * delta));
+
+ const canvas = nodesCanvasRef.current;
+ if (!canvas) return;
+ const rect = canvas.getBoundingClientRect();
+ const cursorX = (e.clientX - rect.left) * 2; // Account for 2x canvas scaling
+ const cursorY = (e.clientY - rect.top) * 2;
+
+ // Adjust pan to zoom towards cursor
+ const zoomRatio = newZoom / zoomRef.current;
+ const newPanX = cursorX - (cursorX - panRef.current.x) * zoomRatio;
+ const newPanY = cursorY - (cursorY - panRef.current.y) * zoomRatio;
+
+ setZoom(newZoom);
+ setPan({ x: newPanX, y: newPanY });
+ }, []);
+
+ // Pan handlers
+ const handleMouseDown = useCallback((e: MouseEvent) => {
+ if (e.button === 1 || (e.button === 0 && e.shiftKey)) {
+ e.preventDefault();
+ isPanningRef.current = true;
+ lastPanPosRef.current = { x: e.clientX, y: e.clientY };
+ }
+ }, []);
+
+ const handleMouseUp = useCallback(() => {
+ isPanningRef.current = false;
+ }, []);
+
+ // Reset zoom/pan
+ const handleResetView = useCallback(() => {
+ setZoom(1);
+ setPan({ x: 0, y: 0 });
+ }, []);
+
+ // Redraw when zoom/pan changes
+ useEffect(() => {
+ if (containerSize.width > 0) {
+ redrawAllLayers();
+ }
+ }, [zoom, pan, containerSize, redrawAllLayers]);
+
+ const canvasStyle: React.CSSProperties = {
+ position: "absolute",
+ top: 0,
+ left: 0,
+ width: "100%",
+ height: "100%",
+ };
+
+ return (
+
+ {/* Layer 1: Static (grid + domain labels) */}
+
+ {/* Layer 2: Edges */}
+
+ {/* Layer 3: Nodes (on top for interaction) */}
+
+
+
setZoom(z => Math.min(4, z * 1.2))}
+ onZoomOut={() => setZoom(z => Math.max(0.25, z / 1.2))}
+ onReset={handleResetView}
+ />
+
+ {/* Tooltip */}
+ {hovered && (() => {
+ const node = nodesRef.current.find((n) => n.id === hovered);
+ if (!node) return null;
+ // Transform node position to screen coordinates
+ const sx = (node.x * zoomRef.current + panRef.current.x) / 2;
+ const sy = (node.y * zoomRef.current + panRef.current.y) / 2;
+ return (
+
+
+ {node.icon} {node.label}
+
+
+ {Object.entries(node.props || {}).map(([k, v]) => (
+
{k}: {String(v)}
+ ))}
+
+
+ );
+ })()}
+
+ );
+}
diff --git a/ai-context/context-graph-demo/src/components/graph/GraphCanvasSVG.tsx b/ai-context/context-graph-demo/src/components/graph/GraphCanvasSVG.tsx
new file mode 100644
index 00000000..39964f2e
--- /dev/null
+++ b/ai-context/context-graph-demo/src/components/graph/GraphCanvasSVG.tsx
@@ -0,0 +1,456 @@
+import { useEffect, useRef, useState, useCallback, useMemo } from "react";
+import type { DomainKey, Entity, GraphNode, OntologyType, Relationship } from "../../types";
+import { ZoomControls } from "./ZoomControls";
+import { border } from "../../theme";
+
+interface GraphCanvasSVGProps {
+ entities: Entity[];
+ relationships: Relationship[];
+ ontology: OntologyType;
+ highlightedEntities: string[];
+ onNodeClick: (node: GraphNode) => void;
+ activeFilter: DomainKey | null;
+}
+
+const SETTLE_TIME = 10000; // 10 seconds until nodes settle
+
+export function GraphCanvasSVG({ entities, relationships, ontology, highlightedEntities, onNodeClick, activeFilter }: GraphCanvasSVGProps) {
+ const containerRef = useRef(null);
+ const svgRef = useRef(null);
+ const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
+ const [hovered, setHovered] = useState(null);
+ const [settled, setSettled] = useState(false);
+ const [time, setTime] = useState(0);
+ const animRef = useRef(0);
+ const startTimeRef = useRef(0);
+ const lastFrameTimeRef = useRef(0);
+
+ // Zoom and pan state
+ const [zoom, setZoom] = useState(1);
+ const [pan, setPan] = useState({ x: 0, y: 0 });
+ const isPanningRef = useRef(false);
+ const lastPanPosRef = useRef({ x: 0, y: 0 });
+
+ // Track container size
+ useEffect(() => {
+ const container = containerRef.current;
+ if (!container) return;
+
+ const resizeObserver = new ResizeObserver((entries) => {
+ const entry = entries[0];
+ if (entry) {
+ setContainerSize({
+ width: entry.contentRect.width,
+ height: entry.contentRect.height,
+ });
+ }
+ });
+
+ resizeObserver.observe(container);
+ return () => resizeObserver.disconnect();
+ }, []);
+
+ // Calculate node positions
+ const { nodes, domainPositions } = useMemo(() => {
+ if (containerSize.width === 0) return { nodes: [], domainPositions: {} };
+
+ const width = containerSize.width;
+ const height = containerSize.height;
+ const cx = width / 2;
+ const cy = height / 2;
+
+ const domainKeys = Object.keys(ontology);
+ const domainPositions: Record = {};
+ domainKeys.forEach((domain, i) => {
+ const angle = (Math.PI * 2 * i) / domainKeys.length - Math.PI / 2;
+ const radius = Math.min(cx, cy) * 0.45;
+ domainPositions[domain] = {
+ x: cx + Math.cos(angle) * radius,
+ y: cy + Math.sin(angle) * radius,
+ };
+ });
+
+ const nodes: GraphNode[] = entities.map((e) => {
+ const dp = domainPositions[e.domain];
+ const subIdx = ontology[e.domain].subclasses.findIndex((s) => s.id === e.id);
+ const total = ontology[e.domain].subclasses.length;
+ const angle = ((Math.PI * 2) / total) * subIdx - Math.PI / 2;
+ const radius = Math.min(width, height) * 0.1;
+ const x = dp.x + Math.cos(angle) * radius;
+ const y = dp.y + Math.sin(angle) * radius;
+ return {
+ ...e,
+ x,
+ y,
+ vx: 0,
+ vy: 0,
+ targetX: x,
+ targetY: y,
+ r: 9, // Half size since we're not doing 2x canvas scaling
+ };
+ });
+
+ return { nodes, domainPositions };
+ }, [entities, ontology, containerSize]);
+
+ // Animation loop for breathing effect
+ useEffect(() => {
+ if (containerSize.width === 0) return;
+
+ startTimeRef.current = performance.now();
+ setSettled(false);
+ setTime(0);
+
+ const frameInterval = 1000 / 30; // 30fps
+
+ function animate(currentTime: number) {
+ if (currentTime - lastFrameTimeRef.current < frameInterval) {
+ animRef.current = requestAnimationFrame(animate);
+ return;
+ }
+ lastFrameTimeRef.current = currentTime;
+
+ // Check if should settle
+ if (currentTime - startTimeRef.current > SETTLE_TIME) {
+ setSettled(true);
+ // Continue animation only if there are highlights
+ if (highlightedEntities && highlightedEntities.length > 0) {
+ setTime(t => t + 0.01);
+ animRef.current = requestAnimationFrame(animate);
+ }
+ return;
+ }
+
+ setTime(t => t + 0.01);
+ animRef.current = requestAnimationFrame(animate);
+ }
+
+ animRef.current = requestAnimationFrame(animate);
+ return () => cancelAnimationFrame(animRef.current);
+ }, [containerSize, entities, ontology]);
+
+ // Restart animation when highlights change
+ useEffect(() => {
+ if (highlightedEntities && highlightedEntities.length > 0 && settled && animRef.current === 0) {
+ const frameInterval = 1000 / 30;
+
+ function animate(currentTime: number) {
+ if (currentTime - lastFrameTimeRef.current < frameInterval) {
+ animRef.current = requestAnimationFrame(animate);
+ return;
+ }
+ lastFrameTimeRef.current = currentTime;
+ setTime(t => t + 0.01);
+
+ if (highlightedEntities && highlightedEntities.length > 0) {
+ animRef.current = requestAnimationFrame(animate);
+ } else {
+ animRef.current = 0;
+ }
+ }
+
+ animRef.current = requestAnimationFrame(animate);
+ }
+ }, [highlightedEntities, settled]);
+
+ // Generate grid lines
+ const gridLines = useMemo(() => {
+ const lines: React.ReactElement[] = [];
+ const { width, height } = containerSize;
+ if (width === 0) return lines;
+
+ for (let x = 0; x < width; x += 30) {
+ lines.push(
+
+ );
+ }
+ for (let y = 0; y < height; y += 30) {
+ lines.push(
+
+ );
+ }
+ return lines;
+ }, [containerSize]);
+
+ // Calculate edge path with curve
+ const getEdgePath = useCallback((fromNode: GraphNode, toNode: GraphNode, time: number, isSettled: boolean) => {
+ const driftX1 = isSettled ? 0 : Math.sin(time + fromNode.targetX * 0.01) * 0.3;
+ const driftY1 = isSettled ? 0 : Math.cos(time + fromNode.targetY * 0.01) * 0.3;
+ const driftX2 = isSettled ? 0 : Math.sin(time + toNode.targetX * 0.01) * 0.3;
+ const driftY2 = isSettled ? 0 : Math.cos(time + toNode.targetY * 0.01) * 0.3;
+
+ const x1 = fromNode.x + driftX1;
+ const y1 = fromNode.y + driftY1;
+ const x2 = toNode.x + driftX2;
+ const y2 = toNode.y + driftY2;
+
+ const mx = (x1 + x2) / 2 + (y1 - y2) * 0.1;
+ const my = (y1 + y2) / 2 + (x2 - x1) * 0.1;
+
+ return { path: `M ${x1} ${y1} Q ${mx} ${my} ${x2} ${y2}`, mx, my, x1, y1, x2, y2 };
+ }, []);
+
+ // Get node position with drift
+ const getNodePosition = useCallback((node: GraphNode, time: number, isSettled: boolean) => {
+ if (isSettled) {
+ return { x: node.x, y: node.y };
+ }
+ const driftX = Math.sin(time + node.targetX * 0.01) * 0.3;
+ const driftY = Math.cos(time + node.targetY * 0.01) * 0.3;
+ return { x: node.x + driftX, y: node.y + driftY };
+ }, []);
+
+ const handleNodeClick = useCallback((node: GraphNode) => {
+ onNodeClick(node);
+ }, [onNodeClick]);
+
+ // Zoom handler - zoom towards cursor position
+ const handleWheel = useCallback((e: React.WheelEvent) => {
+ e.preventDefault();
+ const delta = e.deltaY > 0 ? 0.9 : 1.1;
+ const newZoom = Math.min(4, Math.max(0.25, zoom * delta));
+
+ // Get cursor position relative to SVG
+ const svg = svgRef.current;
+ if (!svg) return;
+ const rect = svg.getBoundingClientRect();
+ const cursorX = e.clientX - rect.left;
+ const cursorY = e.clientY - rect.top;
+
+ // Adjust pan to zoom towards cursor
+ const zoomRatio = newZoom / zoom;
+ const newPanX = cursorX - (cursorX - pan.x) * zoomRatio;
+ const newPanY = cursorY - (cursorY - pan.y) * zoomRatio;
+
+ setZoom(newZoom);
+ setPan({ x: newPanX, y: newPanY });
+ }, [zoom, pan]);
+
+ // Pan handlers
+ const handleMouseDown = useCallback((e: React.MouseEvent) => {
+ // Only pan with middle mouse or when holding space (we'll just use middle mouse for now)
+ if (e.button === 1 || e.button === 0 && e.shiftKey) {
+ e.preventDefault();
+ isPanningRef.current = true;
+ lastPanPosRef.current = { x: e.clientX, y: e.clientY };
+ }
+ }, []);
+
+ const handleMouseMove = useCallback((e: React.MouseEvent) => {
+ if (!isPanningRef.current) return;
+ const dx = e.clientX - lastPanPosRef.current.x;
+ const dy = e.clientY - lastPanPosRef.current.y;
+ lastPanPosRef.current = { x: e.clientX, y: e.clientY };
+ setPan(p => ({ x: p.x + dx, y: p.y + dy }));
+ }, []);
+
+ const handleMouseUp = useCallback(() => {
+ isPanningRef.current = false;
+ }, []);
+
+ // Reset zoom/pan
+ const handleResetView = useCallback(() => {
+ setZoom(1);
+ setPan({ x: 0, y: 0 });
+ }, []);
+
+ if (containerSize.width === 0) {
+ return ;
+ }
+
+ const filteredRels = activeFilter
+ ? relationships.filter((r) => r.domain.includes(activeFilter))
+ : relationships;
+
+ return (
+
+
+
+
setZoom(z => Math.min(4, z * 1.2))}
+ onZoomOut={() => setZoom(z => Math.max(0.25, z / 1.2))}
+ onReset={handleResetView}
+ />
+
+ {/* Tooltip */}
+ {hovered && (() => {
+ const node = nodes.find((n) => n.id === hovered);
+ if (!node) return null;
+ const { x, y } = getNodePosition(node, time, settled);
+ return (
+
+
+ {node.icon} {node.label}
+
+
+ {Object.entries(node.props || {}).map(([k, v]) => (
+
{k}: {String(v)}
+ ))}
+
+
+ );
+ })()}
+
+ );
+}
diff --git a/ai-context/context-graph-demo/src/components/graph/NodeDetailPanel.tsx b/ai-context/context-graph-demo/src/components/graph/NodeDetailPanel.tsx
new file mode 100644
index 00000000..ffb93a9d
--- /dev/null
+++ b/ai-context/context-graph-demo/src/components/graph/NodeDetailPanel.tsx
@@ -0,0 +1,70 @@
+import type { Entity, Relationship, OntologyType } from "../../types";
+import { SectionLabel, Card } from "../common";
+import { text, border } from "../../theme";
+
+interface NodeDetailPanelProps {
+ node: Entity;
+ relationships: Relationship[];
+ entities: Entity[];
+ ontology: OntologyType;
+ propertyLabels: Record;
+ onClose: () => void;
+ onNodeSelect: (node: Entity) => void;
+}
+
+export function NodeDetailPanel({ node, relationships, entities, ontology, propertyLabels, onClose, onNodeSelect }: NodeDetailPanelProps) {
+ // Filter relationships for this node
+ const nodeRelationships = relationships.filter(
+ r => r.from === node.id || r.to === node.id
+ );
+
+ return (
+
+
+
+ {ontology[node.domain].label.toUpperCase()} ENTITY
+
+
+
+
+ {node.icon} {node.label}
+
+
+
PROPERTIES
+ {Object.entries(node.props || {}).map(([k, v]) => (
+
+ {propertyLabels[k] || k}
+ {String(v)}
+
+ ))}
+
+
+
RELATIONSHIPS
+ {nodeRelationships.map((r, i) => {
+ const otherId = r.from === node.id ? r.to : r.from;
+ const other = entities.find(e => e.id === otherId);
+ const direction = r.from === node.id ? "→" : "←";
+ return (
+
{ if (other) onNodeSelect(other); }}
+ style={{ marginBottom: 4 }}
+ >
+
+ {direction} {other?.label}
+
+
+ {r.predicate.replace(/_/g, " ")}
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/ai-context/context-graph-demo/src/components/graph/ZoomControls.tsx b/ai-context/context-graph-demo/src/components/graph/ZoomControls.tsx
new file mode 100644
index 00000000..7e3e3ab3
--- /dev/null
+++ b/ai-context/context-graph-demo/src/components/graph/ZoomControls.tsx
@@ -0,0 +1,73 @@
+import { surface, border, text } from "../../theme";
+
+interface ZoomControlsProps {
+ zoom: number;
+ onZoomIn: () => void;
+ onZoomOut: () => void;
+ onReset: () => void;
+}
+
+export function ZoomControls({ zoom, onZoomIn, onZoomOut, onReset }: ZoomControlsProps) {
+ const buttonStyle: React.CSSProperties = {
+ width: 28,
+ height: 28,
+ border: "none",
+ borderRadius: 4,
+ background: border.medium,
+ color: text.subtle,
+ cursor: "pointer",
+ fontSize: 16,
+ fontWeight: "bold",
+ };
+
+ return (
+ <>
+ {/* Zoom controls */}
+
+
+
+
+
+
+ {/* Zoom indicator */}
+ {zoom !== 1 && (
+
+ {Math.round(zoom * 100)}%
+
+ )}
+ >
+ );
+}
diff --git a/ai-context/context-graph-demo/src/components/graph/index.ts b/ai-context/context-graph-demo/src/components/graph/index.ts
new file mode 100644
index 00000000..6f8ebcaf
--- /dev/null
+++ b/ai-context/context-graph-demo/src/components/graph/index.ts
@@ -0,0 +1,6 @@
+export { GraphCanvas } from "./GraphCanvas";
+export { GraphCanvasSVG } from "./GraphCanvasSVG";
+export { ExplainGraph } from "./ExplainGraph";
+export type { ExplainGraphNode, ExplainGraphEdge } from "./ExplainGraph";
+export { NodeDetailPanel } from "./NodeDetailPanel";
+export { ZoomControls } from "./ZoomControls";
diff --git a/ai-context/context-graph-demo/src/components/index.ts b/ai-context/context-graph-demo/src/components/index.ts
new file mode 100644
index 00000000..aca26425
--- /dev/null
+++ b/ai-context/context-graph-demo/src/components/index.ts
@@ -0,0 +1,7 @@
+// Common shared components
+export { SectionLabel, FilterButton, Header, StatusBar, Typewriter, Card, Badge, LoadingState, Toaster, SearchInput, FilterBar, MessageBubble } from "./common";
+export type { FilterItem, Message } from "./common";
+
+// Graph visualization components
+export { GraphCanvas, GraphCanvasSVG, ExplainGraph, NodeDetailPanel, ZoomControls } from "./graph";
+export type { ExplainGraphNode, ExplainGraphEdge } from "./graph";
diff --git a/ai-context/context-graph-demo/src/config.ts b/ai-context/context-graph-demo/src/config.ts
new file mode 100644
index 00000000..1a831d15
--- /dev/null
+++ b/ai-context/context-graph-demo/src/config.ts
@@ -0,0 +1,2 @@
+// TrustGraph collection identifier
+export const COLLECTION = "default";
diff --git a/ai-context/context-graph-demo/src/index.css b/ai-context/context-graph-demo/src/index.css
new file mode 100644
index 00000000..57ced3fc
--- /dev/null
+++ b/ai-context/context-graph-demo/src/index.css
@@ -0,0 +1,18 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+html, body, #root {
+ width: 100%;
+ height: 100%;
+ background: #0A0A0F;
+}
+
+body {
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
diff --git a/ai-context/context-graph-demo/src/main.tsx b/ai-context/context-graph-demo/src/main.tsx
new file mode 100644
index 00000000..9f851904
--- /dev/null
+++ b/ai-context/context-graph-demo/src/main.tsx
@@ -0,0 +1,29 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { SocketProvider } from '@trustgraph/react-provider'
+import { NotificationProvider, NotificationHandler } from '@trustgraph/react-state'
+import { toast } from './state'
+import './index.css'
+import App from './App'
+
+const queryClient = new QueryClient()
+
+const notificationHandler: NotificationHandler = {
+ success: (message: string) => toast.success(message),
+ error: (message: string) => toast.error(message),
+ warning: (message: string) => toast.warning(message),
+ info: (message: string) => toast.info(message),
+}
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+
+
+
+
+
+ ,
+)
diff --git a/ai-context/context-graph-demo/src/pages/DataView.tsx b/ai-context/context-graph-demo/src/pages/DataView.tsx
new file mode 100644
index 00000000..58e355e7
--- /dev/null
+++ b/ai-context/context-graph-demo/src/pages/DataView.tsx
@@ -0,0 +1,393 @@
+import { useState, useCallback, useMemo } from "react";
+import { SectionLabel, Card, LoadingState, SearchInput, FilterBar } from "../components";
+import type { FilterItem } from "../components";
+import { useSchemas, useEmbeddings, useRowEmbeddingsQuery, useRowsQuery } from "@trustgraph/react-state";
+import { COLLECTION } from "../config";
+import { semantic, palette, text, border, surface } from "../theme";
+
+// Schema field type
+interface SchemaField {
+ name: string;
+ type: string;
+ description?: string;
+}
+
+// Schema type based on what useSchemas returns
+interface SchemaData {
+ name: string;
+ description?: string;
+ fields?: SchemaField[];
+ indexes?: { name: string; fields: string[] }[];
+}
+
+interface SchemaInfo {
+ key: string;
+ name: string;
+ description?: string;
+ fields: SchemaField[];
+ indexes: { name: string; fields: string[] }[];
+}
+
+// Type for accumulated results with schema info and row data
+interface AccumulatedMatch {
+ schemaKey: string;
+ index_name: string;
+ index_value: string[];
+ text: string;
+ score: number;
+ rowData?: Record;
+}
+
+export function DataView() {
+ // Input state
+ const [searchTerm, setSearchTerm] = useState("");
+
+ // Filter state (display only - doesn't trigger re-fetch)
+ const [selectedSchema, setSelectedSchema] = useState(null);
+
+ // Results state
+ const [allMatches, setAllMatches] = useState([]);
+ const [isSearching, setIsSearching] = useState(false);
+ const [hasSearched, setHasSearched] = useState(false);
+
+ // Fetch schemas
+ const { schemas: rawSchemas, schemasLoading, schemasError } = useSchemas();
+
+ // Embeddings hook - we'll use refetch for manual triggering
+ const [embeddingsTerm, setEmbeddingsTerm] = useState("");
+ const { embeddings, isLoading: embeddingsLoading, refetch: _refetchEmbeddings } = useEmbeddings({
+ flow: "default",
+ term: embeddingsTerm,
+ });
+
+ // Row embeddings query
+ const { executeQueryAsync } = useRowEmbeddingsQuery({ flow: "default" });
+
+ // Rows query for fetching full row data
+ const { executeQueryAsync: executeRowsQueryAsync } = useRowsQuery({ flow: "default" });
+
+ // Parse schemas into usable format
+ const schemas: SchemaInfo[] = useMemo(() => {
+ return (rawSchemas || []).map((s: unknown, idx: number) => {
+ if (Array.isArray(s)) {
+ const schemaData = s[1] as SchemaData | undefined;
+ return {
+ key: String(s[0]),
+ name: schemaData?.name || String(s[0]),
+ description: schemaData?.description,
+ fields: schemaData?.fields || [],
+ indexes: schemaData?.indexes || [],
+ };
+ }
+ const schemaObj = s as SchemaData & { key?: string };
+ return {
+ key: schemaObj.key || schemaObj.name || `schema-${idx}`,
+ name: schemaObj.name || `Schema ${idx}`,
+ description: schemaObj.description,
+ fields: schemaObj.fields || [],
+ indexes: schemaObj.indexes || [],
+ };
+ });
+ }, [rawSchemas]);
+
+ // Build GraphQL query for a schema
+ const buildGraphQLQuery = useCallback((schema: SchemaInfo) => {
+ const gqlName = schema.key.replace(/-/g, '_');
+ const fieldNames = schema.fields.map(f => f.name).join('\n ');
+ return `query { ${gqlName} { ${fieldNames} } }`;
+ }, []);
+
+ // Core search function - searches ALL schemas, stores ALL results
+ const performSearch = useCallback(async (vectors: number[][]) => {
+ try {
+ // Always search ALL schemas
+ const embeddingsResults = await Promise.all(
+ schemas.map(async (schema) => {
+ try {
+ const matches = await executeQueryAsync({
+ vectors,
+ schemaName: schema.key,
+ collection: COLLECTION,
+ limit: 10,
+ });
+ return matches.map(m => ({ ...m, schemaKey: schema.key }));
+ } catch {
+ return [];
+ }
+ })
+ );
+
+ const flatMatches = embeddingsResults.flat();
+
+ // Deduplicate
+ const seen = new Set();
+ const uniqueMatches = flatMatches.filter(match => {
+ const key = `${match.schemaKey}:${match.index_value.join(',')}`;
+ if (seen.has(key)) return false;
+ seen.add(key);
+ return true;
+ });
+
+ // Fetch full row data for schemas with matches
+ const schemaKeysWithMatches = [...new Set(uniqueMatches.map(m => m.schemaKey))];
+ const rowDataBySchema: Record[]> = {};
+
+ await Promise.all(
+ schemaKeysWithMatches.map(async (schemaKey) => {
+ const schema = schemas.find(s => s.key === schemaKey);
+ if (!schema || schema.fields.length === 0) return;
+
+ try {
+ const query = buildGraphQLQuery(schema);
+ const result = await executeRowsQueryAsync({ query, collection: COLLECTION });
+ const gqlName = schemaKey.replace(/-/g, '_');
+ const rows = (result?.data as Record)?.[gqlName] || [];
+ rowDataBySchema[schemaKey] = rows as Record[];
+ } catch (err) {
+ console.error(`Failed to fetch rows for ${schemaKey}:`, err);
+ }
+ })
+ );
+
+ // Match row data to embeddings results
+ const matchesWithRowData = uniqueMatches.map(match => {
+ const rows = rowDataBySchema[match.schemaKey] || [];
+ const indexFields = match.index_name.split('.');
+ const indexFieldName = indexFields[indexFields.length - 1];
+
+ const matchedRow = rows.find(row => {
+ const rowValue = row[indexFieldName];
+ return match.index_value.some(iv =>
+ String(rowValue).toLowerCase() === iv.toLowerCase()
+ );
+ });
+
+ return { ...match, rowData: matchedRow };
+ });
+
+ setAllMatches(matchesWithRowData);
+ setHasSearched(true);
+ } finally {
+ setIsSearching(false);
+ }
+ }, [schemas, executeQueryAsync, executeRowsQueryAsync, buildGraphQLQuery]);
+
+ // Handle search button click
+ const handleSearch = useCallback(async () => {
+ const term = searchTerm.trim();
+ if (!term) return;
+
+ setIsSearching(true);
+ setAllMatches([]);
+
+ // If same term, use refetch; otherwise set new term
+ if (term === embeddingsTerm && embeddings && embeddings.length > 0) {
+ // Same term - we already have embeddings, just re-run the search
+ await performSearch(embeddings);
+ } else {
+ // New term - update embeddings term and wait for it
+ setEmbeddingsTerm(term);
+ }
+ }, [searchTerm, embeddingsTerm, embeddings, performSearch]);
+
+ // When embeddings become available for a new term, run the search
+ // This only triggers when embeddingsTerm changes and embeddings load
+ const prevEmbeddingsTermRef = useMemo(() => ({ current: "" }), []);
+
+ if (
+ isSearching &&
+ embeddingsTerm &&
+ embeddings &&
+ embeddings.length > 0 &&
+ !embeddingsLoading &&
+ prevEmbeddingsTermRef.current !== embeddingsTerm
+ ) {
+ prevEmbeddingsTermRef.current = embeddingsTerm;
+ performSearch(embeddings);
+ }
+
+ // Filter results for display (doesn't affect stored data)
+ const displayMatches = useMemo(() => {
+ if (!selectedSchema) return allMatches;
+ return allMatches.filter(m => m.schemaKey === selectedSchema);
+ }, [allMatches, selectedSchema]);
+
+ // Group filtered matches by schema for display
+ const matchesBySchema = useMemo(() => {
+ return displayMatches.reduce((acc, match) => {
+ if (!acc[match.schemaKey]) {
+ acc[match.schemaKey] = [];
+ }
+ acc[match.schemaKey].push(match);
+ return acc;
+ }, {} as Record);
+ }, [displayMatches]);
+
+ if (schemasLoading) {
+ return ;
+ }
+
+ if (schemasError) {
+ return ;
+ }
+
+ // Build filter items from schemas
+ const filterItems: FilterItem[] = schemas.slice(0, 10).map((schema) => ({
+ key: schema.key,
+ label: schema.name,
+ }));
+
+ const filterStats = selectedSchema
+ ? `${displayMatches.length} of ${allMatches.length} results`
+ : `${allMatches.length} results`;
+
+ return (
+
+ {/* Schema Filter Bar */}
+
+
+ {/* Search Input */}
+
+ SEARCH DATA
+
+
+
+ {/* Results Area */}
+
+ {!hasSearched && !isSearching ? (
+
+ Enter a search term to find data across tables.
+
+ ) : isSearching ? (
+
+ Searching...
+
+ ) : displayMatches.length === 0 ? (
+
+ {selectedSchema ? "No matches in this schema. Try selecting 'All'." : "No matches found."}
+
+ ) : (
+
+ {Object.entries(matchesBySchema).map(([schemaKey, schemaMatches]) => {
+ if (!schemaMatches || schemaMatches.length === 0) return null;
+ const schema = schemas.find(s => s.key === schemaKey);
+
+ return (
+
+ {/* Table Header */}
+
+
+ ▤ {schema?.name || schemaKey}
+
+
+ {schemaMatches.length} matches
+
+
+
+ {/* Results List */}
+
+ {schemaMatches.map((match, idx) => (
+
{
+ e.currentTarget.style.background = surface.card;
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.background = "transparent";
+ }}
+ >
+ {match.rowData ? (
+
+ {Object.entries(match.rowData).map(([key, value]) => (
+
+
+ {key}
+
+
+ {String(value ?? "")}
+
+
+ ))}
+
+ ) : (
+
+ {match.text}
+
+ )}
+
+
+ 0.8 ? semantic.success : match.score > 0.5 ? palette.amber : text.subtle,
+ }}>
+ {(match.score * 100).toFixed(1)}% match
+
+
+
+ ))}
+
+
+ );
+ })}
+
+ )}
+
+
+ );
+}
diff --git a/ai-context/context-graph-demo/src/pages/ExplainView.tsx b/ai-context/context-graph-demo/src/pages/ExplainView.tsx
new file mode 100644
index 00000000..5f69fbe5
--- /dev/null
+++ b/ai-context/context-graph-demo/src/pages/ExplainView.tsx
@@ -0,0 +1,1411 @@
+import { useState, useEffect, useRef, useCallback, useMemo } from "react";
+import { SectionLabel, SearchInput, ExplainGraph } from "../components";
+import type { ExplainGraphNode, ExplainGraphEdge } from "../components";
+import { COLLECTION } from "../config";
+import { useInference } from "@trustgraph/react-state";
+import type { ExplainEvent, Triple, Term } from "@trustgraph/react-state";
+import { useSocket } from "@trustgraph/react-provider";
+import type { BaseApi } from "@trustgraph/react-provider";
+import { palette, text, border, withGlow, semantic } from "../theme";
+
+// ── Namespaces ──────────────────────────────────────────────────────
+const TG = "https://trustgraph.ai/ns/";
+const TG_QUERY = TG + "query";
+const TG_CONCEPT = TG + "concept";
+const TG_ENTITY = TG + "entity";
+const TG_EDGE_COUNT = TG + "edgeCount";
+const TG_SELECTED_EDGE = TG + "selectedEdge";
+const TG_EDGE = TG + "edge";
+const TG_REASONING = TG + "reasoning";
+const TG_CONTENT = TG + "content";
+const TG_CONTAINS = TG + "contains";
+const TG_CHUNK_COUNT = TG + "chunkCount";
+const TG_ACTION = TG + "action";
+const TG_ARGUMENTS = TG + "arguments";
+const TG_THOUGHT = TG + "thought";
+const TG_OBSERVATION = TG + "observation";
+const TG_DOCUMENT = TG + "document";
+const RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
+const PROV = "http://www.w3.org/ns/prov#";
+const PROV_STARTED_AT_TIME = PROV + "startedAtTime";
+const PROV_WAS_DERIVED_FROM = PROV + "wasDerivedFrom";
+const RDFS_LABEL = "http://www.w3.org/2000/01/rdf-schema#label";
+
+// ── Types ───────────────────────────────────────────────────────────
+
+interface EdgeSelection {
+ edgeUri: string;
+ edge?: { s: string; p: string; o: string };
+ edgeLabels?: { s: string; p: string; o: string };
+ reasoning?: string;
+ sources?: ProvenanceChain[];
+}
+
+interface ProvenanceChain {
+ chain: { uri: string; label: string }[];
+}
+
+interface QuestionData {
+ query?: string;
+ timestamp?: string;
+}
+
+interface GroundingData {
+ concepts: string[];
+}
+
+interface ExplorationData {
+ edgeCount?: string;
+ chunkCount?: string;
+ entities: string[];
+ entityLabels?: string[];
+}
+
+interface FocusData {
+ edgeSelections: EdgeSelection[];
+}
+
+interface SynthesisData {
+ contentLength?: number;
+}
+
+interface AnalysisData {
+ action?: string;
+ arguments?: string;
+ thoughtUri?: string;
+ observationUri?: string;
+}
+
+interface ConclusionData {
+ documentUri?: string;
+}
+
+interface ReflectionData {
+ documentUri?: string;
+ reflectionType?: string;
+}
+
+type EventData = QuestionData | GroundingData | ExplorationData | FocusData | SynthesisData | AnalysisData | ConclusionData | ReflectionData;
+
+interface SourcePanelState {
+ chunkUri: string;
+ documentUri: string;
+ documentTitle?: string;
+ documentTags?: string[];
+ chunkText?: string;
+ loading: boolean;
+ error?: string;
+}
+
+interface ExplainNode {
+ explainId: string;
+ explainGraph: string;
+ eventType: string;
+ data?: EventData;
+ fetched: boolean;
+ fetching: boolean;
+ error?: string;
+}
+
+// ── Helpers ─────────────────────────────────────────────────────────
+
+function shortUri(uri: string): string {
+ if (uri.startsWith("urn:trustgraph:prov:")) return "tg:prov:" + uri.slice(20);
+ if (uri.startsWith("urn:trustgraph:")) return "tg:" + uri.slice(15);
+ if (uri.startsWith(TG)) return "tg:" + uri.slice(TG.length);
+ if (uri.startsWith(PROV)) return "prov:" + uri.slice(PROV.length);
+ if (uri.startsWith("http://www.w3.org/2000/01/rdf-schema#")) return "rdfs:" + uri.slice(37);
+ if (uri.startsWith("http://www.w3.org/1999/02/22-rdf-syntax-ns#")) return "rdf:" + uri.slice(43);
+ if (uri.startsWith("urn:")) return uri;
+ const pos = Math.max(uri.lastIndexOf("#"), uri.lastIndexOf("/"));
+ return pos >= 0 ? uri.slice(pos + 1) : uri;
+}
+
+// Ordered type checks — mirrors the Python ExplainEntity.from_triples logic.
+// Each entry: [type URI to look for, display name].
+// First match wins.
+const TYPE_CHECKS: [string, string][] = [
+ [TG + "GraphRagQuestion", "question"],
+ [TG + "DocRagQuestion", "question"],
+ [TG + "AgentQuestion", "question"],
+ [TG + "Question", "question"],
+ [TG + "Grounding", "grounding"],
+ [TG + "Exploration", "exploration"],
+ [TG + "Focus", "focus"],
+ [TG + "Synthesis", "synthesis"],
+ [TG + "Reflection", "reflection"],
+ [TG + "Thought", "reflection"],
+ [TG + "Observation", "reflection"],
+ [TG + "Analysis", "analysis"],
+ [TG + "Conclusion", "conclusion"],
+];
+
+function getEventTypeFromTriples(triples: Triple[]): string {
+ const types = new Set();
+ for (const t of triples) {
+ if (predIri(t) === RDF_TYPE) types.add(objValue(t));
+ }
+ for (const [typeUri, displayName] of TYPE_CHECKS) {
+ if (types.has(typeUri)) return displayName;
+ }
+ return "unknown";
+}
+
+function eventTypeColor(eventType: string): string {
+ switch (eventType) {
+ case "question": return palette.amber;
+ case "grounding": return palette.orange;
+ case "exploration": return palette.blue;
+ case "focus": return palette.purple;
+ case "analysis": return palette.purple;
+ case "reflection": return palette.cyan;
+ case "synthesis": return palette.emerald;
+ case "conclusion": return palette.emerald;
+ default: return text.muted;
+ }
+}
+
+// Get predicate IRI from a triple
+function predIri(triple: Triple): string {
+ return triple.p.t === "i" ? triple.p.i : "";
+}
+
+// Get object value (string) from a triple
+function objValue(triple: Triple): string {
+ const o = triple.o;
+ if (o.t === "i") return o.i;
+ if (o.t === "l") return o.v;
+ if (o.t === "b") return o.d;
+ return "";
+}
+
+// Get object as quoted triple {s, p, o} if it's a triple term
+function objQuotedTriple(triple: Triple): { s: string; p: string; o: string } | null {
+ const o = triple.o;
+ if (o.t === "t" && o.tr) {
+ return {
+ s: o.tr.s.t === "i" ? o.tr.s.i : (o.tr.s as any).v || "",
+ p: o.tr.p.t === "i" ? o.tr.p.i : (o.tr.p as any).v || "",
+ o: o.tr.o.t === "i" ? o.tr.o.i : (o.tr.o as any).v || "",
+ };
+ }
+ return null;
+}
+
+// ── KG query helpers (using the socket API) ─────────────────────────
+
+async function queryTriples(
+ api: ReturnType,
+ subject: string,
+ predicate?: string,
+ limit = 100,
+ collection = COLLECTION,
+ graph?: string,
+): Promise {
+ const s: Term = { t: "i", i: subject };
+ const p: Term | undefined = predicate ? { t: "i", i: predicate } : undefined;
+ return api.triplesQuery(s, p, undefined, limit, collection, graph);
+}
+
+// Backoff retry for eventually-consistent event triples.
+// Calls onUpdate each time new triples arrive, settles when two consecutive
+// fetches return the same count, or after maxTries (6 = 1 initial + 5 retries).
+// Backoff: 50ms × 3 each retry, capped at 1500ms.
+async function queryTriplesUntilSettled(
+ api: ReturnType,
+ subject: string,
+ onUpdate: (triples: Triple[]) => void,
+ limit = 100,
+ collection = COLLECTION,
+ graph?: string,
+ maxTries = 6,
+): Promise {
+ let prevCount = -1;
+ let settled: Triple[] = [];
+ let delay = 50;
+
+ for (let attempt = 0; attempt < maxTries; attempt++) {
+ const triples = await queryTriples(api, subject, undefined, limit, collection, graph);
+
+ if (triples.length !== prevCount) {
+ settled = triples;
+ onUpdate(triples);
+ } else {
+ // Two consecutive identical counts — settled
+ return settled;
+ }
+
+ prevCount = triples.length;
+
+ if (attempt < maxTries - 1) {
+ await new Promise(r => setTimeout(r, delay));
+ delay = Math.min(delay * 3, 1500);
+ }
+ }
+
+ return settled;
+}
+
+// Resolve rdfs:label for a URI, with cache
+async function resolveLabel(
+ api: ReturnType,
+ uri: string,
+ cache: Map,
+): Promise {
+ if (cache.has(uri)) return cache.get(uri)!;
+ try {
+ const triples = await api.triplesQuery(
+ { t: "i", i: uri },
+ { t: "i", i: RDFS_LABEL },
+ undefined,
+ 1,
+ COLLECTION,
+ );
+ const label = triples.length > 0 ? objValue(triples[0]) : shortUri(uri);
+ cache.set(uri, label);
+ return label;
+ } catch {
+ const fallback = shortUri(uri);
+ cache.set(uri, fallback);
+ return fallback;
+ }
+}
+
+// Trace prov:wasDerivedFrom chain up to root
+async function traceProvenanceChain(
+ api: ReturnType,
+ startUri: string,
+ labelCache: Map,
+ maxDepth = 10,
+): Promise {
+ const chain: { uri: string; label: string }[] = [];
+ let current: string | null = startUri;
+
+ for (let i = 0; i < maxDepth && current; i++) {
+ const label = await resolveLabel(api, current, labelCache);
+ chain.push({ uri: current, label });
+
+ // Find parent
+ const parentTriples = await api.triplesQuery(
+ { t: "i", i: current },
+ { t: "i", i: PROV_WAS_DERIVED_FROM },
+ undefined,
+ 1,
+ COLLECTION,
+ );
+
+ const parentUri = parentTriples.length > 0 ? objValue(parentTriples[0]) : null;
+ if (!parentUri || parentUri === current) break;
+ current = parentUri;
+ }
+
+ return { chain };
+}
+
+// Query edge provenance: find subgraphs containing the edge via tg:contains
+async function queryEdgeProvenance(
+ api: ReturnType,
+ edge: { s: string; p: string; o: string },
+ labelCache: Map,
+): Promise {
+ // Find subgraphs that contain this edge: ?subgraph tg:contains <>
+ const oTerm: Term = (edge.o.startsWith("http") || edge.o.startsWith("urn:"))
+ ? { t: "i", i: edge.o }
+ : { t: "l", v: edge.o };
+
+ const containsTriples = await api.triplesQuery(
+ undefined,
+ { t: "i", i: TG_CONTAINS },
+ {
+ t: "t",
+ tr: {
+ s: { t: "i", i: edge.s },
+ p: { t: "i", i: edge.p },
+ o: oTerm,
+ },
+ },
+ 10,
+ COLLECTION,
+ );
+
+ // For each subgraph, follow wasDerivedFrom to sources
+ const chains: ProvenanceChain[] = [];
+ for (const t of containsTriples) {
+ const subgraphUri = t.s.t === "i" ? t.s.i : "";
+ if (!subgraphUri) continue;
+
+ const derivedTriples = await api.triplesQuery(
+ { t: "i", i: subgraphUri },
+ { t: "i", i: PROV_WAS_DERIVED_FROM },
+ undefined,
+ 10,
+ COLLECTION,
+ );
+
+ for (const dt of derivedTriples) {
+ const sourceUri = objValue(dt);
+ if (sourceUri) {
+ const chain = await traceProvenanceChain(api, sourceUri, labelCache);
+ chains.push(chain);
+ }
+ }
+ }
+
+ return chains;
+}
+
+// ── Parse basic event data (synchronous, from already-fetched triples) ──
+
+function parseBasicEventData(eventType: string, triples: Triple[]): EventData {
+ switch (eventType) {
+ case "question": {
+ const data: QuestionData = {};
+ for (const t of triples) {
+ const p = predIri(t);
+ if (p === TG_QUERY) data.query = objValue(t);
+ if (p === PROV_STARTED_AT_TIME) data.timestamp = objValue(t);
+ }
+ return data;
+ }
+
+ case "grounding": {
+ const concepts: string[] = [];
+ for (const t of triples) {
+ if (predIri(t) === TG_CONCEPT) {
+ const v = objValue(t);
+ if (v) concepts.push(v);
+ }
+ }
+ return { concepts } as GroundingData;
+ }
+
+ case "exploration": {
+ const data: ExplorationData = { entities: [] };
+ for (const t of triples) {
+ const p = predIri(t);
+ if (p === TG_EDGE_COUNT) data.edgeCount = objValue(t);
+ if (p === TG_CHUNK_COUNT) data.chunkCount = objValue(t);
+ if (p === TG_ENTITY) {
+ const uri = objValue(t);
+ if (uri) data.entities.push(uri);
+ }
+ }
+ return data;
+ }
+
+ case "focus": {
+ const edgeSelUris: string[] = [];
+ for (const t of triples) {
+ if (predIri(t) === TG_SELECTED_EDGE) {
+ const uri = objValue(t);
+ if (uri) edgeSelUris.push(uri);
+ }
+ }
+ return {
+ edgeSelections: edgeSelUris.map(uri => ({ edgeUri: uri })),
+ } as FocusData;
+ }
+
+ case "synthesis": {
+ const data: SynthesisData = {};
+ for (const t of triples) {
+ if (predIri(t) === TG_CONTENT) {
+ data.contentLength = objValue(t).length;
+ }
+ }
+ return data;
+ }
+
+ case "analysis": {
+ const data: AnalysisData = {};
+ for (const t of triples) {
+ const p = predIri(t);
+ if (p === TG_ACTION) data.action = objValue(t);
+ if (p === TG_ARGUMENTS) data.arguments = objValue(t);
+ if (p === TG_THOUGHT) data.thoughtUri = objValue(t);
+ if (p === TG_OBSERVATION) data.observationUri = objValue(t);
+ }
+ return data;
+ }
+
+ case "conclusion": {
+ const data: ConclusionData = {};
+ for (const t of triples) {
+ if (predIri(t) === TG_DOCUMENT) data.documentUri = objValue(t);
+ }
+ return data;
+ }
+
+ case "reflection": {
+ const data: ReflectionData = {};
+ for (const t of triples) {
+ if (predIri(t) === TG_DOCUMENT) data.documentUri = objValue(t);
+ }
+ return data;
+ }
+
+ default:
+ return {};
+ }
+}
+
+// ── Enrich event data (async — labels, edge details, provenance) ────
+
+async function enrichEventData(
+ api: ReturnType,
+ eventType: string,
+ _triples: Triple[],
+ basicData: EventData,
+ labelCache: Map,
+ explainGraph: string,
+): Promise {
+ switch (eventType) {
+ case "exploration": {
+ const data = { ...(basicData as ExplorationData) };
+ if (data.entities.length > 0) {
+ data.entityLabels = await Promise.all(
+ data.entities.map(uri => resolveLabel(api, uri, labelCache))
+ );
+ }
+ return data;
+ }
+
+ case "focus": {
+ const basic = basicData as FocusData;
+ const edgeSelections = await Promise.all(basic.edgeSelections.map(async (basicSel) => {
+ const edgeTriples = await queryTriples(
+ api, basicSel.edgeUri, undefined, 100, COLLECTION, explainGraph,
+ );
+
+ const sel: EdgeSelection = { edgeUri: basicSel.edgeUri };
+ for (const et of edgeTriples) {
+ const p = predIri(et);
+ if (p === TG_EDGE) sel.edge = objQuotedTriple(et) || undefined;
+ if (p === TG_REASONING) sel.reasoning = objValue(et);
+ }
+
+ if (sel.edge) {
+ const [labels, sources] = await Promise.all([
+ Promise.all([
+ resolveLabel(api, sel.edge.s, labelCache),
+ resolveLabel(api, sel.edge.p, labelCache),
+ resolveLabel(api, sel.edge.o, labelCache),
+ ]),
+ queryEdgeProvenance(api, sel.edge, labelCache),
+ ]);
+ sel.edgeLabels = { s: labels[0], p: labels[1], o: labels[2] };
+ sel.sources = sources;
+ }
+
+ return sel;
+ }));
+
+ return { edgeSelections } as FocusData;
+ }
+
+ default:
+ return basicData;
+ }
+}
+
+// ── Component ───────────────────────────────────────────────────────
+
+type QueryMode = "graph-rag" | "doc-rag" | "agent";
+
+const queryModeLabels: Record = {
+ "graph-rag": "Graph RAG",
+ "doc-rag": "Doc RAG",
+ "agent": "Agent",
+};
+
+export function ExplainView() {
+ const [input, setInput] = useState("");
+ const [queryMode, setQueryMode] = useState("graph-rag");
+ const [response, setResponse] = useState("");
+ const [agentMessages, setAgentMessages] = useState<{ type: string; text: string; done?: boolean }[]>([]);
+ const [isQuerying, setIsQuerying] = useState(false);
+ const [explainNodes, setExplainNodes] = useState([]);
+ const [error, setError] = useState(null);
+ const [highlightedNodeIds, setHighlightedNodeIds] = useState([]);
+ const [highlightedEdgeIds, setHighlightedEdgeIds] = useState([]);
+ const [sourcePanel, setSourcePanel] = useState(null);
+ const scrollRef = useRef(null);
+ const explainScrollRef = useRef(null);
+ const labelCacheRef = useRef(new Map());
+
+ const { graphRag, documentRag, agent } = useInference({});
+ const socket = useSocket();
+
+ useEffect(() => {
+ scrollRef.current?.scrollIntoView({ behavior: "smooth" });
+ }, [response]);
+
+ useEffect(() => {
+ explainScrollRef.current?.scrollIntoView({ behavior: "smooth" });
+ }, [explainNodes]);
+
+ // Fetch event data when new nodes arrive
+ // Use a ref to access current nodes without re-rendering
+ const nodesRef = useRef(explainNodes);
+ nodesRef.current = explainNodes;
+
+ const fetchNode = useCallback(async (explainId: string) => {
+ setExplainNodes(prev => prev.map(n =>
+ n.explainId === explainId ? { ...n, fetching: true } : n
+ ));
+
+ try {
+ const api = socket.flow("default");
+ const node = nodesRef.current.find(n => n.explainId === explainId);
+ if (!node) return;
+
+ const updateNode = (updates: Partial) => {
+ setExplainNodes(prev => prev.map(n =>
+ n.explainId === explainId ? { ...n, ...updates } : n
+ ));
+ };
+
+ // Phase 1: Fetch event triples with backoff until settled.
+ // These are eventually consistent — render progressively as they arrive.
+ let latestEventType = "unknown";
+ let latestBasicData: EventData = {};
+
+ const settledTriples = await queryTriplesUntilSettled(
+ api, node.explainId,
+ (triples) => {
+ latestEventType = getEventTypeFromTriples(triples);
+ latestBasicData = parseBasicEventData(latestEventType, triples);
+ updateNode({ eventType: latestEventType, data: latestBasicData, fetched: true, fetching: false });
+ },
+ 100, COLLECTION, node.explainGraph,
+ );
+
+ if (settledTriples.length === 0) {
+ updateNode({ fetched: true, fetching: false });
+ return;
+ }
+
+ // Phase 2: Enrich with KG lookups (labels, edge details, provenance).
+ // These reference known-to-exist data — no retry needed, just fetch once.
+ const enriched = await enrichEventData(api, latestEventType, settledTriples, latestBasicData, labelCacheRef.current, node.explainGraph);
+ if (enriched !== latestBasicData) {
+ updateNode({ data: enriched });
+ }
+ } catch (err) {
+ setExplainNodes(prev => prev.map(n =>
+ n.explainId === explainId
+ ? { ...n, error: String(err), fetching: false }
+ : n
+ ));
+ }
+ }, [socket]);
+
+ useEffect(() => {
+ for (const node of explainNodes) {
+ if (!node.fetched && !node.fetching && !node.error) {
+ fetchNode(node.explainId);
+ }
+ }
+ }, [explainNodes, fetchNode]);
+
+ const addExplainEvent = useCallback((event: ExplainEvent) => {
+ setExplainNodes(prev => {
+ if (prev.some(n => n.explainId === event.explainId)) return prev;
+ return [...prev, {
+ explainId: event.explainId,
+ explainGraph: event.explainGraph,
+ eventType: "unknown",
+ fetched: false,
+ fetching: false,
+ }];
+ });
+ }, []);
+
+ const handleSubmit = useCallback(async (query: string) => {
+ if (!query.trim() || isQuerying) return;
+
+ setIsQuerying(true);
+ setResponse("");
+ setAgentMessages([]);
+ setExplainNodes([]);
+ setHighlightedNodeIds([]);
+ setHighlightedEdgeIds([]);
+ setSourcePanel(null);
+ setError(null);
+ setInput("");
+ labelCacheRef.current.clear();
+
+ const trimmed = query.trim();
+
+ try {
+ switch (queryMode) {
+ case "graph-rag": {
+ await graphRag({
+ input: trimmed,
+ collection: COLLECTION,
+ options: { maxSubgraphSize: 150 },
+ callbacks: {
+ onChunk: (chunk: string) => setResponse(prev => prev + chunk),
+ onExplain: addExplainEvent,
+ onError: (err: string) => setError(err),
+ },
+ });
+ break;
+ }
+
+ case "doc-rag": {
+ await documentRag({
+ input: trimmed,
+ collection: COLLECTION,
+ callbacks: {
+ onChunk: (chunk: string) => setResponse(prev => prev + chunk),
+ onExplain: addExplainEvent,
+ onError: (err: string) => setError(err),
+ },
+ });
+ break;
+ }
+
+ case "agent": {
+ // Track current streaming message per type
+ const accum: Record = {};
+
+ const appendChunk = (type: string, chunk: string, complete?: boolean) => {
+ accum[type] = (accum[type] || "") + chunk;
+ const currentText = accum[type];
+ setAgentMessages(prev => {
+ // Find existing in-progress message of this type at end
+ const lastIdx = prev.length - 1;
+ if (lastIdx >= 0 && prev[lastIdx].type === type && !prev[lastIdx].done) {
+ const updated = [...prev];
+ updated[lastIdx] = { type, text: currentText, done: !!complete };
+ return updated;
+ }
+ // New message
+ return [...prev, { type, text: currentText, done: !!complete }];
+ });
+ if (complete) {
+ accum[type] = "";
+ }
+ };
+
+ await agent({
+ input: trimmed,
+ callbacks: {
+ onThink: (chunk: string, complete?: boolean) => appendChunk("thinking", chunk, complete),
+ onObserve: (chunk: string, complete?: boolean) => appendChunk("observation", chunk, complete),
+ onAnswer: (chunk: string, complete?: boolean) => appendChunk("answer", chunk, complete),
+ onExplain: addExplainEvent,
+ onError: (err: string) => setError(err),
+ },
+ });
+ break;
+ }
+ }
+ } catch (err) {
+ setError(String(err));
+ } finally {
+ setIsQuerying(false);
+ }
+ }, [graphRag, documentRag, agent, queryMode, isQuerying, addExplainEvent]);
+
+ // ── Derive graph nodes and edges from explain events ──────────────
+ const { graphNodes, graphEdges } = useMemo(() => {
+ const nodeMap = new Map();
+ const edgeList: ExplainGraphEdge[] = [];
+
+ for (const node of explainNodes) {
+ if (!node.fetched || !node.data) continue;
+
+ if (node.eventType === "exploration") {
+ const d = node.data as ExplorationData;
+ const labels = d.entityLabels || [];
+ d.entities.forEach((uri, i) => {
+ if (!nodeMap.has(uri)) {
+ nodeMap.set(uri, { id: uri, label: labels[i] || shortUri(uri), color: palette.blue });
+ }
+ });
+ }
+
+ if (node.eventType === "focus") {
+ const d = node.data as FocusData;
+ for (const sel of d.edgeSelections) {
+ if (!sel.edge) continue;
+ const { s, p, o } = sel.edge;
+ const sLabel = sel.edgeLabels?.s || shortUri(s);
+ const pLabel = sel.edgeLabels?.p || shortUri(p);
+ const oLabel = sel.edgeLabels?.o || shortUri(o);
+
+ // Ensure nodes exist
+ if (!nodeMap.has(s)) nodeMap.set(s, { id: s, label: sLabel, color: palette.pink });
+ if (!nodeMap.has(o)) nodeMap.set(o, { id: o, label: oLabel, color: palette.pink });
+
+ edgeList.push({
+ id: sel.edgeUri,
+ from: s,
+ to: o,
+ label: pLabel,
+ reasoning: sel.reasoning,
+ });
+ }
+ }
+ }
+
+ return { graphNodes: Array.from(nodeMap.values()), graphEdges: edgeList };
+ }, [explainNodes]);
+
+ // ── Entity/edge click → neighbourhood highlight on graph ─────────
+ const handleEntityClick = useCallback((entityUri: string) => {
+ // Highlight this node + connected edges + neighbour nodes
+ const connectedEdges = graphEdges.filter(e => e.from === entityUri || e.to === entityUri);
+ const neighbourIds = new Set([entityUri]);
+ const edgeIds: string[] = [];
+ for (const e of connectedEdges) {
+ edgeIds.push(e.id);
+ neighbourIds.add(e.from);
+ neighbourIds.add(e.to);
+ }
+ setHighlightedNodeIds(Array.from(neighbourIds));
+ setHighlightedEdgeIds(edgeIds);
+ }, [graphEdges]);
+
+ const handleEdgeClick = useCallback((sel: EdgeSelection) => {
+ // Highlight this edge + its two endpoint nodes
+ const nodeIds: string[] = [];
+ if (sel.edge) {
+ nodeIds.push(sel.edge.s, sel.edge.o);
+ }
+ setHighlightedNodeIds(nodeIds);
+ setHighlightedEdgeIds([sel.edgeUri]);
+ }, []);
+
+ const handleSourceClick = useCallback((source: ProvenanceChain) => {
+ // chain[0] = chunk (closest to edge), chain[last] = root document
+ const chunkNode = source.chain[0];
+ const docNode = source.chain[source.chain.length - 1];
+ if (!chunkNode || !docNode) return;
+
+ // Same chunk — ignore (use the × button to close)
+ if (sourcePanel?.chunkUri === chunkNode.uri) return;
+
+ setSourcePanel({
+ chunkUri: chunkNode.uri,
+ documentUri: docNode.uri,
+ loading: true,
+ });
+
+ const librarian = socket.librarian();
+
+ // Fetch parent document metadata (title, tags) from librarian
+ librarian.getDocumentMetadata(docNode.uri).then(meta => {
+ setSourcePanel(prev => prev?.chunkUri === chunkNode.uri
+ ? { ...prev, documentTitle: meta?.title, documentTags: meta?.tags }
+ : prev
+ );
+ }).catch(() => {
+ // Metadata not available — that's OK
+ });
+
+ // The chunk URI is itself a document ID in the librarian — stream it directly
+ let chunkText = "";
+ librarian.streamDocument(
+ chunkNode.uri,
+ (content, _chunkIndex, _totalChunks, complete) => {
+ try {
+ chunkText += atob(content);
+ } catch {
+ chunkText += content;
+ }
+ if (complete) {
+ setSourcePanel(prev => prev?.chunkUri === chunkNode.uri
+ ? { ...prev, chunkText, loading: false }
+ : prev
+ );
+ }
+ },
+ (err) => {
+ setSourcePanel(prev => prev?.chunkUri === chunkNode.uri
+ ? { ...prev, loading: false, error: err }
+ : prev
+ );
+ },
+ );
+ }, [socket, sourcePanel?.chunkUri]);
+
+ return (
+
+ {/* LHS: Query + Response */}
+
+
+
{queryModeLabels[queryMode].toUpperCase()} QUERY
+
+ {(["graph-rag", "doc-rag", "agent"] as QueryMode[]).map(mode => (
+
+ ))}
+
+
handleSubmit(input)}
+ placeholder="Ask a question..."
+ buttonText="Query"
+ isLoading={isQuerying}
+ buttonColor={palette.cyan}
+ />
+
+
+
+ {error && (
+
+ )}
+
+ {!response && !isQuerying && !error && agentMessages.length === 0 && (
+
+ Ask a question to see {queryModeLabels[queryMode]} in action with live explainability.
+
+ )}
+
+ {/* Streaming response for graph-rag and doc-rag */}
+ {(response || (isQuerying && queryMode !== "agent")) && (
+
+ {response && (
+
+
+ ✓ RESPONSE
+
+
{response}
+
+ )}
+ {isQuerying && (
+
+ {response ? "Streaming..." : "Processing query..."}
+
+ )}
+
+ )}
+
+ {/* Agent messages */}
+ {queryMode === "agent" && agentMessages.length > 0 && (
+
+ {agentMessages.map((msg, i) => {
+ const colors: Record
= {
+ thinking: palette.purple,
+ observation: palette.blue,
+ answer: palette.emerald,
+ };
+ const color = colors[msg.type] || text.muted;
+ return (
+
+
+ {msg.type}
+
+
{msg.text}
+
+ );
+ })}
+
+ )}
+
+ {queryMode === "agent" && isQuerying && agentMessages.length === 0 && (
+
+ Agent is working...
+
+ )}
+
+
+
+
+ {/* Source text panel — shown when a provenance link is clicked */}
+ {sourcePanel && (
+
+ {/* Header with document metadata */}
+
+
+ SOURCE
+ {sourcePanel.documentTitle ? (
+
+ {sourcePanel.documentTitle}
+
+ ) : (
+
+ {shortUri(sourcePanel.documentUri)}
+
+ )}
+ {sourcePanel.documentTags && sourcePanel.documentTags.length > 0 && (
+
+ {sourcePanel.documentTags.map((tag, i) => (
+
+ {tag}
+
+ ))}
+
+ )}
+
+
+
+
+ {/* Chunk text content */}
+
+ {sourcePanel.loading && (
+
+ Loading source text...
+
+ )}
+ {sourcePanel.error && (
+
+ {sourcePanel.error}
+
+ )}
+ {sourcePanel.chunkText && (
+
+ {sourcePanel.chunkText}
+
+ )}
+
+
+ )}
+
+
+ {/* RHS: Graph + Explainability panel */}
+
+ {/* Graph view — top half */}
+
+ {
+ setHighlightedNodeIds(prev =>
+ prev.includes(nodeId) ? prev.filter(id => id !== nodeId) : [...prev, nodeId]
+ );
+ }}
+ onEdgeClick={(edgeId) => {
+ setHighlightedEdgeIds(prev =>
+ prev.includes(edgeId) ? prev.filter(id => id !== edgeId) : [...prev, edgeId]
+ );
+ }}
+ />
+
+
+ {/* Event cards — bottom half */}
+
+
+
+ EVENTS
+ {explainNodes.length > 0 && (
+
+ {explainNodes.length} event{explainNodes.length !== 1 ? "s" : ""}
+
+ )}
+
+
+
+
+ {explainNodes.length === 0 && !isQuerying && (
+
+ Explain events will appear here as the query progresses.
+
+ )}
+
+ {isQuerying && explainNodes.length === 0 && (
+
+ Waiting for explain events...
+
+ )}
+
+
+ {explainNodes.map((node, idx) => (
+
+ ))}
+
+
+
+
+
+
+ );
+}
+
+// ── ExplainCard ─────────────────────────────────────────────────────
+
+function ExplainCard({ node, index, onEntityClick, onEdgeClick, onSourceClick }: {
+ node: ExplainNode;
+ index: number;
+ onEntityClick?: (uri: string) => void;
+ onEdgeClick?: (sel: EdgeSelection) => void;
+ onSourceClick?: (source: ProvenanceChain) => void;
+}) {
+ const typeColor = eventTypeColor(node.eventType);
+
+ return (
+
+ {/* Header */}
+
+
+ {index + 1}
+
+
+ {node.eventType}
+
+ {node.fetching && (
+ loading...
+ )}
+
+
+ {/* Event data */}
+ {node.fetched && node.data && (
+
+ )}
+
+ {node.error && (
+
{node.error}
+ )}
+
+ );
+}
+
+// ── EventDataView ───────────────────────────────────────────────────
+
+function EventDataView({ eventType, data, onEntityClick, onEdgeClick, onSourceClick }: {
+ eventType: string;
+ data: EventData;
+ onEntityClick?: (uri: string) => void;
+ onEdgeClick?: (sel: EdgeSelection) => void;
+ onSourceClick?: (source: ProvenanceChain) => void;
+}) {
+ const mono = { fontFamily: "'IBM Plex Mono', monospace" } as const;
+
+ switch (eventType) {
+ case "question": {
+ const d = data as QuestionData;
+ return (
+
+ {d.query && (
+
+ Query: {d.query}
+
+ )}
+ {d.timestamp && (
+
+ {d.timestamp}
+
+ )}
+
+ );
+ }
+
+ case "grounding": {
+ const d = data as GroundingData;
+ return (
+
+ {d.concepts.length > 0 && (
+ <>
+
+ {d.concepts.length} concept{d.concepts.length !== 1 ? "s" : ""} extracted
+
+
+ {d.concepts.map((concept, i) => (
+
+ {concept}
+
+ ))}
+
+ >
+ )}
+
+ );
+ }
+
+ case "exploration": {
+ const d = data as ExplorationData;
+ return (
+
+ {d.edgeCount && (
+
+ Subgraph extracted: {d.edgeCount} edges
+
+ )}
+ {d.chunkCount && (
+
+ Chunks retrieved: {d.chunkCount}
+
+ )}
+ {d.entityLabels && d.entityLabels.length > 0 && (
+
+
+ {d.entityLabels.length} seed entit{d.entityLabels.length !== 1 ? "ies" : "y"}
+
+
+ {d.entityLabels.map((label, i) => (
+ onEntityClick?.(d.entities[i])}
+ style={{
+ fontSize: 11, padding: "3px 8px", borderRadius: 4,
+ background: withGlow(palette.blue, 0.1),
+ border: `1px solid ${withGlow(palette.blue, 0.2)}`,
+ color: text.secondary, ...mono,
+ cursor: onEntityClick ? "pointer" : "default",
+ transition: "all 0.15s ease",
+ }}
+ onMouseEnter={e => { if (onEntityClick) (e.currentTarget.style.background = withGlow(palette.blue, 0.25)); }}
+ onMouseLeave={e => { (e.currentTarget.style.background = withGlow(palette.blue, 0.1)); }}
+ >
+ {label}
+
+ ))}
+
+
+ )}
+
+ );
+ }
+
+ case "focus": {
+ const d = data as FocusData;
+ return (
+
+ {d.edgeSelections && d.edgeSelections.length > 0 && (
+ <>
+
+ Focused on {d.edgeSelections.length} edge{d.edgeSelections.length !== 1 ? "s" : ""}
+
+ {d.edgeSelections.map((sel, i) => (
+
onEdgeClick?.(sel)} onSourceClick={onSourceClick} />
+ ))}
+ >
+ )}
+
+ );
+ }
+
+ case "synthesis": {
+ const d = data as SynthesisData;
+ return (
+
+ {d.contentLength != null && (
+
+ Synthesis: {d.contentLength} chars
+
+ )}
+
+ );
+ }
+
+ case "analysis": {
+ const d = data as AnalysisData;
+ let parsedArgs: Record | null = null;
+ if (d.arguments) {
+ try { parsedArgs = JSON.parse(d.arguments); } catch { /* ignore */ }
+ }
+ return (
+
+ {d.action && (
+
+ Tool: {d.action}
+
+ )}
+ {parsedArgs && Object.entries(parsedArgs).map(([key, val]) => (
+
+ {key}: {String(val)}
+
+ ))}
+ {!parsedArgs && d.arguments && (
+
+ {d.arguments}
+
+ )}
+
+ );
+ }
+
+ case "conclusion": {
+ const d = data as ConclusionData;
+ return (
+
+ {d.documentUri && (
+
+ {shortUri(d.documentUri)}
+
+ )}
+
+ );
+ }
+
+ case "reflection": {
+ const d = data as ReflectionData;
+ return (
+
+ {d.documentUri && (
+
+ {shortUri(d.documentUri)}
+
+ )}
+
+ );
+ }
+
+ default:
+ return null;
+ }
+}
+
+// ── EdgeSelectionView ───────────────────────────────────────────────
+
+function EdgeSelectionView({ sel, onClick, onSourceClick }: {
+ sel: EdgeSelection;
+ onClick?: () => void;
+ onSourceClick?: (source: ProvenanceChain) => void;
+}) {
+ const mono = { fontFamily: "'IBM Plex Mono', monospace" } as const;
+
+ return (
+ { if (onClick) e.currentTarget.style.background = withGlow(palette.purple, 0.08); }}
+ onMouseLeave={e => { e.currentTarget.style.background = "transparent"; }}
+ >
+ {/* Edge triple */}
+ {sel.edgeLabels && (
+
+ {sel.edgeLabels.s}
+ →
+ {sel.edgeLabels.p}
+ →
+ {sel.edgeLabels.o}
+
+ )}
+
+ {/* Provenance sources — clickable to view source text */}
+ {sel.sources && sel.sources.length > 0 && (
+
+ {sel.sources.map((source, si) => {
+ const chainLabel = source.chain.map(c => c.label).join(" → ");
+ return (
+ {
+ e.stopPropagation();
+ onSourceClick?.(source);
+ }}
+ title={`View source: ${chainLabel}`}
+ style={{
+ fontSize: 10, padding: "2px 7px", borderRadius: 4,
+ background: withGlow(palette.amber, 0.08),
+ border: `1px solid ${withGlow(palette.amber, 0.2)}`,
+ color: text.hint, ...mono,
+ cursor: onSourceClick ? "pointer" : "default",
+ transition: "all 0.15s ease",
+ }}
+ onMouseEnter={e => { if (onSourceClick) { e.currentTarget.style.background = withGlow(palette.amber, 0.2); e.currentTarget.style.color = palette.amber; } }}
+ onMouseLeave={e => { e.currentTarget.style.background = withGlow(palette.amber, 0.08); e.currentTarget.style.color = text.hint; }}
+ >
+ {chainLabel}
+
+ );
+ })}
+
+ )}
+
+ {/* Reasoning - compact */}
+ {sel.reasoning && (
+
+ {sel.reasoning.length > 120 ? sel.reasoning.slice(0, 120) + "..." : sel.reasoning}
+
+ )}
+
+ );
+}
diff --git a/ai-context/context-graph-demo/src/pages/GraphView.tsx b/ai-context/context-graph-demo/src/pages/GraphView.tsx
new file mode 100644
index 00000000..4ddaadb7
--- /dev/null
+++ b/ai-context/context-graph-demo/src/pages/GraphView.tsx
@@ -0,0 +1,93 @@
+import type { DomainKey, Entity, OntologyDomain } from "../types";
+import { GraphCanvasSVG as GraphCanvas, NodeDetailPanel, LoadingState, FilterBar } from "../components";
+import type { FilterItem } from "../components";
+import { useGraphData } from "../state";
+
+interface GraphViewProps {
+ activeFilter: DomainKey | null;
+ onFilterChange: (filter: DomainKey | null) => void;
+ selectedNode: Entity | null;
+ onNodeSelect: (node: Entity | null) => void;
+}
+
+export function GraphView({ activeFilter, onFilterChange, selectedNode, onNodeSelect }: GraphViewProps) {
+ const { entities, relationships, ontology, propertyLabels, isLoading, isError } = useGraphData();
+
+ const highlightedEntities = selectedNode
+ ? [selectedNode.id, ...relationships.filter(r => r.from === selectedNode.id || r.to === selectedNode.id).map(r => r.from === selectedNode.id ? r.to : r.from)]
+ : [];
+
+ // Compute relevant filter domains based on selected node's connections
+ const relevantDomains = selectedNode
+ ? (() => {
+ const domains = new Set([selectedNode.domain]);
+ const connectedIds = relationships
+ .filter(r => r.from === selectedNode.id || r.to === selectedNode.id)
+ .map(r => r.from === selectedNode.id ? r.to : r.from);
+ for (const id of connectedIds) {
+ const entity = entities.find(e => e.id === id);
+ if (entity) domains.add(entity.domain);
+ }
+ return domains;
+ })()
+ : null;
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (isError || !ontology) {
+ return ;
+ }
+
+ // Build filter items from relevant domains
+ const filterItems: FilterItem[] = selectedNode
+ ? (Object.entries(ontology) as [DomainKey, OntologyDomain][])
+ .filter(([key]) => relevantDomains?.has(key))
+ .slice(0, 10)
+ .map(([key, data]) => ({
+ key,
+ label: data.label,
+ icon: data.icon,
+ color: data.color,
+ }))
+ : [];
+
+ return (
+ <>
+ {/* Domain Filter Bar */}
+ onFilterChange(key as DomainKey | null)}
+ stats={`${entities.length} entities · ${relationships.length} relationships`}
+ emptyMessage={selectedNode ? undefined : "Select a node to filter"}
+ />
+
+ {/* Main Content */}
+
+
+ onNodeSelect(selectedNode?.id === node.id ? null : node)}
+ activeFilter={activeFilter}
+ />
+
+ {selectedNode && (
+
onNodeSelect(null)}
+ onNodeSelect={onNodeSelect}
+ />
+ )}
+
+ >
+ );
+}
diff --git a/ai-context/context-graph-demo/src/pages/OntologyView.tsx b/ai-context/context-graph-demo/src/pages/OntologyView.tsx
new file mode 100644
index 00000000..e5191ef7
--- /dev/null
+++ b/ai-context/context-graph-demo/src/pages/OntologyView.tsx
@@ -0,0 +1,116 @@
+import type { DomainKey, OntologyDomain } from "../types";
+import { SectionLabel, Card, Badge, LoadingState } from "../components";
+import { useGraphData, useOntologySchema } from "../state";
+import { getLocalName } from "../utils";
+import { text, surface, border } from "../theme";
+
+export function OntologyView() {
+ const { ontology, isLoading: graphLoading } = useGraphData();
+ const { schema, isLoading: schemaLoading } = useOntologySchema();
+
+ const isLoading = graphLoading || schemaLoading;
+
+ if (isLoading || !ontology || !schema) {
+ return ;
+ }
+
+ // Count total instances
+ const totalInstances = Object.values(ontology).reduce((sum, d) => sum + d.subclasses.length, 0);
+
+ return (
+
+
+
ONTOLOGY SCHEMA
+
+ {/* Ontology class cards */}
+
+ {(Object.entries(ontology) as [DomainKey, OntologyDomain][]).map(([key, data]) => {
+ // Find datatype properties for this domain from schema
+ const domainProps = schema.datatypeProperties
+ .filter(p => p.domain && getLocalName(p.domain) === data.label)
+ .map(p => p.label);
+
+ return (
+
+
+
{data.icon}
+
+
{data.label}
+
owl:Class
+
+
+ {data.description}
+ PROPERTIES ({domainProps.length})
+
+ {domainProps.map((p) => (
+ {p}
+ ))}
+
+ INSTANCES ({data.subclasses.length})
+ {data.subclasses.map((sc) => (
+
+ {sc.label}
+ {sc.id}
+
+ ))}
+
+ );
+ })}
+
+
+ {/* Relationship predicates (Object Properties) */}
+
+ RELATIONSHIP PREDICATES ({schema.objectProperties.length})
+
+ {schema.objectProperties.map((prop) => {
+ const fromDomain = prop.domain ? getLocalName(prop.domain).toLowerCase() as DomainKey : null;
+ const toDomain = prop.range ? getLocalName(prop.range).toLowerCase() as DomainKey : null;
+
+ return (
+
+
+ {prop.label}
+
+
+ {fromDomain && ontology[fromDomain] && (
+ {ontology[fromDomain].label}
+ )}
+ {fromDomain && toDomain && " → "}
+ {toDomain && ontology[toDomain] && (
+ {ontology[toDomain].label}
+ )}
+
+
+ );
+ })}
+
+
+
+ {/* Triple count summary */}
+
+ {[
+ { label: "Classes", value: schema.classes.length },
+ { label: "Instances", value: totalInstances },
+ { label: "Object Props", value: schema.objectProperties.length },
+ { label: "Data Props", value: schema.datatypeProperties.length },
+ ].map((s) => (
+
+
{s.value}
+
{s.label.toUpperCase()}
+
+ ))}
+
+
+
+ );
+}
diff --git a/ai-context/context-graph-demo/src/pages/QueryView.tsx b/ai-context/context-graph-demo/src/pages/QueryView.tsx
new file mode 100644
index 00000000..73b72972
--- /dev/null
+++ b/ai-context/context-graph-demo/src/pages/QueryView.tsx
@@ -0,0 +1,231 @@
+import { useState, useEffect, useRef } from "react";
+import { GraphCanvasSVG as GraphCanvas, NodeDetailPanel, SectionLabel, Badge, LoadingState, SearchInput, MessageBubble } from "../components";
+import { useGraphData } from "../state";
+import { COLLECTION } from "../config";
+import type { Entity } from "../types";
+import { useChat, useConversation, useEmbeddings, useGraphEmbeddings } from "@trustgraph/react-state";
+import { getLocalName } from "../utils";
+import { palette, text, border, withGlow } from "../theme";
+
+// Type for embedding result items
+interface EmbeddingResultItem {
+ id: string;
+ uri: string;
+ label: string;
+ color: string;
+ icon: string;
+ isEntity: boolean;
+}
+
+export function QueryView() {
+ const [customInput, setCustomInput] = useState("");
+ const [queryForEmbeddings, setQueryForEmbeddings] = useState(undefined);
+ const [selectedEntityId, setSelectedEntityId] = useState(null);
+ const [selectedNode, setSelectedNode] = useState(null);
+ const scrollRef = useRef(null);
+
+ const { entities, relationships, ontology, propertyLabels, isLoading: graphLoading } = useGraphData();
+ const { submitMessage, isSubmitting } = useChat();
+ const messages = useConversation((state) => state.messages);
+ const setChatMode = useConversation((state) => state.setChatMode);
+
+ // Get embeddings for the query text - only fetch when we have a committed query
+ const { embeddings, isLoading: embeddingsLoading } = useEmbeddings({
+ flow: "default",
+ term: queryForEmbeddings || undefined,
+ });
+
+ // Get graph entities from embeddings - only fetch when we have embeddings
+ const hasEmbeddings = embeddings && embeddings.length > 0;
+ const { graphEmbeddings, isLoading: graphEmbeddingsLoading } = useGraphEmbeddings({
+ vecs: hasEmbeddings ? embeddings : [[]],
+ limit: hasEmbeddings ? 10 : 0,
+ collection: COLLECTION,
+ });
+
+ // Set chat mode to agent on mount
+ useEffect(() => {
+ setChatMode("agent");
+ }, [setChatMode]);
+
+ // Auto-scroll to bottom when messages change
+ useEffect(() => {
+ scrollRef.current?.scrollIntoView({ behavior: "smooth" });
+ }, [messages]);
+
+ const handleSubmit = (query: string) => {
+ if (query.trim() && !isSubmitting) {
+ const trimmedQuery = query.trim();
+ submitMessage({ input: trimmedQuery });
+ setQueryForEmbeddings(trimmedQuery);
+ setSelectedEntityId(null);
+ setSelectedNode(null);
+ setCustomInput("");
+ }
+ };
+
+
+ // Match graph embedding entities to our loaded entities for labels and highlighting
+ // graphEmbeddings returns RDF terms: { t: "i", i: "http://..." }
+ // Only show matched entities, deduplicated by URI
+ const embeddingResults: EmbeddingResultItem[] = [];
+ const seenUris = new Set();
+
+ for (const ge of (hasEmbeddings && graphEmbeddings || []) as { t: string; i?: string }[]) {
+ const uri = ge.i;
+ if (!uri || seenUris.has(uri)) continue;
+
+ const entityId = getLocalName(uri);
+ const found = entities.find(e => e.id === entityId || e.uri === uri);
+
+ // Only include actual entities, not properties/concepts
+ if (found) {
+ seenUris.add(uri);
+ embeddingResults.push({
+ id: entityId,
+ uri,
+ label: found.label,
+ color: found.color,
+ icon: found.icon,
+ isEntity: true,
+ });
+ }
+ }
+
+ // Auto-select first embedding result when results arrive
+ useEffect(() => {
+ if (embeddingResults.length > 0 && !selectedEntityId && !selectedNode) {
+ setSelectedEntityId(embeddingResults[0].id);
+ }
+ }, [embeddingResults.length, selectedEntityId, selectedNode]);
+
+ // Extract entity IDs for highlighting on graph
+ // Priority: selectedNode (graph click) > selectedEntityId (button click) > all embedding results
+ const highlightedEntities = (() => {
+ const focusId = selectedNode?.id || selectedEntityId;
+ if (!focusId) {
+ return embeddingResults.map(e => e.id);
+ }
+ // Find all entities connected to the focused entity
+ const connected = new Set([focusId]);
+ for (const rel of relationships) {
+ if (rel.from === focusId) {
+ connected.add(rel.to);
+ } else if (rel.to === focusId) {
+ connected.add(rel.from);
+ }
+ }
+ return Array.from(connected);
+ })();
+
+ if (graphLoading || !ontology) {
+ return ;
+ }
+
+ return (
+
+
+ {/* Query input area */}
+
+ AGENT QUERIES
+
+ handleSubmit(customInput)}
+ placeholder="Type your own question..."
+ buttonText="Ask"
+ isLoading={isSubmitting}
+ buttonColor={palette.amber}
+ />
+
+
+ {/* Related entities from graph embeddings */}
+ {queryForEmbeddings && (
+
+
+ RELATED ENTITIES {(embeddingsLoading || graphEmbeddingsLoading) && loading...}
+
+
+ {embeddingResults.length === 0 && !embeddingsLoading && !graphEmbeddingsLoading && (
+ No related concepts found
+ )}
+ {embeddingResults.map((item) => {
+ const isSelected = selectedEntityId === item.id;
+ return (
+ {
+ setSelectedEntityId(isSelected ? null : item.id);
+ setSelectedNode(null);
+ }}
+ >
+ {item.icon}
+ {item.label}
+
+ );
+ })}
+
+
+ )}
+
+ {/* Response area */}
+
+ {messages.length === 0 ? (
+
+ Type your question to get started.
+
+ ) : (
+
+ {messages.map((msg, idx) => (
+
+ ))}
+ {isSubmitting && (
+
+ Processing...
+
+ )}
+
+
+ )}
+
+
+
+ {/* Graph visualization */}
+
+ {
+ setSelectedNode(selectedNode?.id === node.id ? null : node);
+ setSelectedEntityId(null);
+ }}
+ activeFilter={null}
+ />
+
+ {selectedNode && (
+
setSelectedNode(null)}
+ onNodeSelect={(node) => {
+ setSelectedNode(node);
+ setSelectedEntityId(null);
+ }}
+ />
+ )}
+
+ );
+}
diff --git a/ai-context/context-graph-demo/src/pages/index.ts b/ai-context/context-graph-demo/src/pages/index.ts
new file mode 100644
index 00000000..08ad3690
--- /dev/null
+++ b/ai-context/context-graph-demo/src/pages/index.ts
@@ -0,0 +1,5 @@
+export { GraphView } from "./GraphView";
+export { QueryView } from "./QueryView";
+export { ExplainView } from "./ExplainView";
+export { DataView } from "./DataView";
+export { OntologyView } from "./OntologyView";
diff --git a/ai-context/context-graph-demo/src/state/index.ts b/ai-context/context-graph-demo/src/state/index.ts
new file mode 100644
index 00000000..32554341
--- /dev/null
+++ b/ai-context/context-graph-demo/src/state/index.ts
@@ -0,0 +1,9 @@
+// Main data hook - provides entities, relationships, and ontology
+export { useGraphData } from "./useGraphData";
+
+// Schema hook - for OWL ontology schema view
+export { useOntologySchema } from "./useOntologySchema";
+
+// Toast notifications
+export { useToastStore, toast } from "./toastStore";
+export type { Toast, ToastType } from "./toastStore";
diff --git a/ai-context/context-graph-demo/src/state/toastStore.ts b/ai-context/context-graph-demo/src/state/toastStore.ts
new file mode 100644
index 00000000..2f5f1f12
--- /dev/null
+++ b/ai-context/context-graph-demo/src/state/toastStore.ts
@@ -0,0 +1,52 @@
+import { create } from "zustand";
+
+export type ToastType = "success" | "error" | "warning" | "info";
+
+export interface Toast {
+ id: string;
+ type: ToastType;
+ message: string;
+ persistent?: boolean;
+}
+
+interface ToastStore {
+ toasts: Toast[];
+ addToast: (type: ToastType, message: string, persistent?: boolean) => void;
+ removeToast: (id: string) => void;
+}
+
+let toastId = 0;
+
+export const useToastStore = create((set) => ({
+ toasts: [],
+
+ addToast: (type, message, persistent = false) => {
+ const id = `toast-${++toastId}`;
+ set((state) => ({
+ toasts: [...state.toasts.slice(-3), { id, type, message, persistent }],
+ }));
+
+ // Auto-dismiss after 6 seconds unless explicitly persistent
+ if (!persistent) {
+ setTimeout(() => {
+ set((state) => ({
+ toasts: state.toasts.filter((t) => t.id !== id),
+ }));
+ }, 6000);
+ }
+ },
+
+ removeToast: (id) => {
+ set((state) => ({
+ toasts: state.toasts.filter((t) => t.id !== id),
+ }));
+ },
+}));
+
+// Helper functions for easy access outside React
+export const toast = {
+ success: (message: string) => useToastStore.getState().addToast("success", message),
+ error: (message: string) => useToastStore.getState().addToast("error", message),
+ warning: (message: string) => useToastStore.getState().addToast("warning", message),
+ info: (message: string) => useToastStore.getState().addToast("info", message),
+};
diff --git a/ai-context/context-graph-demo/src/state/useGraphData.ts b/ai-context/context-graph-demo/src/state/useGraphData.ts
new file mode 100644
index 00000000..001be00d
--- /dev/null
+++ b/ai-context/context-graph-demo/src/state/useGraphData.ts
@@ -0,0 +1,243 @@
+import { useState, useEffect, useMemo } from "react";
+import { useSocket } from "@trustgraph/react-provider";
+import type { Triple } from "@trustgraph/react-state";
+import type { Entity, Relationship, DomainKey, OntologyType } from "../types";
+import { COLLECTION } from "../config";
+import { domainColors } from "../theme";
+
+const RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
+const RDFS_LABEL = "http://www.w3.org/2000/01/rdf-schema#label";
+const RDFS_COMMENT = "http://www.w3.org/2000/01/rdf-schema#comment";
+const OWL_CLASS = "http://www.w3.org/2002/07/owl#Class";
+const OWL_DATATYPE_PROPERTY = "http://www.w3.org/2002/07/owl#DatatypeProperty";
+const OWL_OBJECT_PROPERTY = "http://www.w3.org/2002/07/owl#ObjectProperty";
+
+// Helper to extract value from a Term
+function getTermValue(term: { t: string; i?: string; v?: string }): string {
+ if (term.t === "i") return term.i || "";
+ if (term.t === "l") return term.v || "";
+ return "";
+}
+
+// Helper to create a short ID from a URI
+function uriToId(uri: string): string {
+ const hashIndex = uri.lastIndexOf("#");
+ const slashIndex = uri.lastIndexOf("/");
+ const index = Math.max(hashIndex, slashIndex);
+ return index >= 0 ? uri.substring(index + 1) : uri;
+}
+
+// Helper to get icon for a class (placeholder for now)
+function getClassIcon(_classUri: string): string {
+ return "●";
+}
+
+// Helper to extract predicate name from URI
+function predicateToName(uri: string): string {
+ const hashIndex = uri.lastIndexOf("#");
+ const slashIndex = uri.lastIndexOf("/");
+ const index = Math.max(hashIndex, slashIndex);
+ const name = index >= 0 ? uri.substring(index + 1) : uri;
+ return name.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
+}
+
+export function useGraphData(domain?: DomainKey) {
+ const socket = useSocket();
+ const [triples, setTriples] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isError, setIsError] = useState(false);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ let cancelled = false;
+
+ (async () => {
+ try {
+ setIsLoading(true);
+ setIsError(false);
+ setError(null);
+
+ const api = socket.flow("default");
+ const result = await api.triplesQuery(
+ undefined, undefined, undefined,
+ 10000, COLLECTION, "",
+ );
+
+ if (!cancelled) {
+ setTriples(result);
+ setIsLoading(false);
+ }
+ } catch (err) {
+ if (!cancelled) {
+ setIsError(true);
+ setError(err instanceof Error ? err : new Error(String(err)));
+ setIsLoading(false);
+ }
+ }
+ })();
+
+ return () => { cancelled = true; };
+ }, [socket]);
+
+ // Process all data from the query
+ const { entities, relationships, ontology, propertyLabels } = useMemo(() => {
+ if (isLoading || !triples) {
+ return { entities: [], relationships: [], ontology: undefined, propertyLabels: {} };
+ }
+
+ // First pass: collect all labels, comments, and find OWL classes and properties
+ const allLabels = new Map();
+ const allComments = new Map();
+ const owlClasses = new Set();
+ const propertyUris = new Set();
+
+ for (const triple of triples) {
+ const subjectUri = getTermValue(triple.s);
+ const predicate = getTermValue(triple.p);
+ const objectUri = getTermValue(triple.o);
+
+ if (predicate === RDFS_LABEL) {
+ allLabels.set(subjectUri, getTermValue(triple.o));
+ } else if (predicate === RDFS_COMMENT) {
+ allComments.set(subjectUri, getTermValue(triple.o));
+ } else if (predicate === RDF_TYPE) {
+ if (objectUri === OWL_CLASS) {
+ owlClasses.add(subjectUri);
+ } else if (objectUri === OWL_DATATYPE_PROPERTY || objectUri === OWL_OBJECT_PROPERTY) {
+ propertyUris.add(subjectUri);
+ }
+ }
+ }
+
+ // Build property labels map: local name -> label
+ const propertyLabels: Record = {};
+ for (const propUri of propertyUris) {
+ const localName = uriToId(propUri);
+ const label = allLabels.get(propUri);
+ if (label) {
+ propertyLabels[localName] = label;
+ }
+ }
+
+ // Build class config dynamically from discovered OWL classes
+ const classConfig = new Map();
+ let colorIndex = 0;
+ for (const classUri of owlClasses) {
+ const localName = uriToId(classUri).toLowerCase();
+ const palette = domainColors[colorIndex % domainColors.length];
+ classConfig.set(classUri, {
+ domain: localName,
+ color: palette.color,
+ glow: palette.glow,
+ icon: getClassIcon(classUri),
+ label: allLabels.get(classUri) || uriToId(classUri),
+ description: allComments.get(classUri) || "",
+ });
+ colorIndex++;
+ }
+
+ // Second pass: find entities (instances of OWL classes) and their properties
+ const entityMap = new Map();
+ const entityProps = new Map>();
+
+ // Collect entity properties first
+ for (const triple of triples) {
+ const subjectUri = getTermValue(triple.s);
+ const predicate = getTermValue(triple.p);
+ const value = getTermValue(triple.o);
+
+ // Skip schema-level predicates and URIs as values
+ if (predicate !== RDF_TYPE && predicate !== RDFS_LABEL && predicate !== RDFS_COMMENT &&
+ value && !value.startsWith("http")) {
+ if (!entityProps.has(subjectUri)) {
+ entityProps.set(subjectUri, {});
+ }
+ const propName = uriToId(predicate);
+ entityProps.get(subjectUri)![propName] = value;
+ }
+ }
+
+ // Find entities by type (instances of OWL classes)
+ for (const triple of triples) {
+ const subjectUri = getTermValue(triple.s);
+ const predicate = getTermValue(triple.p);
+ const objectUri = getTermValue(triple.o);
+
+ if (predicate === RDF_TYPE && classConfig.has(objectUri)) {
+ const config = classConfig.get(objectUri)!;
+ if (domain && config.domain !== domain) continue;
+
+ const entityId = uriToId(subjectUri);
+ entityMap.set(subjectUri, {
+ id: entityId,
+ uri: subjectUri,
+ label: allLabels.get(subjectUri) || entityId,
+ props: entityProps.get(subjectUri) || {},
+ domain: config.domain,
+ color: config.color,
+ glow: config.glow,
+ icon: config.icon,
+ });
+ }
+ }
+
+ // Find relationships: triples where both subject and object are known entities
+ const relationships: Relationship[] = [];
+ const entityUris = new Set(entityMap.keys());
+
+ for (const triple of triples) {
+ const subjectUri = getTermValue(triple.s);
+ const predicate = getTermValue(triple.p);
+ const objectUri = getTermValue(triple.o);
+
+ // Skip rdf:type and rdfs:label
+ if (predicate === RDF_TYPE || predicate === RDFS_LABEL) continue;
+
+ // If both subject and object are entities, it's a relationship
+ if (entityUris.has(subjectUri) && entityUris.has(objectUri)) {
+ const fromEntity = entityMap.get(subjectUri)!;
+ const toEntity = entityMap.get(objectUri)!;
+
+ relationships.push({
+ from: fromEntity.id,
+ to: toEntity.id,
+ predicate: predicateToName(predicate),
+ domain: [fromEntity.domain, toEntity.domain],
+ });
+ }
+ }
+
+ const entities = Array.from(entityMap.values());
+
+ // Build ontology metadata dynamically from discovered classes
+ const ontology: OntologyType = {};
+ for (const [, config] of classConfig) {
+ ontology[config.domain] = {
+ label: config.label,
+ color: config.color,
+ glow: config.glow,
+ icon: config.icon,
+ description: config.description,
+ properties: [],
+ subclasses: entities.filter(e => e.domain === config.domain).map(e => ({
+ id: e.id,
+ uri: e.uri,
+ label: e.label,
+ props: e.props,
+ })),
+ };
+ }
+
+ return { entities, relationships, ontology, propertyLabels };
+ }, [isLoading, triples, domain]);
+
+ return {
+ entities,
+ relationships,
+ ontology,
+ propertyLabels,
+ isLoading,
+ isError,
+ error,
+ };
+}
diff --git a/ai-context/context-graph-demo/src/state/useOntologySchema.ts b/ai-context/context-graph-demo/src/state/useOntologySchema.ts
new file mode 100644
index 00000000..b5dcd14c
--- /dev/null
+++ b/ai-context/context-graph-demo/src/state/useOntologySchema.ts
@@ -0,0 +1,178 @@
+import { useMemo } from "react";
+import { useTriples } from "@trustgraph/react-state";
+import { COLLECTION } from "../config";
+const RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
+const RDFS_LABEL = "http://www.w3.org/2000/01/rdf-schema#label";
+const RDFS_DOMAIN = "http://www.w3.org/2000/01/rdf-schema#domain";
+const RDFS_RANGE = "http://www.w3.org/2000/01/rdf-schema#range";
+const RDFS_COMMENT = "http://www.w3.org/2000/01/rdf-schema#comment";
+const OWL_CLASS = "http://www.w3.org/2002/07/owl#Class";
+const OWL_OBJECT_PROPERTY = "http://www.w3.org/2002/07/owl#ObjectProperty";
+const OWL_DATATYPE_PROPERTY = "http://www.w3.org/2002/07/owl#DatatypeProperty";
+
+// Helper to extract value from a Term
+function getTermValue(term: { t: string; i?: string; v?: string }): string {
+ if (term.t === "i") return term.i || "";
+ if (term.t === "l") return term.v || "";
+ return "";
+}
+
+// Helper to get local name from URI
+function getLocalName(uri: string): string {
+ const hashIndex = uri.lastIndexOf("#");
+ const slashIndex = uri.lastIndexOf("/");
+ const index = Math.max(hashIndex, slashIndex);
+ return index >= 0 ? uri.substring(index + 1) : uri;
+}
+
+export interface OntologyClass {
+ uri: string;
+ label: string;
+ comment?: string;
+}
+
+export interface OntologyProperty {
+ uri: string;
+ label: string;
+ domain?: string;
+ range?: string;
+}
+
+export interface OntologySchema {
+ classes: OntologyClass[];
+ objectProperties: OntologyProperty[];
+ datatypeProperties: OntologyProperty[];
+ // Sets for quick lookup
+ objectPropertyUris: Set;
+ datatypePropertyUris: Set;
+}
+
+export function useOntologySchema() {
+ // Query for classes
+ const classTriples = useTriples({
+ p: { t: "i", i: RDF_TYPE },
+ o: { t: "i", i: OWL_CLASS },
+ limit: 100,
+ collection: COLLECTION,
+ });
+
+ // Query for object properties
+ const objectPropertyTriples = useTriples({
+ p: { t: "i", i: RDF_TYPE },
+ o: { t: "i", i: OWL_OBJECT_PROPERTY },
+ limit: 100,
+ collection: COLLECTION,
+ });
+
+ // Query for datatype properties
+ const datatypePropertyTriples = useTriples({
+ p: { t: "i", i: RDF_TYPE },
+ o: { t: "i", i: OWL_DATATYPE_PROPERTY },
+ limit: 100,
+ collection: COLLECTION,
+ });
+
+ // Query for all triples to get labels, domains, ranges
+ const allTriples = useTriples({
+ limit: 1000,
+ collection: COLLECTION,
+ });
+
+ const isLoading = classTriples.isLoading || objectPropertyTriples.isLoading ||
+ datatypePropertyTriples.isLoading || allTriples.isLoading;
+ const isError = classTriples.isError || objectPropertyTriples.isError ||
+ datatypePropertyTriples.isError || allTriples.isError;
+ const error = classTriples.error || objectPropertyTriples.error ||
+ datatypePropertyTriples.error || allTriples.error;
+
+ const schema = useMemo((): OntologySchema | undefined => {
+ if (isLoading) return undefined;
+
+ // Build a map of URI -> metadata from all triples
+ const metadata = new Map();
+
+ for (const triple of allTriples.triples || []) {
+ const subject = getTermValue(triple.s);
+ const predicate = getTermValue(triple.p);
+ const value = getTermValue(triple.o);
+
+ if (!metadata.has(subject)) {
+ metadata.set(subject, {});
+ }
+ const meta = metadata.get(subject)!;
+
+ if (predicate === RDFS_LABEL) {
+ meta.label = value;
+ } else if (predicate === RDFS_DOMAIN) {
+ meta.domain = value;
+ } else if (predicate === RDFS_RANGE) {
+ meta.range = value;
+ } else if (predicate === RDFS_COMMENT) {
+ meta.comment = value;
+ }
+ }
+
+ // Build classes list
+ const classes: OntologyClass[] = [];
+ for (const triple of classTriples.triples || []) {
+ const uri = getTermValue(triple.s);
+ const meta = metadata.get(uri) || {};
+ classes.push({
+ uri,
+ label: meta.label || getLocalName(uri),
+ comment: meta.comment,
+ });
+ }
+
+ // Build object properties list
+ const objectProperties: OntologyProperty[] = [];
+ const objectPropertyUris = new Set();
+ for (const triple of objectPropertyTriples.triples || []) {
+ const uri = getTermValue(triple.s);
+ const meta = metadata.get(uri) || {};
+ objectPropertyUris.add(uri);
+ objectProperties.push({
+ uri,
+ label: meta.label || getLocalName(uri),
+ domain: meta.domain,
+ range: meta.range,
+ });
+ }
+
+ // Build datatype properties list
+ const datatypeProperties: OntologyProperty[] = [];
+ const datatypePropertyUris = new Set();
+ for (const triple of datatypePropertyTriples.triples || []) {
+ const uri = getTermValue(triple.s);
+ const meta = metadata.get(uri) || {};
+ datatypePropertyUris.add(uri);
+ datatypeProperties.push({
+ uri,
+ label: meta.label || getLocalName(uri),
+ domain: meta.domain,
+ range: meta.range,
+ });
+ }
+
+ return {
+ classes,
+ objectProperties,
+ datatypeProperties,
+ objectPropertyUris,
+ datatypePropertyUris,
+ };
+ }, [
+ isLoading,
+ classTriples.triples,
+ objectPropertyTriples.triples,
+ datatypePropertyTriples.triples,
+ allTriples.triples,
+ ]);
+
+ return {
+ schema,
+ isLoading,
+ isError,
+ error,
+ };
+}
diff --git a/ai-context/context-graph-demo/src/theme/colors.ts b/ai-context/context-graph-demo/src/theme/colors.ts
new file mode 100644
index 00000000..c6ca9cb0
--- /dev/null
+++ b/ai-context/context-graph-demo/src/theme/colors.ts
@@ -0,0 +1,72 @@
+// Primary palette (migrated from useGraphData.ts)
+export const palette = {
+ emerald: "#6EE7B7",
+ pink: "#F9A8D4",
+ blue: "#93C5FD",
+ amber: "#FCD34D",
+ purple: "#C4B5FD",
+ rose: "#FDA4AF",
+ cyan: "#67E8F9",
+ red: "#FCA5A5",
+ orange: "#F97316",
+};
+
+// Semantic colors
+export const semantic = {
+ success: palette.emerald,
+ error: "#f66",
+ warning: palette.orange,
+ info: palette.blue,
+ thinking: palette.blue,
+ observation: palette.purple,
+ answer: palette.emerald,
+ user: palette.amber,
+};
+
+// Text colors (dark theme)
+export const text = {
+ primary: "#ddd",
+ secondary: "#bbb",
+ muted: "#aaa",
+ subtle: "#888",
+ faint: "#666",
+ disabled: "#555",
+ hint: "#444",
+};
+
+// Surface/background colors
+export const surface = {
+ base: "#0A0A0F",
+ overlay: "rgba(15,15,20,0.95)",
+ overlayLight: "rgba(15,15,20,0.8)",
+ card: "rgba(255,255,255,0.02)",
+ cardHover: "rgba(255,255,255,0.04)",
+};
+
+// Border colors
+export const border = {
+ subtle: "rgba(255,255,255,0.04)",
+ default: "rgba(255,255,255,0.06)",
+ medium: "rgba(255,255,255,0.1)",
+ grid: "rgba(255,255,255,0.015)",
+};
+
+// Helper: Generate glow color from hex
+export function withGlow(hex: string, opacity = 0.4): string {
+ const r = parseInt(hex.slice(1, 3), 16);
+ const g = parseInt(hex.slice(3, 5), 16);
+ const b = parseInt(hex.slice(5, 7), 16);
+ return `rgba(${r},${g},${b},${opacity})`;
+}
+
+// Domain color palette (array for cycling)
+export const domainColors = [
+ { color: palette.emerald, glow: withGlow(palette.emerald) },
+ { color: palette.pink, glow: withGlow(palette.pink) },
+ { color: palette.blue, glow: withGlow(palette.blue) },
+ { color: palette.amber, glow: withGlow(palette.amber) },
+ { color: palette.purple, glow: withGlow(palette.purple) },
+ { color: palette.rose, glow: withGlow(palette.rose) },
+ { color: palette.cyan, glow: withGlow(palette.cyan) },
+ { color: palette.red, glow: withGlow(palette.red) },
+];
diff --git a/ai-context/context-graph-demo/src/theme/index.ts b/ai-context/context-graph-demo/src/theme/index.ts
new file mode 100644
index 00000000..bbdc1712
--- /dev/null
+++ b/ai-context/context-graph-demo/src/theme/index.ts
@@ -0,0 +1 @@
+export * from "./colors";
diff --git a/ai-context/context-graph-demo/src/types/index.ts b/ai-context/context-graph-demo/src/types/index.ts
new file mode 100644
index 00000000..89dd67dd
--- /dev/null
+++ b/ai-context/context-graph-demo/src/types/index.ts
@@ -0,0 +1,65 @@
+// ── Domain Types ─────────────────────────────────────────────────
+export type DomainKey = string;
+
+export interface EntityProps {
+ [key: string]: string | number;
+}
+
+export interface Subclass {
+ id: string;
+ uri?: string;
+ label: string;
+ props: EntityProps;
+}
+
+export interface OntologyDomain {
+ label: string;
+ color: string;
+ glow: string;
+ icon: string;
+ description: string;
+ properties: string[];
+ subclasses: Subclass[];
+}
+
+export type OntologyType = Record;
+
+// ── Relationship Types ───────────────────────────────────────────
+export interface Relationship {
+ from: string;
+ to: string;
+ predicate: string;
+ strength?: number;
+ domain: [DomainKey, DomainKey];
+}
+
+// ── Query Types ──────────────────────────────────────────────────
+export interface DemoQuery {
+ q: string;
+ thinking: string[];
+ answer: string;
+ entities: string[];
+ triples: number;
+}
+
+// ── Entity Types ─────────────────────────────────────────────────
+export interface Entity extends Subclass {
+ domain: DomainKey;
+ color: string;
+ glow: string;
+ icon: string;
+}
+
+export interface GraphNode extends Entity {
+ x: number;
+ y: number;
+ vx: number;
+ vy: number;
+ targetX: number;
+ targetY: number;
+ r: number;
+}
+
+// ── UI State Types ───────────────────────────────────────────────
+export type TabKey = "graph" | "query" | "explain" | "ontology" | "data";
+export type QueryPhase = "idle" | "thinking" | "answering" | "done";
diff --git a/ai-context/context-graph-demo/src/utils/index.ts b/ai-context/context-graph-demo/src/utils/index.ts
new file mode 100644
index 00000000..1883c845
--- /dev/null
+++ b/ai-context/context-graph-demo/src/utils/index.ts
@@ -0,0 +1 @@
+export { getLocalName } from "./uri";
diff --git a/ai-context/context-graph-demo/src/utils/uri.ts b/ai-context/context-graph-demo/src/utils/uri.ts
new file mode 100644
index 00000000..7f7e5fab
--- /dev/null
+++ b/ai-context/context-graph-demo/src/utils/uri.ts
@@ -0,0 +1,9 @@
+/**
+ * Extract the local name from a URI by taking the fragment after # or the last path segment
+ */
+export function getLocalName(uri: string): string {
+ const hashIndex = uri.lastIndexOf("#");
+ const slashIndex = uri.lastIndexOf("/");
+ const index = Math.max(hashIndex, slashIndex);
+ return index >= 0 ? uri.substring(index + 1) : uri;
+}
diff --git a/ai-context/context-graph-demo/src/vite-env.d.ts b/ai-context/context-graph-demo/src/vite-env.d.ts
new file mode 100644
index 00000000..11f02fe2
--- /dev/null
+++ b/ai-context/context-graph-demo/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/ai-context/context-graph-demo/trustgraph-retail-demo.jsx b/ai-context/context-graph-demo/trustgraph-retail-demo.jsx
new file mode 100644
index 00000000..32f4c01e
--- /dev/null
+++ b/ai-context/context-graph-demo/trustgraph-retail-demo.jsx
@@ -0,0 +1,843 @@
+import { useState, useEffect, useRef, useCallback, useMemo } from "react";
+
+// ═══════════════════════════════════════════════════════════════════
+// TRUSTGRAPH RETAIL INTELLIGENCE DEMO
+// Ontology-Driven Context Graph: Consumer × Agent × Retail × Brand
+// ═══════════════════════════════════════════════════════════════════
+
+// ── Ontology Schema ──────────────────────────────────────────────
+const ONTOLOGY = {
+ consumer: {
+ label: "Consumer",
+ color: "#6EE7B7",
+ glow: "rgba(110,231,183,0.4)",
+ icon: "👤",
+ description: "Individuals and segments interacting with brands through retail channels",
+ properties: ["segment", "preferences", "journeyStage", "lifetime_value", "sentiment"],
+ subclasses: [
+ { id: "cs1", label: "Urban Millennials", props: { size: "2.4M", avgSpend: "$142/mo", loyalty: 0.78, journeyStage: "Engaged" } },
+ { id: "cs2", label: "Active Families", props: { size: "1.8M", avgSpend: "$218/mo", loyalty: 0.85, journeyStage: "Loyal" } },
+ { id: "cs3", label: "Eco-Conscious Gen Z", props: { size: "3.1M", avgSpend: "$96/mo", loyalty: 0.62, journeyStage: "Exploring" } },
+ { id: "cs4", label: "Luxury Seekers", props: { size: "890K", avgSpend: "$384/mo", loyalty: 0.91, journeyStage: "Advocate" } },
+ { id: "cs5", label: "Weekend Warriors", props: { size: "1.5M", avgSpend: "$167/mo", loyalty: 0.73, journeyStage: "Engaged" } },
+ ],
+ },
+ brand: {
+ label: "Brand",
+ color: "#F9A8D4",
+ glow: "rgba(249,168,212,0.4)",
+ icon: "✦",
+ description: "Product brands seeking to connect with consumers through retail experiences",
+ properties: ["identity", "positioning", "campaigns", "products", "partnerships"],
+ subclasses: [
+ { id: "br1", label: "Lumière Beauty", props: { category: "Cosmetics", positioning: "Premium", campaigns: 12, sentiment: 0.87 } },
+ { id: "br2", label: "Nordic Trail", props: { category: "Outdoor Apparel", positioning: "Sustainable", campaigns: 8, sentiment: 0.82 } },
+ { id: "br3", label: "Velo Sport", props: { category: "Athletics", positioning: "Performance", campaigns: 15, sentiment: 0.79 } },
+ { id: "br4", label: "Casa Verde", props: { category: "Home & Living", positioning: "Artisanal", campaigns: 6, sentiment: 0.90 } },
+ { id: "br5", label: "Artisan Coffee Co.", props: { category: "F&B", positioning: "Community", campaigns: 10, sentiment: 0.85 } },
+ ],
+ },
+ retail: {
+ label: "Retail",
+ color: "#93C5FD",
+ glow: "rgba(147,197,253,0.4)",
+ icon: "🏬",
+ description: "Channels, touchpoints, and experiences where brands meet consumers",
+ properties: ["channel", "location", "traffic", "conversionRate", "experience_score"],
+ subclasses: [
+ { id: "rt1", label: "Flagship Store NYC", props: { channel: "Physical", traffic: "48K/mo", conversion: "12.3%", experience: 0.91 } },
+ { id: "rt2", label: "Mobile Commerce App", props: { channel: "Digital", traffic: "1.2M/mo", conversion: "4.7%", experience: 0.78 } },
+ { id: "rt3", label: "Pop-Up Experience", props: { channel: "Experiential", traffic: "8K/event", conversion: "18.6%", experience: 0.95 } },
+ { id: "rt4", label: "Social Commerce", props: { channel: "Social", traffic: "890K/mo", conversion: "3.2%", experience: 0.72 } },
+ { id: "rt5", label: "Loyalty Hub", props: { channel: "Omnichannel", traffic: "340K/mo", conversion: "22.1%", experience: 0.88 } },
+ ],
+ },
+ agent: {
+ label: "Agent",
+ color: "#FCD34D",
+ glow: "rgba(252,211,77,0.4)",
+ icon: "⚡",
+ description: "AI agents that orchestrate personalized brand-consumer connections",
+ properties: ["capability", "contextSources", "accuracy", "latency", "decisions_per_day"],
+ subclasses: [
+ { id: "ag1", label: "Recommendation Agent", props: { capability: "Product Discovery", accuracy: "94.2%", latency: "120ms", decisions: "2.1M/day" } },
+ { id: "ag2", label: "Personalization Agent", props: { capability: "Experience Tailoring", accuracy: "91.8%", latency: "85ms", decisions: "890K/day" } },
+ { id: "ag3", label: "Campaign Orchestrator", props: { capability: "Brand Activation", accuracy: "88.5%", latency: "200ms", decisions: "340K/day" } },
+ { id: "ag4", label: "Sentiment Analyst", props: { capability: "Brand Perception", accuracy: "96.1%", latency: "150ms", decisions: "1.5M/day" } },
+ { id: "ag5", label: "Journey Optimizer", props: { capability: "Path Optimization", accuracy: "89.7%", latency: "180ms", decisions: "560K/day" } },
+ ],
+ },
+};
+
+// ── Ontology Relationships (Triples) ──────────────────────────────
+const RELATIONSHIPS = [
+ // Consumer ↔ Brand
+ { from: "cs1", to: "br1", predicate: "has_affinity_for", strength: 0.85, domain: ["consumer", "brand"] },
+ { from: "cs1", to: "br5", predicate: "frequents", strength: 0.69, domain: ["consumer", "brand"] },
+ { from: "cs2", to: "br2", predicate: "has_affinity_for", strength: 0.78, domain: ["consumer", "brand"] },
+ { from: "cs2", to: "br3", predicate: "purchases_from", strength: 0.88, domain: ["consumer", "brand"] },
+ { from: "cs3", to: "br2", predicate: "advocates_for", strength: 0.71, domain: ["consumer", "brand"] },
+ { from: "cs3", to: "br4", predicate: "has_affinity_for", strength: 0.65, domain: ["consumer", "brand"] },
+ { from: "cs3", to: "br5", predicate: "frequents", strength: 0.58, domain: ["consumer", "brand"] },
+ { from: "cs4", to: "br1", predicate: "loyal_to", strength: 0.92, domain: ["consumer", "brand"] },
+ { from: "cs4", to: "br4", predicate: "purchases_from", strength: 0.82, domain: ["consumer", "brand"] },
+ { from: "cs5", to: "br3", predicate: "has_affinity_for", strength: 0.76, domain: ["consumer", "brand"] },
+ { from: "cs5", to: "br5", predicate: "frequents", strength: 0.74, domain: ["consumer", "brand"] },
+ // Consumer ↔ Retail
+ { from: "cs1", to: "rt2", predicate: "shops_via", strength: 0.82, domain: ["consumer", "retail"] },
+ { from: "cs1", to: "rt4", predicate: "discovers_through", strength: 0.71, domain: ["consumer", "retail"] },
+ { from: "cs2", to: "rt1", predicate: "shops_via", strength: 0.85, domain: ["consumer", "retail"] },
+ { from: "cs2", to: "rt5", predicate: "member_of", strength: 0.90, domain: ["consumer", "retail"] },
+ { from: "cs3", to: "rt3", predicate: "experiences", strength: 0.88, domain: ["consumer", "retail"] },
+ { from: "cs3", to: "rt4", predicate: "discovers_through", strength: 0.79, domain: ["consumer", "retail"] },
+ { from: "cs4", to: "rt1", predicate: "shops_via", strength: 0.94, domain: ["consumer", "retail"] },
+ { from: "cs4", to: "rt3", predicate: "experiences", strength: 0.86, domain: ["consumer", "retail"] },
+ { from: "cs5", to: "rt2", predicate: "shops_via", strength: 0.72, domain: ["consumer", "retail"] },
+ { from: "cs5", to: "rt5", predicate: "member_of", strength: 0.68, domain: ["consumer", "retail"] },
+ // Brand ↔ Retail
+ { from: "br1", to: "rt1", predicate: "merchandises_in", strength: 0.90, domain: ["brand", "retail"] },
+ { from: "br1", to: "rt3", predicate: "activates_via", strength: 0.85, domain: ["brand", "retail"] },
+ { from: "br2", to: "rt3", predicate: "activates_via", strength: 0.82, domain: ["brand", "retail"] },
+ { from: "br2", to: "rt4", predicate: "promotes_on", strength: 0.75, domain: ["brand", "retail"] },
+ { from: "br3", to: "rt2", predicate: "sells_through", strength: 0.80, domain: ["brand", "retail"] },
+ { from: "br3", to: "rt5", predicate: "rewards_via", strength: 0.77, domain: ["brand", "retail"] },
+ { from: "br4", to: "rt1", predicate: "merchandises_in", strength: 0.88, domain: ["brand", "retail"] },
+ { from: "br4", to: "rt4", predicate: "promotes_on", strength: 0.70, domain: ["brand", "retail"] },
+ { from: "br5", to: "rt3", predicate: "activates_via", strength: 0.79, domain: ["brand", "retail"] },
+ { from: "br5", to: "rt5", predicate: "rewards_via", strength: 0.83, domain: ["brand", "retail"] },
+ // Agent ↔ Consumer
+ { from: "ag1", to: "cs1", predicate: "recommends_to", strength: 0.87, domain: ["agent", "consumer"] },
+ { from: "ag1", to: "cs3", predicate: "recommends_to", strength: 0.81, domain: ["agent", "consumer"] },
+ { from: "ag2", to: "cs4", predicate: "personalizes_for", strength: 0.93, domain: ["agent", "consumer"] },
+ { from: "ag2", to: "cs2", predicate: "personalizes_for", strength: 0.84, domain: ["agent", "consumer"] },
+ { from: "ag4", to: "cs1", predicate: "monitors_sentiment_of", strength: 0.78, domain: ["agent", "consumer"] },
+ { from: "ag4", to: "cs3", predicate: "monitors_sentiment_of", strength: 0.82, domain: ["agent", "consumer"] },
+ { from: "ag5", to: "cs2", predicate: "optimizes_journey_for", strength: 0.86, domain: ["agent", "consumer"] },
+ { from: "ag5", to: "cs5", predicate: "optimizes_journey_for", strength: 0.75, domain: ["agent", "consumer"] },
+ // Agent ↔ Brand
+ { from: "ag3", to: "br1", predicate: "orchestrates_campaign_for", strength: 0.88, domain: ["agent", "brand"] },
+ { from: "ag3", to: "br2", predicate: "orchestrates_campaign_for", strength: 0.82, domain: ["agent", "brand"] },
+ { from: "ag3", to: "br5", predicate: "orchestrates_campaign_for", strength: 0.79, domain: ["agent", "brand"] },
+ { from: "ag4", to: "br1", predicate: "analyzes_perception_of", strength: 0.91, domain: ["agent", "brand"] },
+ { from: "ag4", to: "br3", predicate: "analyzes_perception_of", strength: 0.85, domain: ["agent", "brand"] },
+ { from: "ag1", to: "br3", predicate: "curates_products_for", strength: 0.83, domain: ["agent", "brand"] },
+ { from: "ag1", to: "br4", predicate: "curates_products_for", strength: 0.77, domain: ["agent", "brand"] },
+ // Agent ↔ Retail
+ { from: "ag2", to: "rt1", predicate: "tailors_experience_at", strength: 0.89, domain: ["agent", "retail"] },
+ { from: "ag2", to: "rt2", predicate: "tailors_experience_at", strength: 0.85, domain: ["agent", "retail"] },
+ { from: "ag3", to: "rt3", predicate: "deploys_campaign_at", strength: 0.81, domain: ["agent", "retail"] },
+ { from: "ag3", to: "rt4", predicate: "deploys_campaign_at", strength: 0.86, domain: ["agent", "retail"] },
+ { from: "ag5", to: "rt1", predicate: "optimizes_flow_at", strength: 0.84, domain: ["agent", "retail"] },
+ { from: "ag5", to: "rt5", predicate: "optimizes_flow_at", strength: 0.80, domain: ["agent", "retail"] },
+];
+
+// ── Pre-built Agent Queries & Responses ────────────────────────────
+const DEMO_QUERIES = [
+ {
+ q: "Which brands should activate at the Pop-Up Experience to reach Eco-Conscious Gen Z?",
+ thinking: [
+ "Traversing ontology: Consumer[cs3] → has_affinity_for → Brand[br2, br4, br5]",
+ "Traversing ontology: Consumer[cs3] → experiences → Retail[rt3]",
+ "Cross-referencing: Brand activations at Retail[rt3]",
+ "Ranking by: affinity strength × activation fit × conversion potential",
+ ],
+ answer: "Nordic Trail and Artisan Coffee Co. are the strongest activation candidates for the Pop-Up Experience targeting Eco-Conscious Gen Z. Nordic Trail's sustainability positioning aligns with this segment's values (affinity: 0.71) and already activates via experiential retail (strength: 0.82). Artisan Coffee Co. has existing frequency with this segment (0.58) and pop-up activation experience (0.79). Casa Verde is a secondary candidate — lower affinity (0.65) but high experiential fit.",
+ entities: ["cs3", "br2", "br5", "br4", "rt3"],
+ triples: 8,
+ },
+ {
+ q: "How should Lumière Beauty optimize its engagement with Luxury Seekers across channels?",
+ thinking: [
+ "Resolving entities: Brand[br1] = Lumière Beauty, Consumer[cs4] = Luxury Seekers",
+ "Traversing: Brand[br1] → merchandises_in → Retail[rt1], activates_via → Retail[rt3]",
+ "Traversing: Consumer[cs4] → shops_via → Retail[rt1], experiences → Retail[rt3]",
+ "Agent context: Personalization Agent[ag2] → personalizes_for → Consumer[cs4] (0.93)",
+ "Aggregating channel overlap and engagement signals",
+ ],
+ answer: "Lumière Beauty has exceptional alignment with Luxury Seekers — the strongest brand-consumer affinity in the graph (0.92). Both converge on Flagship Store NYC and Pop-Up Experiences. The Personalization Agent already achieves 93% accuracy for this segment. Recommendation: Deepen the flagship in-store experience with agent-powered personalized consultations, and create exclusive pop-up previews. The loyalty-to-advocacy pipeline is strong (journey stage: Advocate) — leverage this for referral programs through the Loyalty Hub.",
+ entities: ["br1", "cs4", "rt1", "rt3", "ag2", "rt5"],
+ triples: 12,
+ },
+ {
+ q: "What's the optimal agent configuration for the Mobile Commerce channel?",
+ thinking: [
+ "Resolving: Retail[rt2] = Mobile Commerce App",
+ "Traversing: Agent → tailors_experience_at → Retail[rt2]",
+ "Traversing: Consumer → shops_via → Retail[rt2]: [cs1, cs5]",
+ "Traversing: Brand → sells_through → Retail[rt2]: [br3]",
+ "Evaluating agent capabilities against mobile channel requirements",
+ ],
+ answer: "Mobile Commerce currently has the Personalization Agent deployed (experience tailoring, 85% strength). Primary consumer segments are Urban Millennials (0.82) and Weekend Warriors (0.72). Add the Recommendation Agent — it already serves Urban Millennials (0.87) and can curate Velo Sport products (the channel's primary brand). The Journey Optimizer should be connected to reduce the gap between the channel's high traffic (1.2M/mo) and moderate conversion (4.7%). Projected improvement: 2.1% conversion lift through graph-informed product sequencing.",
+ entities: ["rt2", "ag2", "ag1", "ag5", "cs1", "cs5", "br3"],
+ triples: 11,
+ },
+];
+
+// ── Helper: find all entities ────────────────────────────────────────
+function getAllEntities() {
+ const all = [];
+ Object.entries(ONTOLOGY).forEach(([domain, data]) => {
+ data.subclasses.forEach((sc) => {
+ all.push({ ...sc, domain, color: data.color, glow: data.glow, icon: data.icon });
+ });
+ });
+ return all;
+}
+
+// ── Graph Visualization (Canvas-based force layout) ─────────────────
+function GraphCanvas({ highlightedEntities, onNodeClick, activeFilter }) {
+ const canvasRef = useRef(null);
+ const nodesRef = useRef([]);
+ const animRef = useRef(null);
+ const hoveredRef = useRef(null);
+ const [hovered, setHovered] = useState(null);
+
+ const entities = useMemo(() => getAllEntities(), []);
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+ const rect = canvas.parentElement.getBoundingClientRect();
+ canvas.width = rect.width * 2;
+ canvas.height = rect.height * 2;
+ canvas.style.width = rect.width + "px";
+ canvas.style.height = rect.height + "px";
+
+ const cx = canvas.width / 2;
+ const cy = canvas.height / 2;
+
+ // Position nodes in domain clusters
+ const domainPositions = {
+ consumer: { x: cx - cx * 0.35, y: cy - cy * 0.32 },
+ brand: { x: cx + cx * 0.35, y: cy - cy * 0.32 },
+ retail: { x: cx + cx * 0.35, y: cy + cy * 0.32 },
+ agent: { x: cx - cx * 0.35, y: cy + cy * 0.32 },
+ };
+
+ nodesRef.current = entities.map((e, i) => {
+ const dp = domainPositions[e.domain];
+ const subIdx = ONTOLOGY[e.domain].subclasses.findIndex((s) => s.id === e.id);
+ const total = ONTOLOGY[e.domain].subclasses.length;
+ const angle = ((Math.PI * 2) / total) * subIdx - Math.PI / 2;
+ const radius = Math.min(canvas.width, canvas.height) * 0.1;
+ return {
+ ...e,
+ x: dp.x + Math.cos(angle) * radius,
+ y: dp.y + Math.sin(angle) * radius,
+ vx: 0,
+ vy: 0,
+ targetX: dp.x + Math.cos(angle) * radius,
+ targetY: dp.y + Math.sin(angle) * radius,
+ r: 18,
+ };
+ });
+
+ const ctx = canvas.getContext("2d");
+ let time = 0;
+
+ function draw() {
+ time += 0.005;
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+ // Subtle grid
+ ctx.strokeStyle = "rgba(255,255,255,0.015)";
+ ctx.lineWidth = 1;
+ for (let x = 0; x < canvas.width; x += 60) {
+ ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke();
+ }
+ for (let y = 0; y < canvas.height; y += 60) {
+ ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke();
+ }
+
+ // Domain labels
+ Object.entries(domainPositions).forEach(([domain, pos]) => {
+ const data = ONTOLOGY[domain];
+ ctx.font = "bold 22px 'IBM Plex Mono', monospace";
+ ctx.fillStyle = data.color + "44";
+ ctx.textAlign = "center";
+ ctx.fillText(data.label.toUpperCase(), pos.x, pos.y - Math.min(canvas.width, canvas.height) * 0.14);
+ });
+
+ // Draw edges
+ const nodes = nodesRef.current;
+ const filteredRels = activeFilter
+ ? RELATIONSHIPS.filter((r) => r.domain.includes(activeFilter))
+ : RELATIONSHIPS;
+
+ filteredRels.forEach((rel) => {
+ const fromNode = nodes.find((n) => n.id === rel.from);
+ const toNode = nodes.find((n) => n.id === rel.to);
+ if (!fromNode || !toNode) return;
+
+ const isHighlighted =
+ highlightedEntities &&
+ highlightedEntities.includes(rel.from) &&
+ highlightedEntities.includes(rel.to);
+
+ const baseAlpha = isHighlighted ? 0.7 : 0.12;
+ const pulse = isHighlighted ? Math.sin(time * 4) * 0.15 + 0.15 : 0;
+
+ ctx.beginPath();
+ ctx.moveTo(fromNode.x, fromNode.y);
+ // Curved edges
+ const mx = (fromNode.x + toNode.x) / 2 + (fromNode.y - toNode.y) * 0.1;
+ const my = (fromNode.y + toNode.y) / 2 + (toNode.x - fromNode.x) * 0.1;
+ ctx.quadraticCurveTo(mx, my, toNode.x, toNode.y);
+
+ const gradient = ctx.createLinearGradient(fromNode.x, fromNode.y, toNode.x, toNode.y);
+ gradient.addColorStop(0, fromNode.color + Math.round((baseAlpha + pulse) * 255).toString(16).padStart(2, "0"));
+ gradient.addColorStop(1, toNode.color + Math.round((baseAlpha + pulse) * 255).toString(16).padStart(2, "0"));
+ ctx.strokeStyle = gradient;
+ ctx.lineWidth = isHighlighted ? 3 : 1.5;
+ ctx.stroke();
+
+ // Animated particles on highlighted edges
+ if (isHighlighted) {
+ const t = (time * 2 + rel.strength) % 1;
+ const px = (1 - t) * (1 - t) * fromNode.x + 2 * (1 - t) * t * mx + t * t * toNode.x;
+ const py = (1 - t) * (1 - t) * fromNode.y + 2 * (1 - t) * t * my + t * t * toNode.y;
+ ctx.beginPath();
+ ctx.arc(px, py, 3, 0, Math.PI * 2);
+ ctx.fillStyle = "#fff";
+ ctx.fill();
+ }
+ });
+
+ // Draw nodes
+ nodes.forEach((node) => {
+ const isHighlighted = highlightedEntities && highlightedEntities.includes(node.id);
+ const isHovered = hoveredRef.current === node.id;
+ const isDimmed = highlightedEntities && highlightedEntities.length > 0 && !isHighlighted;
+ const isFiltered = activeFilter && node.domain !== activeFilter && !RELATIONSHIPS.some(
+ r => r.domain.includes(activeFilter) && (r.from === node.id || r.to === node.id)
+ );
+
+ const alpha = isFiltered ? 0.15 : isDimmed ? 0.3 : 1;
+ const r = isHighlighted || isHovered ? node.r * 1.4 : node.r;
+ const pulseR = isHighlighted ? Math.sin(time * 3) * 3 : 0;
+
+ // Glow
+ if ((isHighlighted || isHovered) && !isFiltered) {
+ ctx.beginPath();
+ ctx.arc(node.x, node.y, r + 12 + pulseR, 0, Math.PI * 2);
+ const grd = ctx.createRadialGradient(node.x, node.y, r, node.x, node.y, r + 12 + pulseR);
+ grd.addColorStop(0, node.glow);
+ grd.addColorStop(1, "rgba(0,0,0,0)");
+ ctx.fillStyle = grd;
+ ctx.fill();
+ }
+
+ // Node circle
+ ctx.beginPath();
+ ctx.arc(node.x, node.y, r, 0, Math.PI * 2);
+ ctx.fillStyle = node.color + Math.round(alpha * 255 * 0.2).toString(16).padStart(2, "0");
+ ctx.fill();
+ ctx.strokeStyle = node.color + Math.round(alpha * 255).toString(16).padStart(2, "0");
+ ctx.lineWidth = isHighlighted ? 2.5 : 1.5;
+ ctx.stroke();
+
+ // Label
+ ctx.font = `${isHighlighted ? "bold " : ""}${isHovered ? 17 : 14}px 'IBM Plex Sans', sans-serif`;
+ ctx.fillStyle = `rgba(255,255,255,${alpha * (isHighlighted ? 1 : 0.75)})`;
+ ctx.textAlign = "center";
+ ctx.fillText(node.label, node.x, node.y + r + 18);
+
+ // Spring physics
+ node.x += (node.targetX - node.x) * 0.02;
+ node.y += (node.targetY - node.y) * 0.02;
+ node.x += Math.sin(time + node.targetX * 0.01) * 0.3;
+ node.y += Math.cos(time + node.targetY * 0.01) * 0.3;
+ });
+
+ animRef.current = requestAnimationFrame(draw);
+ }
+
+ draw();
+ return () => cancelAnimationFrame(animRef.current);
+ }, [entities, highlightedEntities, activeFilter]);
+
+ const handleMouseMove = useCallback((e) => {
+ const canvas = canvasRef.current;
+ const rect = canvas.getBoundingClientRect();
+ const x = (e.clientX - rect.left) * 2;
+ const y = (e.clientY - rect.top) * 2;
+ const nodes = nodesRef.current;
+ let found = null;
+ for (const node of nodes) {
+ const dx = node.x - x;
+ const dy = node.y - y;
+ if (Math.sqrt(dx * dx + dy * dy) < node.r * 1.5) {
+ found = node.id;
+ break;
+ }
+ }
+ hoveredRef.current = found;
+ setHovered(found);
+ canvas.style.cursor = found ? "pointer" : "default";
+ }, []);
+
+ const handleClick = useCallback((e) => {
+ if (hoveredRef.current && onNodeClick) {
+ const node = nodesRef.current.find((n) => n.id === hoveredRef.current);
+ if (node) onNodeClick(node);
+ }
+ }, [onNodeClick]);
+
+ return (
+
+
+ {hovered && (() => {
+ const node = nodesRef.current.find((n) => n.id === hovered);
+ if (!node) return null;
+ const canvas = canvasRef.current;
+ const rect = canvas.getBoundingClientRect();
+ const sx = node.x / 2;
+ const sy = node.y / 2;
+ return (
+
+
+ {node.icon} {node.label}
+
+
+ {Object.entries(node.props || {}).map(([k, v]) => (
+
{k}: {String(v)}
+ ))}
+
+
+ );
+ })()}
+
+ );
+}
+
+// ── Typewriter Effect ────────────────────────────────────────────────
+function Typewriter({ text, speed = 12, onDone }) {
+ const [displayed, setDisplayed] = useState("");
+ const idx = useRef(0);
+
+ useEffect(() => {
+ idx.current = 0;
+ setDisplayed("");
+ const interval = setInterval(() => {
+ idx.current++;
+ if (idx.current >= text.length) {
+ setDisplayed(text);
+ clearInterval(interval);
+ onDone && onDone();
+ } else {
+ setDisplayed(text.slice(0, idx.current));
+ }
+ }, speed);
+ return () => clearInterval(interval);
+ }, [text, speed]);
+
+ return {displayed}▌;
+}
+
+// ── Main App ─────────────────────────────────────────────────────────
+export default function TrustGraphRetailDemo() {
+ const [activeTab, setActiveTab] = useState("graph");
+ const [activeFilter, setActiveFilter] = useState(null);
+ const [selectedQuery, setSelectedQuery] = useState(null);
+ const [queryPhase, setQueryPhase] = useState("idle"); // idle, thinking, answering, done
+ const [thinkingStep, setThinkingStep] = useState(0);
+ const [selectedNode, setSelectedNode] = useState(null);
+ const [showOntology, setShowOntology] = useState(false);
+
+ const runQuery = (idx) => {
+ setSelectedQuery(idx);
+ setQueryPhase("thinking");
+ setThinkingStep(0);
+ const q = DEMO_QUERIES[idx];
+ let step = 0;
+ const interval = setInterval(() => {
+ step++;
+ if (step >= q.thinking.length) {
+ clearInterval(interval);
+ setTimeout(() => setQueryPhase("answering"), 400);
+ }
+ setThinkingStep(step);
+ }, 800);
+ };
+
+ const highlightedEntities = selectedQuery !== null && queryPhase !== "idle"
+ ? DEMO_QUERIES[selectedQuery].entities
+ : selectedNode
+ ? [selectedNode.id, ...RELATIONSHIPS.filter(r => r.from === selectedNode.id || r.to === selectedNode.id).map(r => r.from === selectedNode.id ? r.to : r.from)]
+ : [];
+
+ return (
+
+ {/* ── Header ────────────────────────────────────────── */}
+
+
+
TG
+
+
+ TrustGraph
+
+
+ RETAIL INTELLIGENCE PLATFORM
+
+
+
+
+ {["graph", "query", "ontology"].map((tab) => (
+
+ ))}
+
+
+
+ {/* ── Domain Filter Bar ──────────────────────────────── */}
+ {activeTab === "graph" && (
+
+
FILTER:
+
+ {Object.entries(ONTOLOGY).map(([key, data]) => (
+
+ ))}
+
+ {getAllEntities().length} entities · {RELATIONSHIPS.length} relationships
+
+
+ )}
+
+ {/* ── Main Content ──────────────────────────────────── */}
+
+
+ {/* ── Graph View ──────────────────────────────────── */}
+ {activeTab === "graph" && (
+ <>
+
+ setSelectedNode(selectedNode?.id === node.id ? null : node)}
+ activeFilter={activeFilter}
+ />
+
+ {/* Side panel for selected node */}
+ {selectedNode && (
+
+
+
+ {ONTOLOGY[selectedNode.domain].label.toUpperCase()} ENTITY
+
+
+
+
+ {selectedNode.icon} {selectedNode.label}
+
+
+
PROPERTIES
+ {Object.entries(selectedNode.props || {}).map(([k, v]) => (
+
+ {k}
+ {String(v)}
+
+ ))}
+
+
+
RELATIONSHIPS
+ {RELATIONSHIPS.filter(r => r.from === selectedNode.id || r.to === selectedNode.id).map((r, i) => {
+ const otherId = r.from === selectedNode.id ? r.to : r.from;
+ const other = getAllEntities().find(e => e.id === otherId);
+ const direction = r.from === selectedNode.id ? "→" : "←";
+ return (
+
{ const n = getAllEntities().find(e => e.id === otherId); if (n) setSelectedNode(n); }}>
+
+ {direction} {other?.label}
+
+
+ {r.predicate.replace(/_/g, " ")} · strength: {r.strength}
+
+
+ );
+ })}
+
+
+ )}
+ >
+ )}
+
+ {/* ── Agent Query View ────────────────────────────── */}
+ {activeTab === "query" && (
+
+
+ {/* Query selector */}
+
+
+ SELECT A QUERY TO SEE GRAPH-POWERED AGENT INTELLIGENCE
+
+ {DEMO_QUERIES.map((dq, idx) => (
+
+ ))}
+
+
+ {/* Response area */}
+ {selectedQuery !== null && (
+
+ {/* Graph traversal steps */}
+ {(queryPhase === "thinking" || queryPhase === "answering" || queryPhase === "done") && (
+
+
+ ◈ GRAPH TRAVERSAL
+
+ {DEMO_QUERIES[selectedQuery].thinking.map((step, i) => (
+
+ {i < thinkingStep && ✓}
+ {step}
+
+ ))}
+ {queryPhase === "thinking" && (
+
+ Traversing graph...
+
+ )}
+
+ )}
+
+ {/* Answer */}
+ {(queryPhase === "answering" || queryPhase === "done") && (
+
+
+
+ AGENT RESPONSE
+
+
+ {DEMO_QUERIES[selectedQuery].triples} triples traversed · {DEMO_QUERIES[selectedQuery].entities.length} entities resolved
+
+
+
+ setQueryPhase("done")}
+ />
+
+
+ )}
+
+ )}
+
+
+ {/* Graph visualization alongside query */}
+
+ {}} activeFilter={null} />
+
+
+ )}
+
+ {/* ── Ontology View ──────────────────────────────── */}
+ {activeTab === "ontology" && (
+
+
+
+ ONTOLOGY SCHEMA · RETAIL INTELLIGENCE DOMAIN
+
+
+ {/* Ontology class cards */}
+
+ {Object.entries(ONTOLOGY).map(([key, data]) => (
+
+
+
{data.icon}
+
+
{data.label}
+
owl:Class
+
+
+
{data.description}
+
PROPERTIES
+
+ {data.properties.map((p) => (
+ {p}
+ ))}
+
+
+ INSTANCES ({data.subclasses.length})
+
+ {data.subclasses.map((sc) => (
+
+ {sc.label}
+ {sc.id}
+
+ ))}
+
+ ))}
+
+
+ {/* Relationship predicates */}
+
+
+ RELATIONSHIP PREDICATES
+
+
+ {[...new Set(RELATIONSHIPS.map(r => r.predicate))].map((pred) => {
+ const sample = RELATIONSHIPS.find(r => r.predicate === pred);
+ const fromDomain = sample.domain[0];
+ const toDomain = sample.domain[1];
+ return (
+
+
+ {pred.replace(/_/g, " ")}
+
+
+ {ONTOLOGY[fromDomain].label}
+ {" → "}
+ {ONTOLOGY[toDomain].label}
+
+
+ );
+ })}
+
+
+
+ {/* Triple count summary */}
+
+ {[
+ { label: "Classes", value: 4 },
+ { label: "Instances", value: getAllEntities().length },
+ { label: "Predicates", value: [...new Set(RELATIONSHIPS.map(r => r.predicate))].length },
+ { label: "Triples", value: RELATIONSHIPS.length },
+ ].map((s) => (
+
+
{s.value}
+
{s.label.toUpperCase()}
+
+ ))}
+
+
+
+ )}
+
+
+ {/* ── Bottom Status Bar ──────────────────────────────── */}
+
+
+ ◈ Ontology: Consumer × Agent × Retail × Brand
+ ⬡ GraphRAG: Active
+ ⚡ Agent Orchestration: Online
+
+
+ ● Context Graph Connected
+ |
+ trustgraph.ai
+
+
+
+ );
+}
diff --git a/ai-context/context-graph-demo/tsconfig.json b/ai-context/context-graph-demo/tsconfig.json
new file mode 100644
index 00000000..a4c834a6
--- /dev/null
+++ b/ai-context/context-graph-demo/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
+}
diff --git a/ai-context/context-graph-demo/vite.config.js b/ai-context/context-graph-demo/vite.config.js
new file mode 100644
index 00000000..fba45ddf
--- /dev/null
+++ b/ai-context/context-graph-demo/vite.config.js
@@ -0,0 +1,39 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import path from 'path'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react()],
+ resolve: {
+ dedupe: ['react', 'react-dom', '@tanstack/react-query'],
+ alias: {
+ react: path.resolve('./node_modules/react'),
+ 'react-dom': path.resolve('./node_modules/react-dom'),
+ },
+ },
+ server: {
+ proxy: {
+ "/api/socket": {
+ // target: "wss://broker.app.trustgraph.ai/",
+ target: "ws://localhost:8088/",
+ changeOrigin: true,
+ ws: true,
+ secure: false,
+ rewrite: (path) => path.replace("/api/socket", "/api/v1/socket"),
+ },
+ "/api/export-core": {
+ target: "http://localhost:8088/",
+ changeOrigin: true,
+ secure: false,
+ rewrite: (x) => x.replace("/api/export-core", "/api/v1/export-core"),
+ },
+ "/api/import-core": {
+ target: "http://localhost:8088/",
+ changeOrigin: true,
+ secure: false,
+ rewrite: (x) => x.replace("/api/import-core", "/api/v1/import-core"),
+ },
+ },
+ },
+})
diff --git a/ai-context/trustgraph-client/.github/workflows/ci.yml b/ai-context/trustgraph-client/.github/workflows/ci.yml
new file mode 100644
index 00000000..67cee0e9
--- /dev/null
+++ b/ai-context/trustgraph-client/.github/workflows/ci.yml
@@ -0,0 +1,34 @@
+name: CI
+
+on:
+ push:
+ branches: [master]
+ pull_request:
+ branches: [master]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '22'
+
+ - name: Install dependencies
+ run: npm install
+
+ - name: Type check
+ run: npm run typecheck
+
+ - name: Lint
+ run: npm run lint
+
+ - name: Run tests
+ run: npm test
+
+ - name: Build
+ run: npm run build
diff --git a/ai-context/trustgraph-client/.github/workflows/publish.yml b/ai-context/trustgraph-client/.github/workflows/publish.yml
new file mode 100644
index 00000000..fd681ba0
--- /dev/null
+++ b/ai-context/trustgraph-client/.github/workflows/publish.yml
@@ -0,0 +1,51 @@
+name: Publish to npm
+
+on:
+ push:
+ tags:
+ - 'v*'
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ id-token: write
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '22'
+
+ - name: Upgrade npm for OIDC support
+ run: npm install -g npm@latest
+
+ - name: Install dependencies
+ run: npm install
+
+ - name: Type check
+ run: npm run typecheck
+
+ - name: Lint
+ run: npm run lint
+
+ - name: Run tests
+ run: npm test
+
+ - name: Build
+ run: npm run build
+
+ - name: Verify version matches tag
+ run: |
+ TAG_VERSION=${GITHUB_REF#refs/tags/v}
+ PKG_VERSION=$(node -p "require('./package.json').version")
+ if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
+ echo "Tag version ($TAG_VERSION) doesn't match package.json ($PKG_VERSION)"
+ exit 1
+ fi
+
+ - name: Publish to npm
+ run: npm publish --access public --provenance
diff --git a/ai-context/trustgraph-client/.gitignore b/ai-context/trustgraph-client/.gitignore
new file mode 100644
index 00000000..58aa58fb
--- /dev/null
+++ b/ai-context/trustgraph-client/.gitignore
@@ -0,0 +1,7 @@
+node_modules/
+dist/
+*.log
+.DS_Store
+*.tsbuildinfo
+package-lock.json
+*~
diff --git a/ai-context/trustgraph-client/.prettierrc b/ai-context/trustgraph-client/.prettierrc
new file mode 100644
index 00000000..8b6b8803
--- /dev/null
+++ b/ai-context/trustgraph-client/.prettierrc
@@ -0,0 +1,3 @@
+{
+ "printWidth": 79
+}
diff --git a/ai-context/trustgraph-client/LICENSE b/ai-context/trustgraph-client/LICENSE
new file mode 100644
index 00000000..d9a10c0d
--- /dev/null
+++ b/ai-context/trustgraph-client/LICENSE
@@ -0,0 +1,176 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
diff --git a/ai-context/trustgraph-client/README.md b/ai-context/trustgraph-client/README.md
new file mode 100644
index 00000000..cb25bc94
--- /dev/null
+++ b/ai-context/trustgraph-client/README.md
@@ -0,0 +1,319 @@
+# @trustgraph/client
+
+TypeScript/JavaScript client library for TrustGraph WebSocket API. This package provides a framework-agnostic client for communicating with TrustGraph services.
+
+## Features
+
+- 🌐 **WebSocket-based** - Real-time communication with TrustGraph services
+- 📦 **Zero Dependencies** - No external runtime dependencies
+- 🔐 **Authentication Support** - Optional API key authentication
+- 🔄 **Auto-reconnection** - Handles connection failures gracefully
+- 📝 **Full TypeScript Support** - Complete type definitions
+- 🎯 **Framework Agnostic** - Works with any JavaScript framework or vanilla JS
+
+## Installation
+
+```bash
+npm install @trustgraph/client
+```
+
+## Quick Start
+
+```typescript
+import { createTrustGraphSocket } from "@trustgraph/client";
+
+// Create a socket connection
+const socket = createTrustGraphSocket("your-username");
+
+// Query triples from the knowledge graph
+const triples = await socket.triplesQuery(
+ { v: "http://example.org/subject", e: true },
+ { v: "http://example.org/predicate", e: true },
+ undefined,
+ 10, // limit
+);
+
+console.log(triples);
+```
+
+## With Authentication
+
+```typescript
+const socket = createTrustGraphSocket("your-username", "your-api-key");
+```
+
+## Core APIs
+
+### Knowledge Graph Operations
+
+**Query Triples**
+
+```typescript
+const triples = await socket.triplesQuery(
+ subject?: Value, // Optional subject filter
+ predicate?: Value, // Optional predicate filter
+ object?: Value, // Optional object filter
+ limit: number, // Maximum results
+ collection?: string // Optional collection name
+);
+```
+
+**Graph Embeddings Query**
+
+```typescript
+const entities = await socket.graphEmbeddingsQuery(
+ vectors: number[][], // Embedding vectors
+ limit: number, // Maximum results
+ collection?: string // Optional collection name
+);
+```
+
+### Text & LLM Operations
+
+**Text Completion**
+
+```typescript
+const response = await socket.textCompletion(
+ system: string, // System prompt
+ prompt: string, // User prompt
+ temperature?: number
+);
+```
+
+**Graph RAG**
+
+```typescript
+const answer = await socket.graphRag(
+ query: string,
+ options?: {
+ 'entity-limit'?: number,
+ 'triple-limit'?: number,
+ 'max-subgraph-size'?: number,
+ 'max-path-length'?: number
+ },
+ collection?: string
+);
+```
+
+**Agent**
+
+```typescript
+socket.agent(
+ question: string,
+ think: (thought: string) => void, // Called when agent is thinking
+ observe: (observation: string) => void, // Called on observations
+ answer: (answer: string) => void, // Called with final answer
+ error: (error: string) => void, // Called on errors
+ collection?: string
+);
+```
+
+**Embeddings**
+
+```typescript
+const vectors = await socket.embeddings(text: string);
+```
+
+### Document Operations
+
+**Load Document**
+
+```typescript
+await socket.loadDocument(
+ id: string, // Document ID
+ data: string, // Base64-encoded document
+ metadata: Triple[], // Document metadata as triples
+ collection?: string
+);
+```
+
+**Load Text**
+
+```typescript
+await socket.loadText(
+ id: string, // Document ID
+ text: string, // Plain text content
+ charset: string, // Character encoding (e.g., 'utf-8')
+ metadata: Triple[], // Document metadata as triples
+ collection?: string
+);
+```
+
+### Library Operations
+
+**List Documents**
+
+```typescript
+const docs = await socket.library.listDocuments(
+ user?: string,
+ collection?: string
+);
+```
+
+**Get Document**
+
+```typescript
+const doc = await socket.library.getDocument(
+ id: string,
+ user?: string,
+ collection?: string
+);
+```
+
+**Delete Document**
+
+```typescript
+await socket.library.deleteDocument(
+ id: string,
+ user?: string,
+ collection?: string
+);
+```
+
+### Flow Operations
+
+Flows represent processing pipelines for documents and queries.
+
+**Create Flow API**
+
+```typescript
+const flowApi = socket.flow("flow-id");
+// flowApi has same methods as socket but scoped to this flow
+```
+
+**Start Flow**
+
+```typescript
+await socket.flows.startFlow(
+ flowId: string,
+ className: string,
+ description: string
+);
+```
+
+**Stop Flow**
+
+```typescript
+await socket.flows.stopFlow(flowId: string);
+```
+
+**List Flows**
+
+```typescript
+const flowIds = await socket.flows.getFlows();
+```
+
+**Get Flow Definition**
+
+```typescript
+const flowDef = await socket.flows.getFlow(flowId: string);
+```
+
+**List Flow Classes**
+
+```typescript
+const classes = await socket.flows.getFlowClasses();
+```
+
+**Get Flow Class**
+
+```typescript
+const classDef = await socket.flows.getFlowClass(className: string);
+```
+
+## Connection State Monitoring
+
+```typescript
+// Subscribe to connection state changes
+const unsubscribe = socket.onConnectionStateChange((state) => {
+ console.log("Status:", state.status); // 'connecting' | 'connected' | 'authenticated' | 'disconnected' | 'error'
+ console.log("Authenticated:", state.authenticated);
+ console.log("Error:", state.error);
+});
+
+// Unsubscribe when done
+unsubscribe();
+```
+
+## Data Types
+
+### Value
+
+Represents a subject, predicate, or object in a triple:
+
+```typescript
+interface Value {
+ v: string; // Value (URI or literal)
+ e: boolean; // Is entity (true) or literal (false)
+ label?: string; // Optional human-readable label
+}
+```
+
+### Triple
+
+Represents a subject-predicate-object relationship:
+
+```typescript
+interface Triple {
+ s: Value; // Subject
+ p: Value; // Predicate
+ o: Value; // Object
+}
+```
+
+## Advanced Usage
+
+### Custom Timeout and Retries
+
+Most methods accept optional timeout and retry parameters:
+
+```typescript
+await socket.triplesQuery(
+ subject,
+ predicate,
+ object,
+ limit,
+ collection,
+ 30000, // timeout in ms
+ 5, // retry attempts
+);
+```
+
+### Closing the Connection
+
+```typescript
+socket.close();
+```
+
+## Error Handling
+
+All async methods return Promises that reject on error:
+
+```typescript
+try {
+ const result = await socket.triplesQuery(...);
+} catch (error) {
+ console.error('Query failed:', error);
+}
+```
+
+## React Integration
+
+For React applications, use the companion package:
+
+```bash
+npm install @trustgraph/react-provider
+```
+
+See [@trustgraph/react-provider](https://github.com/trustgraph-ai/trustgraph-client) for React-specific hooks and providers.
+
+## API Reference
+
+Full API documentation is available in the TypeScript definitions. Your IDE will provide autocomplete and inline documentation for all methods.
+
+## License
+
+Apache 2.0
+
+(c) KnowNext Inc., KnowNext Limited 2025
+
diff --git a/ai-context/trustgraph-client/docs/tech-specs/client-module.md b/ai-context/trustgraph-client/docs/tech-specs/client-module.md
new file mode 100644
index 00000000..93e24d9e
--- /dev/null
+++ b/ai-context/trustgraph-client/docs/tech-specs/client-module.md
@@ -0,0 +1,44 @@
+# TrustGraph Client Module - Technical Specification
+
+## Overview
+
+This module extracts reusable code from the existing TrustGraph Workbench
+application and packages it as a standalone client library. The goal is to
+enable developers to build TrustGraph user experiences without having to
+reimplement API communication and state management from scratch.
+
+## Goals
+
+- Extract and package reusable WebSocket API code from TrustGraph Workbench
+- Provide a clean, well-documented interface for TrustGraph WebSocket
+ communication
+- Enable developers to quickly build TrustGraph UX applications
+- Eliminate code duplication across TrustGraph UI projects
+- Maintain compatibility with existing TrustGraph backend services
+
+## Non-Goals
+
+- REST API implementations (WebSocket only)
+- UI components or presentation layer code
+- Backend service implementations
+- Authentication/authorization logic beyond what's needed for WebSocket
+ connections
+- Application-specific business logic
+
+## Architecture
+
+## API Design
+
+## Implementation Plan
+
+## Testing Strategy
+
+## Dependencies
+
+## Security Considerations
+
+## Performance Considerations
+
+## Open Questions
+
+## References
diff --git a/ai-context/trustgraph-client/docs/tech-specs/streaming-support.md b/ai-context/trustgraph-client/docs/tech-specs/streaming-support.md
new file mode 100644
index 00000000..3f4207f1
--- /dev/null
+++ b/ai-context/trustgraph-client/docs/tech-specs/streaming-support.md
@@ -0,0 +1,808 @@
+# Streaming Support for TrustGraph Client
+
+**Status**: Draft for Review
+**Author**: Claude
+**Date**: 2025-11-27
+**Version**: 1.0
+
+## Executive Summary
+
+Extend the TrustGraph TypeScript client to support streaming responses for Graph RAG, Document RAG, Text Completion, and Prompt services. The client already has streaming infrastructure (`ServiceCallMulti`) used by Agent, but the other services only support single-response mode. This spec proposes minimal changes to enable streaming across all services while maintaining backward compatibility.
+
+## Background
+
+### Current State
+
+The client has **two request patterns**:
+
+1. **Single-response** (`makeRequest` → `ServiceCall`)
+ - Used by: text-completion, graph-rag, document-rag, prompt, and most other services
+ - Returns Promise that resolves with single response
+ - Example: `graphRag(text: string): Promise`
+
+2. **Multi-response** (`makeRequestMulti` → `ServiceCallMulti`)
+ - Used by: agent (thoughts/observations/answer), knowledge.getKgCore (large graph streaming)
+ - Accepts `receiver: (resp: unknown) => boolean` callback
+ - Receiver returns `true` to signal end-of-stream
+ - Example: `agent(question, think, observe, answer, error): void`
+
+### Backend Streaming Protocol
+
+Per `STREAMING-IMPLEMENTATION-NOTES.txt`, the backend supports streaming when `streaming: true` is added to requests:
+
+**Graph RAG / Document RAG**:
+- Chunks arrive with `chunk` field
+- Final chunk has `end_of_stream: true`
+
+**Text Completion**:
+- Chunks arrive with `response` field
+- Final chunk has `end_of_stream: true`
+
+**Prompt**:
+- Chunks arrive with `text` field
+- Final chunk has `end_of_stream: true`
+
+**Agent** (already implemented):
+- Multiple messages with `chunk_type` (thought/observation/final-answer)
+- Final chunk has `end_of_dialog: true`
+
+## Problem Statement
+
+**Primary Issue**: Users who want streaming responses for Graph RAG, Document RAG, Text Completion, or Prompt services must:
+1. Drop down to `makeRequestMulti` and handle raw responses
+2. Manually parse `chunk`/`response`/`text` fields
+3. Check `end_of_stream` flag
+4. Handle errors mid-stream
+
+**Secondary Issue**: The Agent API doesn't correctly implement the backend streaming protocol. The backend sends:
+```
+{chunk_type: "thought", content: "I need to", end_of_message: false, end_of_dialog: false}
+{chunk_type: "thought", content: " search", end_of_message: false, end_of_dialog: false}
+```
+
+But the client expects:
+```
+{thought?: string, observation?: string, answer?: string}
+```
+
+The Agent implementation needs to be updated to handle incremental chunks with completion flags.
+
+## Goals
+
+1. **Fix Agent API** to correctly implement backend streaming protocol with chunk-level callbacks
+2. **Add streaming variants** for text-completion, graph-rag, document-rag, and prompt services
+3. **Maintain backward compatibility** - existing non-streaming APIs unchanged (except Agent which needs fixing)
+4. **Policy-free implementation** - no state management (accumulation, buffering, etc.) in client layer
+5. **Minimal callback interface** - single receiver callback with chunk and completion flag
+6. **Minimal type changes** - reuse existing request/response types where possible
+
+## Non-Goals
+
+- Changing the existing non-streaming APIs
+- Supporting streaming for services that don't stream (embeddings, triples, etc.)
+- Implementing state management (accumulation, buffering) - that belongs in higher layers
+- Changing the underlying `ServiceCallMulti` implementation
+
+## Design
+
+### 1. Type Additions
+
+Add streaming-specific response types to `src/models/messages.ts`:
+
+```typescript
+// Agent streaming response (NEW - replaces old AgentResponse for streaming)
+export interface AgentStreamingResponse {
+ chunk_type?: "thought" | "action" | "observation" | "final-answer" | "error";
+ content?: string;
+ end_of_message?: boolean; // Current chunk type is complete
+ end_of_dialog?: boolean; // Entire agent dialog is complete
+
+ // Legacy fields for backward compatibility with non-streaming
+ thought?: string;
+ observation?: string;
+ answer?: string;
+ error?: string;
+}
+
+// Generic streaming response wrapper for RAG/completion services
+export interface StreamingChunk {
+ chunk?: string; // Graph RAG, Document RAG
+ response?: string; // Text Completion
+ text?: string; // Prompt
+ end_of_stream?: boolean;
+ error?: {
+ message: string;
+ type?: string;
+ };
+}
+
+// Request types get optional streaming flag
+export interface AgentRequest {
+ question: string;
+ user?: string;
+ streaming?: boolean; // NEW - enable streaming mode
+}
+
+export interface GraphRagRequest {
+ query: string;
+ user?: string;
+ collection?: string;
+ "entity-limit"?: number;
+ "triple-limit"?: number;
+ "max-subgraph-size"?: number;
+ "max-path-length"?: number;
+ streaming?: boolean; // NEW
+}
+
+export interface DocumentRagRequest {
+ query: string;
+ user?: string;
+ collection?: string;
+ "doc-limit"?: number;
+ streaming?: boolean; // NEW
+}
+
+export interface TextCompletionRequest {
+ system: string;
+ prompt: string;
+ streaming?: boolean; // NEW
+}
+
+export interface PromptRequest {
+ id: string;
+ terms: Record;
+ streaming?: boolean; // NEW
+}
+
+export interface PromptResponse {
+ text: string;
+}
+```
+
+### 2. BaseApi Additions
+
+No changes needed to `BaseApi` - `makeRequestMulti` already exists.
+
+### 3. FlowApi Changes
+
+#### 3.1 Fix Agent Method
+
+Update the existing `agent()` method to correctly handle the backend streaming protocol:
+
+```typescript
+export class FlowApi {
+ /**
+ * Interacts with an AI agent that provides streaming responses
+ * BREAKING CHANGE: Callbacks now receive (chunk, complete) instead of full messages
+ */
+ agent(
+ question: string,
+ think: (chunk: string, complete: boolean) => void,
+ observe: (chunk: string, complete: boolean) => void,
+ answer: (chunk: string, complete: boolean) => void,
+ error: (s: string) => void,
+ ) {
+ const receiver = (response: unknown) => {
+ const resp = response as AgentStreamingResponse;
+
+ // Check for errors
+ if (resp.chunk_type === "error" || resp.error) {
+ const errorMessage = resp.content || resp.error || "Unknown agent error";
+ error(typeof errorMessage === "string" ? errorMessage : String(errorMessage));
+ return true; // End streaming on error
+ }
+
+ // Handle streaming chunks by chunk_type
+ const content = resp.content || "";
+ const messageComplete = !!resp.end_of_message;
+ const dialogComplete = !!resp.end_of_dialog;
+
+ switch (resp.chunk_type) {
+ case "thought":
+ think(content, messageComplete);
+ break;
+ case "observation":
+ observe(content, messageComplete);
+ break;
+ case "final-answer":
+ answer(content, messageComplete);
+ break;
+ case "action":
+ // Actions are typically not streamed incrementally, just logged
+ console.log("Agent action:", content);
+ break;
+ }
+
+ return dialogComplete; // End when backend signals end_of_dialog
+ };
+
+ return this.api
+ .makeRequestMulti(
+ "agent",
+ {
+ question: question,
+ user: this.api.user,
+ streaming: true, // Always use streaming mode
+ },
+ receiver,
+ 120000,
+ 2,
+ this.flowId,
+ )
+ .catch((err) => {
+ const errorMessage =
+ err instanceof Error ? err.message : err?.toString() || "Unknown error";
+ error(`Agent request failed: ${errorMessage}`);
+ });
+ }
+
+#### 3.2 Add New Streaming Methods
+
+Add streaming variants for other services alongside existing methods in `src/socket/trustgraph-socket.ts`:
+
+```typescript
+ // ... existing non-streaming methods unchanged ...
+
+ /**
+ * Performs Graph RAG query with streaming response
+ * @param text - Query text
+ * @param receiver - Called for each chunk with (chunk, complete) where complete=true on final chunk
+ * @param onError - Called on error
+ * @param options - Graph RAG options
+ * @param collection - Collection name
+ */
+ graphRagStreaming(
+ text: string,
+ receiver: (chunk: string, complete: boolean) => void,
+ onError: (error: string) => void,
+ options?: GraphRagOptions,
+ collection?: string,
+ ): void {
+ const recv = (response: unknown): boolean => {
+ const resp = response as StreamingChunk;
+
+ if (resp.error) {
+ onError(resp.error.message);
+ return true; // End streaming
+ }
+
+ const chunk = resp.chunk || "";
+ const complete = !!resp.end_of_stream;
+
+ receiver(chunk, complete);
+
+ return complete; // End when backend signals end_of_stream
+ };
+
+ this.api.makeRequestMulti(
+ "graph-rag",
+ {
+ query: text,
+ user: this.api.user,
+ collection: collection || "default",
+ "entity-limit": options?.entityLimit,
+ "triple-limit": options?.tripleLimit,
+ "max-subgraph-size": options?.maxSubgraphSize,
+ "max-path-length": options?.pathLength,
+ streaming: true,
+ },
+ recv,
+ 60000,
+ undefined,
+ this.flowId,
+ );
+ }
+
+ /**
+ * Performs Document RAG query with streaming response
+ * @param text - Query text
+ * @param receiver - Called for each chunk with (chunk, complete) where complete=true on final chunk
+ * @param onError - Called on error
+ * @param docLimit - Maximum documents to retrieve
+ * @param collection - Collection name
+ */
+ documentRagStreaming(
+ text: string,
+ receiver: (chunk: string, complete: boolean) => void,
+ onError: (error: string) => void,
+ docLimit?: number,
+ collection?: string,
+ ): void {
+ const recv = (response: unknown): boolean => {
+ const resp = response as StreamingChunk;
+
+ if (resp.error) {
+ onError(resp.error.message);
+ return true;
+ }
+
+ const chunk = resp.chunk || "";
+ const complete = !!resp.end_of_stream;
+
+ receiver(chunk, complete);
+
+ return complete;
+ };
+
+ this.api.makeRequestMulti(
+ "document-rag",
+ {
+ query: text,
+ user: this.api.user,
+ collection: collection || "default",
+ "doc-limit": docLimit,
+ streaming: true,
+ },
+ recv,
+ 60000,
+ undefined,
+ this.flowId,
+ );
+ }
+
+ /**
+ * Performs text completion with streaming response
+ * @param system - System prompt
+ * @param text - User prompt
+ * @param receiver - Called for each chunk with (chunk, complete) where complete=true on final chunk
+ * @param onError - Called on error
+ */
+ textCompletionStreaming(
+ system: string,
+ text: string,
+ receiver: (chunk: string, complete: boolean) => void,
+ onError: (error: string) => void,
+ ): void {
+ const recv = (response: unknown): boolean => {
+ const resp = response as StreamingChunk;
+
+ if (resp.error) {
+ onError(resp.error.message);
+ return true;
+ }
+
+ // Text completion uses 'response' field, not 'chunk'
+ const chunk = resp.response || "";
+ const complete = !!resp.end_of_stream;
+
+ receiver(chunk, complete);
+
+ return complete;
+ };
+
+ this.api.makeRequestMulti(
+ "text-completion",
+ {
+ system: system,
+ prompt: text,
+ streaming: true,
+ },
+ recv,
+ 30000,
+ undefined,
+ this.flowId,
+ );
+ }
+
+ /**
+ * Executes a prompt template with streaming response
+ * @param id - Prompt template ID
+ * @param terms - Template variables
+ * @param receiver - Called for each chunk with (chunk, complete) where complete=true on final chunk
+ * @param onError - Called on error
+ */
+ promptStreaming(
+ id: string,
+ terms: Record,
+ receiver: (chunk: string, complete: boolean) => void,
+ onError: (error: string) => void,
+ ): void {
+ const recv = (response: unknown): boolean => {
+ const resp = response as StreamingChunk;
+
+ if (resp.error) {
+ onError(resp.error.message);
+ return true;
+ }
+
+ // Prompt service uses 'text' field
+ const chunk = resp.text || "";
+ const complete = !!resp.end_of_stream;
+
+ receiver(chunk, complete);
+
+ return complete;
+ };
+
+ this.api.makeRequestMulti(
+ "prompt",
+ {
+ id: id,
+ terms: terms,
+ streaming: true,
+ },
+ recv,
+ 30000,
+ undefined,
+ this.flowId,
+ );
+ }
+}
+```
+
+### 4. BaseApi Convenience Methods (Optional)
+
+For users who don't need flow routing, add streaming methods to BaseApi:
+
+```typescript
+export class BaseApi {
+ // Existing methods...
+
+ /**
+ * Streaming text completion without flow routing
+ */
+ textCompletionStreaming(
+ system: string,
+ prompt: string,
+ receiver: (chunk: string, complete: boolean) => void,
+ onError: (error: string) => void,
+ ): void {
+ const flowApi = new FlowApi(this, undefined);
+ flowApi.textCompletionStreaming(system, prompt, receiver, onError);
+ }
+
+ // Similar for graphRagStreaming, documentRagStreaming, promptStreaming...
+}
+```
+
+**Recommendation**: Add these for consistency with existing non-streaming methods on BaseApi.
+
+## Implementation Plan
+
+### Phase 1: Core Types (1 hour)
+1. Add `streaming?: boolean` to request types
+2. Add `StreamingChunk` interface
+3. Add `PromptRequest` and `PromptResponse` types (currently missing)
+
+### Phase 2: FlowApi Streaming Methods (2 hours)
+1. Implement `textCompletionStreaming`
+2. Implement `graphRagStreaming`
+3. Implement `documentRagStreaming`
+4. Implement `promptStreaming`
+5. Add JSDoc comments
+
+### Phase 3: BaseApi Convenience Methods (1 hour)
+1. Add streaming methods to BaseApi
+2. Update interface definitions
+3. Update README with streaming examples
+
+### Phase 4: Testing (2 hours)
+1. Add unit tests for streaming methods
+2. Add integration tests against mock WebSocket
+3. Test error handling mid-stream
+4. Test timeout behavior
+5. Test concurrent streaming requests
+
+### Phase 5: Documentation (1 hour)
+1. Update README with streaming examples
+2. Add streaming guide to docs/
+3. Update API reference
+
+**Total Estimated Time**: 7 hours
+
+## Testing Strategy
+
+### Unit Tests
+
+```typescript
+describe("FlowApi streaming", () => {
+ it("should stream graph-rag chunks", async () => {
+ const chunks: Array<{ chunk: string; complete: boolean }> = [];
+
+ flowApi.graphRagStreaming(
+ "test query",
+ (chunk, complete) => {
+ chunks.push({ chunk, complete });
+ },
+ (error) => fail(error),
+ );
+
+ // Simulate streaming chunks
+ mockWebSocket.simulateMessage({ chunk: "Hello", end_of_stream: false });
+ mockWebSocket.simulateMessage({ chunk: " world", end_of_stream: false });
+ mockWebSocket.simulateMessage({ chunk: "", end_of_stream: true });
+
+ expect(chunks).toEqual([
+ { chunk: "Hello", complete: false },
+ { chunk: " world", complete: false },
+ { chunk: "", complete: true },
+ ]);
+ });
+
+ it("should handle errors mid-stream", async () => {
+ let errorMsg = "";
+ const chunks: string[] = [];
+
+ flowApi.graphRagStreaming(
+ "test query",
+ (chunk, complete) => {
+ chunks.push(chunk);
+ },
+ (error) => {
+ errorMsg = error;
+ },
+ );
+
+ mockWebSocket.simulateMessage({ chunk: "Partial", end_of_stream: false });
+ mockWebSocket.simulateMessage({
+ error: { message: "LLM timeout" },
+ end_of_stream: true,
+ });
+
+ expect(errorMsg).toBe("LLM timeout");
+ expect(chunks).toEqual(["Partial"]); // Receiver gets chunks before error
+ });
+});
+```
+
+### Integration Tests
+
+Test against actual TrustGraph backend (manual testing):
+1. Start TrustGraph backend with streaming enabled
+2. Test each streaming method with real queries
+3. Verify chunks arrive in order
+4. Verify end_of_stream handling
+5. Test error scenarios (invalid query, timeout)
+
+## Migration Guide
+
+### For Users
+
+#### Graph RAG / Document RAG / Text Completion / Prompt
+
+**Before (non-streaming)**:
+```typescript
+const response = await flowApi.graphRag("What is machine learning?");
+console.log(response); // Full text after 10-30 seconds
+```
+
+**After (streaming)**:
+```typescript
+let accumulated = "";
+
+flowApi.graphRagStreaming(
+ "What is machine learning?",
+ (chunk, complete) => {
+ accumulated += chunk;
+ updateDisplay(accumulated);
+
+ if (complete) {
+ console.log("Final:", accumulated);
+ }
+ },
+ (error) => {
+ console.error("Error:", error);
+ }
+);
+```
+
+#### Agent (BREAKING CHANGE)
+
+**Before (old client - incorrect)**:
+```typescript
+flowApi.agent(
+ "What is machine learning?",
+ (thought) => console.log("Thinking:", thought), // Full thought received
+ (observation) => console.log("Observing:", observation), // Full observation received
+ (answer) => console.log("Answer:", answer), // Full answer received
+ (error) => console.error(error),
+);
+```
+
+**After (updated to match backend)**:
+```typescript
+let currentThought = "";
+let currentObservation = "";
+let currentAnswer = "";
+
+flowApi.agent(
+ "What is machine learning?",
+ (chunk, complete) => {
+ currentThought += chunk;
+ updateThinkingDisplay(currentThought);
+ if (complete) {
+ console.log("Thought complete:", currentThought);
+ currentThought = ""; // Reset for next thought
+ }
+ },
+ (chunk, complete) => {
+ currentObservation += chunk;
+ updateObservationDisplay(currentObservation);
+ if (complete) {
+ console.log("Observation complete:", currentObservation);
+ currentObservation = "";
+ }
+ },
+ (chunk, complete) => {
+ currentAnswer += chunk;
+ updateAnswerDisplay(currentAnswer);
+ if (complete) {
+ console.log("Final answer:", currentAnswer);
+ }
+ },
+ (error) => console.error(error),
+);
+```
+
+### Gradual Adoption
+
+**For Graph RAG / Document RAG / Text Completion / Prompt**:
+1. Continue using non-streaming APIs (no breaking changes)
+2. Add streaming variants for user-facing chat interfaces first
+3. Keep non-streaming for background tasks
+4. Optionally add feature flag to toggle streaming on/off
+
+**For Agent (BREAKING CHANGE)**:
+1. Existing Agent users MUST update their callbacks to handle (chunk, complete) signature
+2. Add accumulation logic in callback handlers
+3. Use `complete` flag to detect when to reset accumulator or take final action
+
+## Risks and Mitigations
+
+### Risk 1: BREAKING CHANGE for Agent API
+**Concern**: Existing Agent users must update their code when they upgrade.
+
+**Mitigation**:
+- Document the breaking change clearly in release notes
+- Provide migration examples in this spec
+- Consider: Add deprecation warning in previous version before breaking change
+- Consider: Bump major version to signal breaking change
+- The old API was incorrect anyway - this fixes a bug in the client
+
+### Risk 2: API Surface Growth
+**Concern**: Adding 4 new methods per API class (FlowApi, BaseApi) increases maintenance burden.
+
+**Mitigation**:
+- Methods share identical structure (only field name differs: chunk/response/text)
+- Could extract common streaming handler if needed
+- Backend already implements streaming, so no protocol risk
+
+### Risk 3: TypeScript Type Safety
+**Concern**: `StreamingChunk` union type may be confusing (chunk vs response vs text).
+
+**Mitigation**:
+- Each service method documents which field it uses
+- Runtime code checks correct field
+- Implementation is simple enough that field selection is obvious
+
+### Risk 4: State Management in User Code
+**Concern**: Users must manually accumulate chunks if they need full text.
+
+**Mitigation**:
+- This is intentional - client stays policy-free
+- Higher-level abstractions (React hooks, etc.) can provide accumulation
+- For users who don't need streaming behavior, non-streaming APIs remain unchanged
+
+## Future Enhancements
+
+### 1. Async Iterator API
+Provide a modern streaming API using async iterators:
+
+```typescript
+async *graphRagStream(text: string): AsyncGenerator {
+ // Wraps graphRagStreaming in async iterator
+}
+
+// Usage:
+for await (const chunk of flowApi.graphRagStream("query")) {
+ console.log(chunk);
+}
+```
+
+### 2. Retry on Stream Interruption
+Currently, retries only apply to initial request. Could add mid-stream retry:
+- Detect connection drop mid-stream
+- Resume from last chunk (if backend supports resumption)
+
+### 3. Client-Side Buffering
+For very fast chunk arrival, buffer multiple chunks before calling receiver:
+- Reduces callback frequency
+- Could be opt-in via options parameter
+- Note: This would add policy to the client, may be better in higher layers
+
+### 4. Stream Cancellation
+Allow users to cancel in-flight streaming requests:
+```typescript
+const cancel = flowApi.graphRagStreaming(...);
+// Later:
+cancel();
+```
+
+## Alternatives Considered
+
+### Alternative 1: Separate Callbacks for Chunk and Complete
+Use three callbacks: onChunk, onComplete, onError:
+
+```typescript
+graphRagStreaming(
+ text: string,
+ onChunk: (chunk: string, accumulated: string) => void,
+ onComplete: (fullText: string) => void,
+ onError: (error: string) => void,
+)
+```
+
+**Rejected because**:
+- Adds state management (accumulation) to the client layer
+- Harder for implementations that need both signals at once
+- More verbose callback signature
+
+### Alternative 2: Unified Streaming Flag on Existing Methods
+Modify existing methods to detect streaming callbacks:
+
+```typescript
+graphRag(
+ text: string,
+ options?: GraphRagOptions,
+ collection?: string,
+ receiver?: (chunk: string, complete: boolean) => void,
+): Promise | void
+```
+
+**Rejected because**:
+- Violates single responsibility principle
+- Return type becomes conditional (Promise vs void)
+- Hard to type correctly in TypeScript
+- Confusing API (streaming vs non-streaming behavior implicit)
+
+### Alternative 3: Separate StreamingFlowApi Class
+Create a parallel API class for streaming:
+
+```typescript
+export class StreamingFlowApi {
+ graphRag(text: string, receiver: ..., onError: ...): void;
+ documentRag(text: string, receiver: ..., onError: ...): void;
+}
+```
+
+**Rejected because**:
+- Duplicates all configuration and state management
+- Users must manage two API instances
+- No clear benefit over method suffixes
+
+## Open Questions
+
+1. **Should we add streaming to Prompt service?**
+ - Prompt service is not currently in client (no PromptRequest/Response types)
+ - Could add it alongside streaming support
+ - **Decision**: Yes, add it for completeness (mentioned in backend docs)
+
+2. **Should we add TypeScript overloads?**
+ - Allow `graphRagStreaming(text, callbacks)` vs `graphRagStreaming(text, options, callbacks)`
+ - **Decision**: Use optional parameters (simpler implementation)
+
+## Conclusion
+
+This proposal adds streaming support to the TrustGraph client and fixes the Agent API to correctly implement the backend protocol:
+
+**Changes**:
+1. **Fix Agent API** (BREAKING): Update callbacks to receive `(chunk, complete)` instead of full messages
+2. Add `streaming?: boolean` flag to all request types
+3. Add `AgentStreamingResponse` and `StreamingChunk` response types
+4. Add `*Streaming` method variants to FlowApi and BaseApi for RAG/completion services
+5. Use consistent two-callback pattern: `receiver(chunk, complete)` and `onError(message)` across all services
+
+The implementation is straightforward (~7-10 hours including Agent fix), stays minimal and focused, and provides a clean foundation for higher-level abstractions to build upon.
+
+**Key Design Principles**:
+- **Policy-free**: No accumulation or buffering in client layer
+- **Minimal callbacks**: Single receiver gets both chunk and completion signal
+- **Protocol-correct**: Agent now properly implements backend's chunk_type/content/end_of_message protocol
+- **Consistent**: Same pattern across all streaming services
+- **Backward compatible**: Existing non-streaming APIs unchanged (except Agent which needs fixing)
+
+**Breaking Changes**:
+- Agent API callbacks change from `(fullMessage: string)` to `(chunk: string, complete: boolean)`
+- Requires major version bump
+
+**Recommendation**: Approve and implement in current sprint.
diff --git a/ai-context/trustgraph-client/eslint.config.js b/ai-context/trustgraph-client/eslint.config.js
new file mode 100644
index 00000000..462b81af
--- /dev/null
+++ b/ai-context/trustgraph-client/eslint.config.js
@@ -0,0 +1,35 @@
+import js from "@eslint/js";
+import tseslint from "typescript-eslint";
+import globals from "globals";
+
+export default tseslint.config(
+ js.configs.recommended,
+ ...tseslint.configs.recommended,
+ {
+ files: ["**/*.{ts,tsx}"],
+ languageOptions: {
+ parser: tseslint.parser,
+ parserOptions: {
+ ecmaVersion: "latest",
+ sourceType: "module",
+ },
+ globals: {
+ ...globals.browser,
+ ...globals.es2021,
+ },
+ },
+ rules: {
+ "@typescript-eslint/no-unused-vars": [
+ "warn",
+ {
+ argsIgnorePattern: "^_",
+ varsIgnorePattern: "^_",
+ },
+ ],
+ "@typescript-eslint/no-explicit-any": "warn",
+ },
+ },
+ {
+ ignores: ["dist/**", "node_modules/**", "*.config.js"],
+ },
+);
diff --git a/ai-context/trustgraph-client/package.json b/ai-context/trustgraph-client/package.json
new file mode 100644
index 00000000..c47b1002
--- /dev/null
+++ b/ai-context/trustgraph-client/package.json
@@ -0,0 +1,66 @@
+{
+ "name": "@trustgraph/client",
+ "version": "1.6.0",
+ "description": "TypeScript client for TrustGraph",
+ "type": "module",
+ "main": "dist/index.esm.js",
+ "module": "dist/index.esm.js",
+ "types": "dist/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "import": "./dist/index.esm.js",
+ "require": "./dist/index.cjs"
+ }
+ },
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "build": "rollup -c",
+ "dev": "rollup -c -w",
+ "test": "vitest run",
+ "test:watch": "vitest",
+ "test:ui": "vitest --ui",
+ "lint": "eslint src",
+ "typecheck": "tsc --noEmit",
+ "prettify": "prettier --write .",
+ "prepare": "npm run build"
+ },
+ "keywords": [
+ "trustgraph",
+ "websocket",
+ "typescript",
+ "client"
+ ],
+ "author": "KnowNext Limited",
+ "license": "Apache-2.0",
+ "devDependencies": {
+ "@eslint/js": "^9.37.0",
+ "@rollup/plugin-commonjs": "^25.0.7",
+ "@rollup/plugin-node-resolve": "^15.2.3",
+ "@rollup/plugin-typescript": "^11.1.6",
+ "@vitest/ui": "^3.2.4",
+ "eslint": "^9.39.3",
+ "globals": "^16.4.0",
+ "happy-dom": "^20.0.10",
+ "jiti": "^2.6.1",
+ "prettier": "^3.6.2",
+ "rollup": "^4.9.0",
+ "tslib": "^2.6.2",
+ "typescript": "^5.3.3",
+ "typescript-eslint": "^8.46.0",
+ "vitest": "^3.2.4"
+ },
+ "directories": {
+ "doc": "docs"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/trustgraph-ai/trustgraph-client.git"
+ },
+ "bugs": {
+ "url": "https://github.com/trustgraph-ai/trustgraph-client/issues"
+ },
+ "homepage": "https://github.com/trustgraph-ai/trustgraph-client#readme"
+}
diff --git a/ai-context/trustgraph-client/rollup.config.js b/ai-context/trustgraph-client/rollup.config.js
new file mode 100644
index 00000000..ffa5e319
--- /dev/null
+++ b/ai-context/trustgraph-client/rollup.config.js
@@ -0,0 +1,30 @@
+import resolve from "@rollup/plugin-node-resolve";
+import commonjs from "@rollup/plugin-commonjs";
+import typescript from "@rollup/plugin-typescript";
+
+export default {
+ input: "src/index.ts",
+ output: [
+ {
+ file: "dist/index.cjs",
+ format: "cjs",
+ sourcemap: true,
+ },
+ {
+ file: "dist/index.esm.js",
+ format: "esm",
+ sourcemap: true,
+ },
+ ],
+ external: ["react", "react-dom"],
+ plugins: [
+ resolve(),
+ commonjs(),
+ typescript({
+ tsconfig: "./tsconfig.json",
+ declaration: true,
+ declarationDir: "dist",
+ rootDir: "src",
+ }),
+ ],
+};
diff --git a/ai-context/trustgraph-client/src/__tests__/flows-api.test.ts b/ai-context/trustgraph-client/src/__tests__/flows-api.test.ts
new file mode 100644
index 00000000..e011af72
--- /dev/null
+++ b/ai-context/trustgraph-client/src/__tests__/flows-api.test.ts
@@ -0,0 +1,221 @@
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { FlowsApi } from "../socket/trustgraph-socket";
+import { FlowResponse } from "../models/messages";
+
+describe("FlowsApi", () => {
+ let mockApi: {
+ makeRequest: ReturnType;
+ };
+ let flowsApi: FlowsApi;
+
+ beforeEach(() => {
+ mockApi = {
+ makeRequest: vi.fn(),
+ };
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ flowsApi = new FlowsApi(mockApi as any);
+ });
+
+ describe("startFlow", () => {
+ it("should call makeRequest with correct types and parameters", async () => {
+ const mockResponse: FlowResponse = {
+ flow: "started",
+ description: "Flow started successfully",
+ };
+ mockApi.makeRequest.mockResolvedValue(mockResponse);
+
+ const result = await flowsApi.startFlow(
+ "test-flow-id",
+ "test-class",
+ "Test description",
+ );
+
+ expect(mockApi.makeRequest).toHaveBeenCalledWith(
+ "flow",
+ {
+ operation: "start-flow",
+ "flow-id": "test-flow-id",
+ "blueprint-name": "test-class",
+ description: "Test description",
+ },
+ 30000,
+ );
+ expect(result).toEqual(mockResponse);
+ });
+
+ it("should use FlowRequest and FlowResponse types", async () => {
+ const mockResponse: FlowResponse = {};
+ mockApi.makeRequest.mockResolvedValue(mockResponse);
+
+ await flowsApi.startFlow("id", "class", "desc");
+
+ // Verify the call signature matches FlowRequest/FlowResponse types
+ const callArgs = mockApi.makeRequest.mock.calls[0];
+ const request = callArgs[1];
+
+ // These properties should match FlowRequest interface
+ expect(request).toHaveProperty("operation");
+ expect(request).toHaveProperty("flow-id");
+ expect(request).toHaveProperty("blueprint-name");
+ expect(request).toHaveProperty("description");
+ });
+ });
+
+ describe("stopFlow", () => {
+ it("should call makeRequest with correct types and parameters", async () => {
+ const mockResponse: FlowResponse = {
+ flow: "stopped",
+ description: "Flow stopped successfully",
+ };
+ mockApi.makeRequest.mockResolvedValue(mockResponse);
+
+ const result = await flowsApi.stopFlow("test-flow-id");
+
+ expect(mockApi.makeRequest).toHaveBeenCalledWith(
+ "flow",
+ {
+ operation: "stop-flow",
+ "flow-id": "test-flow-id",
+ },
+ 30000,
+ );
+ expect(result).toEqual(mockResponse);
+ });
+
+ it("should use FlowRequest and FlowResponse types", async () => {
+ const mockResponse: FlowResponse = {};
+ mockApi.makeRequest.mockResolvedValue(mockResponse);
+
+ await flowsApi.stopFlow("id");
+
+ // Verify the call signature matches FlowRequest/FlowResponse types
+ const callArgs = mockApi.makeRequest.mock.calls[0];
+ const request = callArgs[1];
+
+ // These properties should match FlowRequest interface
+ expect(request).toHaveProperty("operation");
+ expect(request).toHaveProperty("flow-id");
+ });
+ });
+
+ describe("getFlows", () => {
+ it("should return flow-ids array from response", async () => {
+ const mockResponse: FlowResponse = {
+ "flow-ids": ["flow1", "flow2", "flow3"],
+ };
+ mockApi.makeRequest.mockResolvedValue(mockResponse);
+
+ const result = await flowsApi.getFlows();
+
+ expect(mockApi.makeRequest).toHaveBeenCalledWith(
+ "flow",
+ {
+ operation: "list-flows",
+ },
+ 60000,
+ );
+ expect(result).toEqual(["flow1", "flow2", "flow3"]);
+ });
+
+ it("should return empty array when flow-ids is undefined", async () => {
+ const mockResponse: FlowResponse = {};
+ mockApi.makeRequest.mockResolvedValue(mockResponse);
+
+ const result = await flowsApi.getFlows();
+
+ expect(result).toEqual([]);
+ });
+
+ it("should handle response with flow-ids property correctly", async () => {
+ // This test ensures we're accessing the hyphenated property name correctly
+ const mockResponse = {
+ "flow-ids": ["test-flow"],
+ "other-property": "should-be-ignored",
+ };
+ mockApi.makeRequest.mockResolvedValue(mockResponse);
+
+ const result = await flowsApi.getFlows();
+
+ expect(result).toEqual(["test-flow"]);
+ });
+ });
+
+ describe("getFlowBlueprints", () => {
+ it("should return blueprint-names array from response", async () => {
+ const mockResponse: FlowResponse = {
+ "blueprint-names": ["class1", "class2"],
+ };
+ mockApi.makeRequest.mockResolvedValue(mockResponse);
+
+ const result = await flowsApi.getFlowBlueprints();
+
+ expect(mockApi.makeRequest).toHaveBeenCalledWith(
+ "flow",
+ {
+ operation: "list-blueprints",
+ },
+ 60000,
+ );
+ expect(result).toEqual(["class1", "class2"]);
+ });
+
+ it("should handle response with blueprint-names property correctly", async () => {
+ // This test ensures we're accessing the hyphenated property name correctly
+ const mockResponse = {
+ "blueprint-names": ["test-class"],
+ "other-property": "should-be-ignored",
+ };
+ mockApi.makeRequest.mockResolvedValue(mockResponse);
+
+ const result = await flowsApi.getFlowBlueprints();
+
+ expect(result).toEqual(["test-class"]);
+ });
+ });
+
+ describe("getFlow", () => {
+ it("should call makeRequest with correct parameters and parse JSON", async () => {
+ const flowDefinition = { type: "flow", config: "test" };
+ const mockResponse: FlowResponse = {
+ flow: JSON.stringify(flowDefinition), // Must be valid JSON string
+ description: "Test flow",
+ };
+ mockApi.makeRequest.mockResolvedValue(mockResponse);
+
+ const result = await flowsApi.getFlow("test-flow-id");
+
+ expect(mockApi.makeRequest).toHaveBeenCalledWith(
+ "flow",
+ {
+ operation: "get-flow",
+ "flow-id": "test-flow-id",
+ },
+ 60000,
+ );
+ expect(result).toEqual(flowDefinition); // Result should be parsed JSON
+ });
+ });
+
+ describe("getFlowBlueprint", () => {
+ it("should call makeRequest with correct parameters and parse JSON", async () => {
+ const blueprintDefinition = { type: "blueprint", name: "test-blueprint" };
+ const mockResponse: FlowResponse = {
+ "blueprint-definition": JSON.stringify(blueprintDefinition), // Must be valid JSON string
+ description: "Test blueprint",
+ };
+ mockApi.makeRequest.mockResolvedValue(mockResponse);
+
+ const result = await flowsApi.getFlowBlueprint("test-class");
+
+ expect(mockApi.makeRequest).toHaveBeenCalledWith(
+ "flow",
+ {
+ operation: "get-blueprint",
+ "blueprint-name": "test-class",
+ },
+ 60000,
+ );
+ expect(result).toEqual(blueprintDefinition); // Result should be parsed JSON
+ });
+ });
+});
diff --git a/ai-context/trustgraph-client/src/__tests__/messages.test.ts b/ai-context/trustgraph-client/src/__tests__/messages.test.ts
new file mode 100644
index 00000000..65d96c9e
--- /dev/null
+++ b/ai-context/trustgraph-client/src/__tests__/messages.test.ts
@@ -0,0 +1,370 @@
+import { describe, it, expect } from "vitest";
+import type {
+ RequestMessage,
+ ApiResponse,
+ TextCompletionRequest,
+ TextCompletionResponse,
+ GraphRagRequest,
+ GraphRagResponse,
+ AgentRequest,
+ AgentResponse,
+ EmbeddingsRequest,
+ EmbeddingsResponse,
+ GraphEmbeddingsQueryRequest,
+ GraphEmbeddingsQueryResponse,
+ TriplesQueryRequest,
+ LoadDocumentRequest,
+ LoadTextRequest,
+ LibraryRequest,
+ LibraryResponse,
+ FlowRequest,
+ FlowResponse,
+ DocumentMetadata,
+ ProcessingMetadata,
+} from "../models/messages";
+
+describe("Message Types", () => {
+ describe("RequestMessage", () => {
+ it("should have correct structure", () => {
+ const message: RequestMessage = {
+ id: "test-id",
+ service: "test-service",
+ request: { test: "data" },
+ };
+
+ expect(message.id).toBe("test-id");
+ expect(message.service).toBe("test-service");
+ expect(message.request).toEqual({ test: "data" });
+ });
+ });
+
+ describe("ApiResponse", () => {
+ it("should have correct structure", () => {
+ const response: ApiResponse = {
+ id: "test-id",
+ response: { result: "success" },
+ };
+
+ expect(response.id).toBe("test-id");
+ expect(response.response).toEqual({ result: "success" });
+ });
+ });
+
+ describe("TextCompletionRequest", () => {
+ it("should have correct structure", () => {
+ const request: TextCompletionRequest = {
+ system: "You are a helpful assistant",
+ prompt: "Hello, world!",
+ };
+
+ expect(request.system).toBe("You are a helpful assistant");
+ expect(request.prompt).toBe("Hello, world!");
+ });
+ });
+
+ describe("TextCompletionResponse", () => {
+ it("should have correct structure", () => {
+ const response: TextCompletionResponse = {
+ response: "Hello! How can I help you today?",
+ };
+
+ expect(response.response).toBe("Hello! How can I help you today?");
+ });
+ });
+
+ describe("GraphRagRequest", () => {
+ it("should have correct structure with required query", () => {
+ const request: GraphRagRequest = {
+ query: "What is the capital of France?",
+ };
+
+ expect(request.query).toBe("What is the capital of France?");
+ });
+
+ it("should have correct structure with optional parameters", () => {
+ const request: GraphRagRequest = {
+ query: "What is the capital of France?",
+ "entity-limit": 100,
+ "triple-limit": 50,
+ "max-subgraph-size": 2000,
+ "max-path-length": 3,
+ };
+
+ expect(request.query).toBe("What is the capital of France?");
+ expect(request["entity-limit"]).toBe(100);
+ expect(request["triple-limit"]).toBe(50);
+ expect(request["max-subgraph-size"]).toBe(2000);
+ expect(request["max-path-length"]).toBe(3);
+ });
+ });
+
+ describe("GraphRagResponse", () => {
+ it("should have correct structure", () => {
+ const response: GraphRagResponse = {
+ response: "The capital of France is Paris.",
+ };
+
+ expect(response.response).toBe("The capital of France is Paris.");
+ });
+ });
+
+ describe("AgentRequest", () => {
+ it("should have correct structure", () => {
+ const request: AgentRequest = {
+ question: "What is the weather like today?",
+ };
+
+ expect(request.question).toBe("What is the weather like today?");
+ });
+ });
+
+ describe("AgentResponse", () => {
+ it("should have correct structure with all fields", () => {
+ const response: AgentResponse = {
+ thought: "I need to check the weather",
+ observation: "Weather API shows sunny conditions",
+ answer: "It is sunny today",
+ error: undefined,
+ };
+
+ expect(response.thought).toBe("I need to check the weather");
+ expect(response.observation).toBe("Weather API shows sunny conditions");
+ expect(response.answer).toBe("It is sunny today");
+ expect(response.error).toBeUndefined();
+ });
+
+ it("should handle error response", () => {
+ const response: AgentResponse = {
+ error: { type: "agent-error", message: "Weather service unavailable" },
+ };
+
+ expect(response.error?.message).toBe("Weather service unavailable");
+ expect(response.error?.type).toBe("agent-error");
+ });
+ });
+
+ describe("EmbeddingsRequest", () => {
+ it("should have correct structure", () => {
+ const request: EmbeddingsRequest = {
+ texts: ["This is a test sentence for embedding", "Another text"],
+ };
+
+ expect(request.texts).toEqual(["This is a test sentence for embedding", "Another text"]);
+ });
+ });
+
+ describe("EmbeddingsResponse", () => {
+ it("should have correct structure", () => {
+ // vectors[text_index][dimension_index] - one vector per input text
+ const response: EmbeddingsResponse = {
+ vectors: [
+ [0.1, 0.2, 0.3], // First text's vector
+ [0.4, 0.5, 0.6], // Second text's vector
+ ],
+ };
+
+ expect(response.vectors).toEqual([
+ [0.1, 0.2, 0.3],
+ [0.4, 0.5, 0.6],
+ ]);
+ });
+ });
+
+ describe("GraphEmbeddingsQueryRequest", () => {
+ it("should have correct structure", () => {
+ const request: GraphEmbeddingsQueryRequest = {
+ vector: [0.1, 0.2, 0.3],
+ limit: 10,
+ };
+
+ expect(request.vector).toEqual([0.1, 0.2, 0.3]);
+ expect(request.limit).toBe(10);
+ });
+ });
+
+ describe("GraphEmbeddingsQueryResponse", () => {
+ it("should have correct structure", () => {
+ const response: GraphEmbeddingsQueryResponse = {
+ entities: [
+ { entity: { t: "i", i: "http://example.org/entity1" }, score: 0.95 },
+ { entity: { t: "i", i: "http://example.org/entity2" }, score: 0.87 },
+ ],
+ };
+
+ expect(response.entities).toHaveLength(2);
+ expect(response.entities[0].score).toBe(0.95);
+ expect(response.entities[0].entity?.t).toBe("i");
+ expect((response.entities[0].entity as { t: "i"; i: string }).i).toBe("http://example.org/entity1");
+ expect(response.entities[1].score).toBe(0.87);
+ });
+ });
+
+ describe("TriplesQueryRequest", () => {
+ it("should have correct structure with all fields", () => {
+ const request: TriplesQueryRequest = {
+ s: { t: "i", i: "http://example.org/subject" },
+ p: { t: "i", i: "http://example.org/predicate" },
+ o: { t: "l", v: "object value" },
+ limit: 100,
+ };
+
+ expect((request.s as { t: "i"; i: string }).i).toBe("http://example.org/subject");
+ expect((request.p as { t: "i"; i: string }).i).toBe("http://example.org/predicate");
+ expect((request.o as { t: "l"; v: string }).v).toBe("object value");
+ expect(request.limit).toBe(100);
+ });
+
+ it("should handle optional fields", () => {
+ const request: TriplesQueryRequest = {
+ limit: 50,
+ };
+
+ expect(request.s).toBeUndefined();
+ expect(request.p).toBeUndefined();
+ expect(request.o).toBeUndefined();
+ expect(request.limit).toBe(50);
+ });
+ });
+
+ describe("LoadDocumentRequest", () => {
+ it("should have correct structure", () => {
+ const request: LoadDocumentRequest = {
+ id: "doc-123",
+ data: "base64-encoded-document-data",
+ metadata: [
+ {
+ s: { t: "i", i: "http://example.org/doc-123" },
+ p: { t: "i", i: "http://example.org/title" },
+ o: { t: "l", v: "Test Document" },
+ },
+ ],
+ };
+
+ expect(request.id).toBe("doc-123");
+ expect(request.data).toBe("base64-encoded-document-data");
+ expect(request.metadata).toHaveLength(1);
+ });
+ });
+
+ describe("LoadTextRequest", () => {
+ it("should have correct structure", () => {
+ const request: LoadTextRequest = {
+ id: "text-123",
+ text: "This is some text to load",
+ charset: "utf-8",
+ metadata: [],
+ };
+
+ expect(request.id).toBe("text-123");
+ expect(request.text).toBe("This is some text to load");
+ expect(request.charset).toBe("utf-8");
+ expect(request.metadata).toEqual([]);
+ });
+ });
+
+ describe("DocumentMetadata", () => {
+ it("should have correct structure", () => {
+ const metadata: DocumentMetadata = {
+ id: "doc-123",
+ time: 1640995200000,
+ kind: "pdf",
+ title: "Test Document",
+ comments: "A test document",
+ metadata: [],
+ user: "test-user",
+ tags: ["test", "document"],
+ };
+
+ expect(metadata.id).toBe("doc-123");
+ expect(metadata.time).toBe(1640995200000);
+ expect(metadata.kind).toBe("pdf");
+ expect(metadata.title).toBe("Test Document");
+ expect(metadata.comments).toBe("A test document");
+ expect(metadata.user).toBe("test-user");
+ expect(metadata.tags).toEqual(["test", "document"]);
+ });
+ });
+
+ describe("ProcessingMetadata", () => {
+ it("should have correct structure", () => {
+ const metadata: ProcessingMetadata = {
+ id: "proc-123",
+ "document-id": "doc-123",
+ time: 1640995200000,
+ flow: "default-flow",
+ user: "test-user",
+ collection: "test-collection",
+ tags: ["processing", "test"],
+ };
+
+ expect(metadata.id).toBe("proc-123");
+ expect(metadata["document-id"]).toBe("doc-123");
+ expect(metadata.time).toBe(1640995200000);
+ expect(metadata.flow).toBe("default-flow");
+ expect(metadata.user).toBe("test-user");
+ expect(metadata.collection).toBe("test-collection");
+ expect(metadata.tags).toEqual(["processing", "test"]);
+ });
+ });
+
+ describe("LibraryRequest", () => {
+ it("should have correct structure", () => {
+ const request: LibraryRequest = {
+ operation: "list_documents",
+ user: "test-user",
+ collection: "test-collection",
+ };
+
+ expect(request.operation).toBe("list_documents");
+ expect(request.user).toBe("test-user");
+ expect(request.collection).toBe("test-collection");
+ });
+ });
+
+ describe("LibraryResponse", () => {
+ it("should have correct structure", () => {
+ const response: LibraryResponse = {
+ error: new Error(),
+ "document-metadatas": [
+ {
+ id: "doc-1",
+ title: "Document 1",
+ time: 1640995200000,
+ },
+ ],
+ };
+
+ expect(response.error).toBeInstanceOf(Error);
+ expect(response["document-metadatas"]).toHaveLength(1);
+ expect(response["document-metadatas"]![0].id).toBe("doc-1");
+ });
+ });
+
+ describe("FlowRequest", () => {
+ it("should have correct structure", () => {
+ const request: FlowRequest = {
+ operation: "get_flow",
+ "flow-id": "default-flow",
+ };
+
+ expect(request.operation).toBe("get_flow");
+ expect(request["flow-id"]).toBe("default-flow");
+ });
+ });
+
+ describe("FlowResponse", () => {
+ it("should have correct structure", () => {
+ const response: FlowResponse = {
+ "flow-ids": ["flow-1", "flow-2"],
+ flow: "flow-definition",
+ description: "A test flow",
+ error: undefined,
+ };
+
+ expect(response["flow-ids"]).toEqual(["flow-1", "flow-2"]);
+ expect(response.flow).toBe("flow-definition");
+ expect(response.description).toBe("A test flow");
+ expect(response.error).toBeUndefined();
+ });
+ });
+});
diff --git a/ai-context/trustgraph-client/src/__tests__/service-call-multi.test.ts b/ai-context/trustgraph-client/src/__tests__/service-call-multi.test.ts
new file mode 100644
index 00000000..c414c574
--- /dev/null
+++ b/ai-context/trustgraph-client/src/__tests__/service-call-multi.test.ts
@@ -0,0 +1,285 @@
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { ServiceCallMulti } from "../socket/service-call-multi";
+
+// Mock WebSocket constants
+vi.stubGlobal("WebSocket", {
+ OPEN: 1,
+ CONNECTING: 0,
+ CLOSING: 2,
+ CLOSED: 3,
+});
+
+// Mock Socket interface
+const mockSocket = {
+ inflight: {} as Record,
+ ws: {
+ send: vi.fn(),
+ readyState: 1, // WebSocket.OPEN
+ },
+ reopen: vi.fn(),
+};
+
+// Mock setTimeout and clearTimeout
+const mockSetTimeout = vi.fn();
+const mockClearTimeout = vi.fn();
+
+vi.stubGlobal("setTimeout", mockSetTimeout);
+vi.stubGlobal("clearTimeout", mockClearTimeout);
+
+describe("ServiceCallMulti", () => {
+ let mockSuccess: ReturnType;
+ let mockError: ReturnType;
+ let mockReceiver: ReturnType;
+ let serviceCallMulti: ServiceCallMulti;
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockSuccess = vi.fn();
+ mockError = vi.fn();
+ mockReceiver = vi.fn();
+ mockSocket.inflight = {} as Record;
+ mockSocket.ws = {
+ send: vi.fn(),
+ readyState: 1, // WebSocket.OPEN
+ };
+ mockSocket.reopen.mockClear();
+
+ serviceCallMulti = new ServiceCallMulti(
+ "test-mid",
+ { id: "test-id", service: "test-service", request: { test: "data" } },
+ mockSuccess,
+ mockError,
+ 5000, // 5 second timeout
+ 3, // 3 retries
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ mockSocket as any,
+ mockReceiver,
+ );
+ });
+
+ it("should initialize with correct properties", () => {
+ expect(serviceCallMulti.mid).toBe("test-mid");
+ expect(serviceCallMulti.timeout).toBe(5000);
+ expect(serviceCallMulti.retries).toBe(3);
+ expect(serviceCallMulti.complete).toBe(false);
+ expect(serviceCallMulti.socket).toBe(mockSocket);
+ expect(serviceCallMulti.receiver).toBe(mockReceiver);
+ });
+
+ it("should register itself in socket inflight when started", () => {
+ serviceCallMulti.start();
+
+ expect(mockSocket.inflight["test-mid"]).toBe(serviceCallMulti);
+ });
+
+ it("should send message on successful attempt", () => {
+ serviceCallMulti.start();
+
+ expect(mockSocket.ws.send).toHaveBeenCalledWith(
+ JSON.stringify({
+ id: "test-id",
+ service: "test-service",
+ request: { test: "data" },
+ }),
+ );
+ expect(mockSetTimeout).toHaveBeenCalled();
+ });
+
+ it("should handle response when receiver returns true (completion)", () => {
+ mockReceiver.mockReturnValue(true); // Signal completion
+ const response = { result: "success" };
+
+ serviceCallMulti.start();
+ serviceCallMulti.onReceived(response);
+
+ expect(mockReceiver).toHaveBeenCalledWith(response);
+ expect(serviceCallMulti.complete).toBe(true);
+ expect(mockSuccess).toHaveBeenCalledWith(response);
+ expect(mockClearTimeout).toHaveBeenCalled();
+ expect(mockSocket.inflight["test-mid"]).toBeUndefined();
+ });
+
+ it("should handle response when receiver returns false (continue)", () => {
+ mockReceiver.mockReturnValue(false); // Signal to continue
+ const response = { partial: "data" };
+
+ serviceCallMulti.start();
+ serviceCallMulti.onReceived(response);
+
+ expect(mockReceiver).toHaveBeenCalledWith(response);
+ expect(serviceCallMulti.complete).toBe(false);
+ expect(mockSuccess).not.toHaveBeenCalled();
+ expect(mockClearTimeout).not.toHaveBeenCalled();
+ expect(mockSocket.inflight["test-mid"]).toBe(serviceCallMulti);
+ });
+
+ it("should handle timeout and retry", () => {
+ serviceCallMulti.start();
+
+ // Initial retries should be 3, but start() calls attempt() which decrements to 2
+ expect(serviceCallMulti.retries).toBe(2);
+
+ // Simulate timeout
+ serviceCallMulti.onTimeout();
+
+ expect(mockClearTimeout).toHaveBeenCalled();
+ expect(serviceCallMulti.retries).toBe(1); // Should decrement from 2 to 1
+ });
+
+ it("should exhaust retries and call error callback", () => {
+ // Set retries to 0 to force immediate failure
+ serviceCallMulti.retries = 0;
+
+ serviceCallMulti.start();
+
+ expect(mockError).toHaveBeenCalledWith("Ran out of retries");
+ expect(mockSocket.inflight["test-mid"]).toBeUndefined();
+ });
+
+ it("should handle WebSocket send failure", () => {
+ mockSocket.ws.send.mockImplementation(() => {
+ throw new Error("Connection failed");
+ });
+
+ serviceCallMulti.start();
+
+ expect(mockSocket.reopen).toHaveBeenCalled();
+
+ // With exponential backoff, the delay should be calculated as:
+ // SOCKET_RECONNECTION_TIMEOUT * Math.pow(2, 3 - retries) + random
+ // Since retries is decremented to 2 after start(), it's 3 - 2 = 1
+ // So base delay is 2000 * 2^1 = 4000, plus random up to 1000
+ // The delay should be between 4000 and 5000ms (capped at 30000)
+ const callArgs = mockSetTimeout.mock.calls[0];
+ expect(callArgs[0]).toEqual(expect.any(Function));
+ expect(callArgs[1]).toBeGreaterThanOrEqual(4000);
+ expect(callArgs[1]).toBeLessThanOrEqual(5000);
+ });
+
+ it("should handle missing WebSocket connection", () => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (mockSocket as any).ws = null;
+
+ serviceCallMulti.start();
+
+ // Should trigger reopen and schedule with exponential backoff
+ expect(mockSocket.reopen).toHaveBeenCalled();
+
+ // Same calculation as above - base delay 4000ms + random up to 1000ms
+ const callArgs = mockSetTimeout.mock.calls[0];
+ expect(callArgs[0]).toEqual(expect.any(Function));
+ expect(callArgs[1]).toBeGreaterThanOrEqual(4000);
+ expect(callArgs[1]).toBeLessThanOrEqual(5000);
+ });
+
+ it("should not process response if already complete", () => {
+ const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
+
+ serviceCallMulti.complete = true;
+ serviceCallMulti.onReceived({ result: "test" });
+
+ expect(consoleSpy).toHaveBeenCalledWith(
+ "test-mid",
+ "should not happen, request is already complete",
+ );
+
+ consoleSpy.mockRestore();
+ });
+
+ it("should not timeout if already complete", () => {
+ const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
+
+ serviceCallMulti.complete = true;
+ serviceCallMulti.onTimeout();
+
+ expect(consoleSpy).toHaveBeenCalledWith(
+ "test-mid",
+ "timeout should not happen, request is already complete",
+ );
+
+ consoleSpy.mockRestore();
+ });
+
+ it("should not attempt if already complete", () => {
+ const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
+
+ serviceCallMulti.complete = true;
+ serviceCallMulti.attempt();
+
+ expect(consoleSpy).toHaveBeenCalledWith(
+ "test-mid",
+ "attempt should not be called, request is already complete",
+ );
+
+ consoleSpy.mockRestore();
+ });
+
+ it("should handle streaming responses correctly", () => {
+ mockReceiver
+ .mockReturnValueOnce(false) // First response - continue
+ .mockReturnValueOnce(false) // Second response - continue
+ .mockReturnValueOnce(true); // Third response - complete
+
+ serviceCallMulti.start();
+
+ // First response
+ serviceCallMulti.onReceived({ chunk: 1 });
+ expect(serviceCallMulti.complete).toBe(false);
+ expect(mockSuccess).not.toHaveBeenCalled();
+
+ // Second response
+ serviceCallMulti.onReceived({ chunk: 2 });
+ expect(serviceCallMulti.complete).toBe(false);
+ expect(mockSuccess).not.toHaveBeenCalled();
+
+ // Third response (final)
+ serviceCallMulti.onReceived({ chunk: 3, final: true });
+ expect(serviceCallMulti.complete).toBe(true);
+ expect(mockSuccess).toHaveBeenCalledWith({ chunk: 3, final: true });
+ });
+
+ it("should handle receiver function errors gracefully", () => {
+ mockReceiver.mockImplementation(() => {
+ throw new Error("Receiver error");
+ });
+
+ serviceCallMulti.start();
+
+ expect(() => {
+ serviceCallMulti.onReceived({ test: "data" });
+ }).toThrow("Receiver error");
+ });
+
+ it("should handle multiple timeout scenarios", () => {
+ const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
+
+ serviceCallMulti.start();
+
+ // After start, retries should be 2 (decremented from 3)
+ expect(serviceCallMulti.retries).toBe(2);
+
+ // First timeout
+ serviceCallMulti.onTimeout();
+ expect(serviceCallMulti.retries).toBe(1);
+
+ // Second timeout
+ serviceCallMulti.onTimeout();
+ expect(serviceCallMulti.retries).toBe(0);
+
+ consoleSpy.mockRestore();
+ });
+
+ it("should clean up properly when receiver signals completion", () => {
+ mockReceiver.mockReturnValue(true);
+
+ serviceCallMulti.start();
+
+ const response = { final: true };
+ serviceCallMulti.onReceived(response);
+
+ expect(serviceCallMulti.complete).toBe(true);
+ expect(mockClearTimeout).toHaveBeenCalled();
+ expect(mockSocket.inflight["test-mid"]).toBeUndefined();
+ expect(mockSuccess).toHaveBeenCalledWith(response);
+ });
+});
diff --git a/ai-context/trustgraph-client/src/__tests__/service-call.test.ts b/ai-context/trustgraph-client/src/__tests__/service-call.test.ts
new file mode 100644
index 00000000..acd72111
--- /dev/null
+++ b/ai-context/trustgraph-client/src/__tests__/service-call.test.ts
@@ -0,0 +1,239 @@
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { ServiceCall } from "../socket/service-call";
+
+// Mock WebSocket constants
+vi.stubGlobal("WebSocket", {
+ OPEN: 1,
+ CONNECTING: 0,
+ CLOSING: 2,
+ CLOSED: 3,
+});
+
+// Mock Socket interface
+const mockSocket = {
+ inflight: {} as Record,
+ ws: {
+ send: vi.fn(),
+ readyState: 1, // WebSocket.OPEN
+ },
+ reopen: vi.fn(),
+};
+
+// Mock setTimeout and clearTimeout
+const mockSetTimeout = vi.fn();
+const mockClearTimeout = vi.fn();
+
+vi.stubGlobal("setTimeout", mockSetTimeout);
+vi.stubGlobal("clearTimeout", mockClearTimeout);
+
+describe("ServiceCall", () => {
+ let mockSuccess: ReturnType;
+ let mockError: ReturnType;
+ let serviceCall: ServiceCall;
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockSuccess = vi.fn();
+ mockError = vi.fn();
+ mockSocket.inflight = {} as Record;
+ mockSocket.ws = {
+ send: vi.fn(),
+ readyState: 1, // WebSocket.OPEN
+ };
+ mockSocket.reopen.mockClear();
+
+ serviceCall = new ServiceCall(
+ "test-mid",
+ { id: "test-id", service: "test-service", request: { test: "data" } },
+ mockSuccess,
+ mockError,
+ 5000, // 5 second timeout
+ 3, // 3 retries
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ mockSocket as any,
+ );
+ });
+
+ it("should initialize with correct properties", () => {
+ expect(serviceCall.mid).toBe("test-mid");
+ expect(serviceCall.timeout).toBe(5000);
+ expect(serviceCall.retries).toBe(3);
+ expect(serviceCall.complete).toBe(false);
+ expect(serviceCall.socket).toBe(mockSocket);
+ });
+
+ it("should register itself in socket inflight when started", () => {
+ serviceCall.start();
+
+ expect(mockSocket.inflight["test-mid"]).toBe(serviceCall);
+ });
+
+ it("should send message on successful attempt", () => {
+ serviceCall.start();
+
+ expect(mockSocket.ws.send).toHaveBeenCalledWith(
+ JSON.stringify({
+ id: "test-id",
+ service: "test-service",
+ request: { test: "data" },
+ }),
+ );
+ expect(mockSetTimeout).toHaveBeenCalled();
+ });
+
+ it("should handle successful response", () => {
+ const responseData = { result: "success" };
+ const message = { response: responseData };
+
+ serviceCall.start();
+ serviceCall.onReceived(message);
+
+ expect(serviceCall.complete).toBe(true);
+ expect(mockSuccess).toHaveBeenCalledWith(responseData);
+ expect(mockClearTimeout).toHaveBeenCalled();
+ expect(mockSocket.inflight["test-mid"]).toBeUndefined();
+ });
+
+ it("should handle timeout and retry", () => {
+ serviceCall.start();
+
+ // Initial retries should be 3, but start() calls attempt() which decrements to 2
+ expect(serviceCall.retries).toBe(2);
+
+ // Simulate timeout
+ serviceCall.onTimeout();
+
+ expect(mockClearTimeout).toHaveBeenCalled();
+ expect(serviceCall.retries).toBe(1); // Should decrement from 2 to 1
+ });
+
+ it("should exhaust retries and call error callback", () => {
+ // Set retries to 0 to force immediate failure
+ serviceCall.retries = 0;
+
+ serviceCall.start();
+
+ expect(mockError).toHaveBeenCalledWith("Ran out of retries");
+ expect(mockSocket.inflight["test-mid"]).toBeUndefined();
+ });
+
+ it("should handle WebSocket send failure", () => {
+ mockSocket.ws.send.mockImplementation(() => {
+ throw new Error("Connection failed");
+ });
+
+ serviceCall.start();
+
+ // Should NOT call reopen anymore - BaseApi handles reconnection
+ expect(mockSocket.reopen).not.toHaveBeenCalled();
+
+ // With exponential backoff, the delay should be calculated as:
+ // SOCKET_RECONNECTION_TIMEOUT * Math.pow(2, 3 - retries) + random
+ // Since retries is decremented to 2 after start(), it's 3 - 2 = 1
+ // So base delay is 2000 * 2^1 = 4000, plus random up to 1000
+ // The delay should be between 4000 and 5000ms (capped at 30000)
+ const callArgs = mockSetTimeout.mock.calls[0];
+ expect(callArgs[0]).toEqual(expect.any(Function));
+ expect(callArgs[1]).toBeGreaterThanOrEqual(4000);
+ expect(callArgs[1]).toBeLessThanOrEqual(5000);
+ });
+
+ it("should handle missing WebSocket connection", () => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (mockSocket as any).ws = null;
+
+ serviceCall.start();
+
+ // Should NOT trigger reopen - just wait for BaseApi to reconnect
+ expect(mockSocket.reopen).not.toHaveBeenCalled();
+
+ // Same calculation as above - base delay 4000ms + random up to 1000ms
+ const callArgs = mockSetTimeout.mock.calls[0];
+ expect(callArgs[0]).toEqual(expect.any(Function));
+ expect(callArgs[1]).toBeGreaterThanOrEqual(4000);
+ expect(callArgs[1]).toBeLessThanOrEqual(5000);
+ });
+
+ it("should not process response if already complete", () => {
+ const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
+
+ serviceCall.complete = true;
+ serviceCall.onReceived({ result: "test" });
+
+ expect(consoleSpy).toHaveBeenCalledWith(
+ "test-mid",
+ "should not happen, request is already complete",
+ );
+
+ consoleSpy.mockRestore();
+ });
+
+ it("should not timeout if already complete", () => {
+ const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
+
+ serviceCall.complete = true;
+ serviceCall.onTimeout();
+
+ expect(consoleSpy).toHaveBeenCalledWith(
+ "test-mid",
+ "timeout should not happen, request is already complete",
+ );
+
+ consoleSpy.mockRestore();
+ });
+
+ it("should not attempt if already complete", () => {
+ const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
+
+ serviceCall.complete = true;
+ serviceCall.attempt();
+
+ expect(consoleSpy).toHaveBeenCalledWith(
+ "test-mid",
+ "attempt should not be called, request is already complete",
+ );
+
+ consoleSpy.mockRestore();
+ });
+
+ it("should handle multiple retries correctly", () => {
+ mockSocket.ws.send.mockImplementation(() => {
+ throw new Error("Connection failed");
+ });
+
+ serviceCall.start();
+
+ // Should have decremented retries and scheduled a retry
+ expect(serviceCall.retries).toBe(2);
+ // Should NOT call reopen - BaseApi handles reconnection
+ expect(mockSocket.reopen).not.toHaveBeenCalled();
+ });
+
+ it("should clean up properly on successful response", () => {
+ serviceCall.start();
+
+ const responseData = { success: true };
+ const message = { response: responseData };
+ serviceCall.onReceived(message);
+
+ expect(serviceCall.complete).toBe(true);
+ expect(mockClearTimeout).toHaveBeenCalled();
+ expect(mockSocket.inflight["test-mid"]).toBeUndefined();
+ expect(mockSuccess).toHaveBeenCalledWith(responseData);
+ });
+
+ it("should handle edge case of negative retries", () => {
+ serviceCall.retries = -1;
+
+ serviceCall.attempt();
+
+ expect(mockError).toHaveBeenCalledWith("Ran out of retries");
+ });
+
+ it("should bind timeout callbacks correctly", () => {
+ serviceCall.start();
+
+ // Verify that setTimeout was called with a bound function
+ expect(mockSetTimeout).toHaveBeenCalledWith(expect.any(Function), 5000);
+ });
+});
diff --git a/ai-context/trustgraph-client/src/index.ts b/ai-context/trustgraph-client/src/index.ts
new file mode 100644
index 00000000..c7b5f4b2
--- /dev/null
+++ b/ai-context/trustgraph-client/src/index.ts
@@ -0,0 +1,10 @@
+// @trustgraph/client
+// TrustGraph TypeScript Client
+
+// Export models (data types)
+export * from "./models/Triple";
+export * from "./models/messages";
+export * from "./models/namespaces";
+
+// Export socket client
+export * from "./socket/trustgraph-socket";
diff --git a/ai-context/trustgraph-client/src/models/Triple.ts b/ai-context/trustgraph-client/src/models/Triple.ts
new file mode 100644
index 00000000..c9d7ca4c
--- /dev/null
+++ b/ai-context/trustgraph-client/src/models/Triple.ts
@@ -0,0 +1,40 @@
+// Term type discriminators matching the wire format
+// i = IRI, b = BLANK node, l = LITERAL, t = TRIPLE (reified)
+export type TermType = "i" | "b" | "l" | "t";
+
+export interface IriTerm {
+ t: "i";
+ i: string;
+}
+
+export interface BlankTerm {
+ t: "b";
+ d: string;
+}
+
+export interface LiteralTerm {
+ t: "l";
+ v: string;
+ dt?: string; // datatype
+ ln?: string; // language
+}
+
+export interface TripleTerm {
+ t: "t";
+ tr?: Triple;
+}
+
+export type Term = IriTerm | BlankTerm | LiteralTerm | TripleTerm;
+
+export interface PartialTriple {
+ s?: Term;
+ p?: Term;
+ o?: Term;
+}
+
+export interface Triple {
+ s: Term;
+ p: Term;
+ o: Term;
+ g?: string; // graph (renamed from direc to match backend)
+}
diff --git a/ai-context/trustgraph-client/src/models/messages.ts b/ai-context/trustgraph-client/src/models/messages.ts
new file mode 100644
index 00000000..26198521
--- /dev/null
+++ b/ai-context/trustgraph-client/src/models/messages.ts
@@ -0,0 +1,496 @@
+import { Triple, Term } from "./Triple";
+
+// FIXME: Better types?
+export type Request = object;
+export type Response = object;
+export type Error = object | string;
+
+export interface ResponseError {
+ type?: string;
+ message: string;
+}
+
+export interface RequestMessage {
+ id: string;
+ service: string;
+ request: Request;
+ flow?: string;
+}
+
+export interface ApiResponse {
+ id: string;
+ response: Response;
+}
+
+export interface Metadata {
+ id?: string;
+ metadata?: Triple[];
+ user?: string;
+ collection?: string;
+}
+
+export interface EntityEmbeddings {
+ entity?: Term;
+ vectors?: number[][];
+}
+
+export interface GraphEmbeddings {
+ metadata?: Metadata;
+ entities?: EntityEmbeddings[];
+}
+
+export interface TextCompletionRequest {
+ system: string;
+ prompt: string;
+ streaming?: boolean;
+}
+
+export interface TextCompletionResponse {
+ response: string;
+ // Streaming fields
+ end_of_stream?: boolean;
+ error?: {
+ message: string;
+ type?: string;
+ };
+ // Token usage (appears in final message)
+ in_token?: number;
+ out_token?: number;
+ model?: string;
+}
+
+export interface GraphRagRequest {
+ query: string;
+ user?: string;
+ collection?: string;
+ "entity-limit"?: number; // Default: 50
+ "triple-limit"?: number; // Default: 30
+ "max-subgraph-size"?: number; // Default: 1000
+ "max-path-length"?: number; // Default: 2
+ streaming?: boolean;
+}
+
+export interface GraphRagResponse {
+ response: string;
+ // Streaming fields
+ chunk?: string;
+ end_of_stream?: boolean;
+ error?: {
+ message: string;
+ type?: string;
+ };
+ // Token usage (appears in final message)
+ in_token?: number;
+ out_token?: number;
+ model?: string;
+ // Explainability fields
+ message_type?: "chunk" | "explain";
+ explain_id?: string;
+ explain_graph?: string; // Named graph where explain data is stored (e.g., urn:graph:retrieval)
+ end_of_session?: boolean;
+}
+
+export interface DocumentRagRequest {
+ query: string;
+ user?: string;
+ collection?: string;
+ "doc-limit"?: number; // Default: 20
+ streaming?: boolean;
+}
+
+export interface DocumentRagResponse {
+ response: string;
+ // Streaming fields
+ chunk?: string;
+ end_of_stream?: boolean;
+ error?: {
+ message: string;
+ type?: string;
+ };
+ // Token usage (appears in final message)
+ in_token?: number;
+ out_token?: number;
+ model?: string;
+ // Explainability fields
+ message_type?: "chunk" | "explain";
+ explain_id?: string;
+ explain_graph?: string;
+ end_of_session?: boolean;
+}
+
+export interface AgentRequest {
+ question: string;
+ user?: string;
+ streaming?: boolean;
+}
+
+export interface AgentResponse {
+ // Streaming response format (new protocol)
+ chunk_type?: "thought" | "action" | "observation" | "answer" | "final-answer" | "explain" | "error";
+ content?: string;
+ end_of_message?: boolean;
+ end_of_dialog?: boolean;
+
+ // Legacy fields for backward compatibility with non-streaming
+ thought?: string;
+ observation?: string;
+ answer?: string;
+ error?: ResponseError;
+
+ // Token usage (appears in final message)
+ in_token?: number;
+ out_token?: number;
+ model?: string;
+
+ // Explainability fields
+ message_type?: "chunk" | "explain";
+ explain_id?: string;
+ explain_graph?: string;
+}
+
+export interface EmbeddingsRequest {
+ texts: string[];
+}
+
+export interface EmbeddingsResponse {
+ vectors: number[][]; // One vector per input text
+}
+
+export interface GraphEmbeddingsQueryRequest {
+ vector: number[]; // Single query vector
+ limit: number;
+ user?: string;
+ collection?: string;
+}
+
+export interface EntityMatch {
+ entity: Term | null;
+ score: number;
+}
+
+export interface GraphEmbeddingsQueryResponse {
+ entities: EntityMatch[];
+}
+
+export interface TriplesQueryRequest {
+ s?: Term;
+ p?: Term;
+ o?: Term;
+ g?: string; // Named graph URI filter (plain string, not Term)
+ limit: number;
+ user?: string;
+ collection?: string;
+}
+
+export interface TriplesQueryResponse {
+ response: Triple[];
+}
+
+export interface RowsQueryRequest {
+ query: string;
+ user?: string;
+ collection?: string;
+ variables?: Record;
+ operation_name?: string;
+}
+
+export interface RowsQueryResponse {
+ data?: Record;
+ errors?: Record[];
+ extensions?: Record;
+ values?: unknown[];
+}
+
+export interface NlpQueryRequest {
+ question: string;
+ max_results?: number;
+}
+
+export interface NlpQueryResponse {
+ graphql_query?: string;
+ variables?: Record;
+ detected_schemas?: Record[];
+ confidence?: number;
+}
+
+export interface StructuredQueryRequest {
+ question: string;
+ user?: string;
+ collection?: string;
+}
+
+export interface StructuredQueryResponse {
+ data?: Record;
+ errors?: Record[];
+}
+
+export interface RowEmbeddingsQueryRequest {
+ vector: number[]; // Single query vector
+ schema_name: string;
+ user?: string;
+ collection?: string;
+ index_name?: string;
+ limit?: number;
+}
+
+export interface RowEmbeddingsMatch {
+ index_name: string;
+ index_value: string[];
+ text: string;
+ score: number;
+}
+
+export interface RowEmbeddingsQueryResponse {
+ matches?: RowEmbeddingsMatch[];
+ error?: {
+ message: string;
+ type?: string;
+ };
+}
+
+export interface LoadDocumentRequest {
+ id?: string;
+ data: string;
+ metadata?: Triple[];
+}
+
+export type LoadDocumentResponse = void;
+
+export interface LoadTextRequest {
+ id?: string;
+ text: string;
+ charset?: string;
+ metadata?: Triple[];
+}
+
+export type LoadTextResponse = void;
+
+export interface DocumentMetadata {
+ id?: string;
+ time?: number;
+ kind?: string;
+ title?: string;
+ comments?: string;
+ metadata?: Triple[];
+ user?: string;
+ tags?: string[];
+ "document-type"?: string;
+}
+
+export interface ProcessingMetadata {
+ id?: string;
+ "document-id"?: string;
+ time?: number;
+ flow?: string;
+ user?: string;
+ collection?: string;
+ tags?: string[];
+}
+
+export interface LibraryRequest {
+ operation: string;
+ "document-id"?: string;
+ "processing-id"?: string;
+ "document-metadata"?: DocumentMetadata;
+ "processing-metadata"?: ProcessingMetadata;
+ content?: string;
+ user?: string;
+ collection?: string;
+ metadata?: Triple[];
+ id?: string;
+ flow?: string;
+}
+
+export interface LibraryResponse {
+ error: Error;
+ "document-metadata"?: DocumentMetadata;
+ content?: string;
+ "document-metadatas"?: DocumentMetadata[];
+ "processing-metadata"?: ProcessingMetadata;
+}
+
+export interface KnowledgeRequest {
+ operation: string;
+ user?: string;
+ id?: string;
+ flow?: string;
+ collection?: string;
+ triples?: Triple[];
+ "graph-embeddings"?: GraphEmbeddings;
+}
+
+export interface KnowledgeResponse {
+ error?: Error;
+ ids?: string[];
+ eos?: boolean;
+ triples?: Triple[];
+ "graph-embeddings"?: GraphEmbeddings;
+}
+
+export interface FlowRequest {
+ operation: string;
+ "blueprint-name"?: string;
+ "blueprint-definition"?: string;
+ description?: string;
+ "flow-id"?: string;
+ parameters?: Record;
+ user?: string;
+}
+
+export interface FlowResponse {
+ "blueprint-names"?: string[];
+ "flow-ids"?: string[];
+ ids?: string[];
+ flow?: string;
+ "blueprint-definition"?: string;
+ description?: string;
+ error?:
+ | {
+ message?: string;
+ }
+ | Error;
+}
+
+export interface PromptRequest {
+ id: string;
+ terms: Record;
+ streaming?: boolean;
+}
+
+export interface PromptResponse {
+ text: string;
+ // Streaming fields
+ end_of_stream?: boolean;
+ error?: {
+ message: string;
+ type?: string;
+ };
+ // Token usage (appears in final message)
+ in_token?: number;
+ out_token?: number;
+ model?: string;
+}
+
+export type ConfigRequest = object;
+export type ConfigResponse = object;
+
+// Chunked Upload Types
+
+export interface ChunkedUploadDocumentMetadata {
+ id: string;
+ time: number;
+ kind: string;
+ title: string;
+ comments?: string;
+ metadata?: Triple[];
+ user: string;
+ collection?: string;
+ tags?: string[];
+}
+
+export interface BeginUploadRequest {
+ operation: "begin-upload";
+ "document-metadata": ChunkedUploadDocumentMetadata;
+ "total-size": number;
+ "chunk-size"?: number;
+}
+
+export interface BeginUploadResponse {
+ "upload-id": string;
+ "chunk-size": number;
+ "total-chunks": number;
+ error?: ResponseError;
+}
+
+export interface UploadChunkRequest {
+ operation: "upload-chunk";
+ "upload-id": string;
+ "chunk-index": number;
+ content: string; // base64-encoded
+ user: string;
+}
+
+export interface UploadChunkResponse {
+ "upload-id": string;
+ "chunk-index": number;
+ "chunks-received": number;
+ "total-chunks": number;
+ "bytes-received": number;
+ "total-bytes": number;
+ error?: ResponseError;
+}
+
+export interface CompleteUploadRequest {
+ operation: "complete-upload";
+ "upload-id": string;
+ user: string;
+}
+
+export interface CompleteUploadResponse {
+ "document-id": string;
+ "object-id": string;
+ error?: ResponseError;
+}
+
+export interface GetUploadStatusRequest {
+ operation: "get-upload-status";
+ "upload-id": string;
+ user: string;
+}
+
+export interface GetUploadStatusResponse {
+ "upload-id": string;
+ "upload-state": "in-progress" | "completed" | "expired";
+ "chunks-received": number;
+ "total-chunks": number;
+ "received-chunks": number[];
+ "missing-chunks": number[];
+ "bytes-received": number;
+ "total-bytes": number;
+ error?: ResponseError;
+}
+
+export interface AbortUploadRequest {
+ operation: "abort-upload";
+ "upload-id": string;
+ user: string;
+}
+
+export interface AbortUploadResponse {
+ error?: ResponseError;
+}
+
+export interface ListUploadsRequest {
+ operation: "list-uploads";
+ user: string;
+}
+
+export interface UploadSession {
+ "upload-id": string;
+ "document-id": string;
+ "document-metadata-json": string;
+ "total-size": number;
+ "chunk-size": number;
+ "total-chunks": number;
+ "chunks-received": number;
+ "created-at": string;
+}
+
+export interface ListUploadsResponse {
+ "upload-sessions": UploadSession[];
+ error?: ResponseError;
+}
+
+export interface StreamDocumentRequest {
+ operation: "stream-document";
+ "document-id": string;
+ "chunk-size"?: number;
+ user: string;
+}
+
+export interface StreamDocumentResponse {
+ content: string; // base64-encoded chunk
+ "chunk-index": number;
+ "total-chunks": number;
+ error?: ResponseError;
+}
diff --git a/ai-context/trustgraph-client/src/models/namespaces.ts b/ai-context/trustgraph-client/src/models/namespaces.ts
new file mode 100644
index 00000000..df75fc04
--- /dev/null
+++ b/ai-context/trustgraph-client/src/models/namespaces.ts
@@ -0,0 +1,42 @@
+/**
+ * RDF namespace constants for TrustGraph
+ * Used for querying explainability data, provenance chains, and knowledge graph
+ */
+
+// TrustGraph namespace
+export const TG = "https://trustgraph.ai/ns/";
+export const TG_QUERY = TG + "query";
+export const TG_EDGE_COUNT = TG + "edgeCount";
+export const TG_SELECTED_EDGE = TG + "selectedEdge";
+export const TG_EDGE = TG + "edge";
+export const TG_REASONING = TG + "reasoning";
+export const TG_CONTENT = TG + "content";
+export const TG_REIFIES = TG + "reifies";
+export const TG_DOCUMENT = TG + "document";
+
+// W3C PROV-O namespace
+export const PROV = "http://www.w3.org/ns/prov#";
+export const PROV_STARTED_AT_TIME = PROV + "startedAtTime";
+export const PROV_WAS_DERIVED_FROM = PROV + "wasDerivedFrom";
+export const PROV_WAS_GENERATED_BY = PROV + "wasGeneratedBy";
+export const PROV_ACTIVITY = PROV + "Activity";
+export const PROV_ENTITY = PROV + "Entity";
+
+// RDFS namespace
+export const RDFS = "http://www.w3.org/2000/01/rdf-schema#";
+export const RDFS_LABEL = RDFS + "label";
+
+// RDF namespace
+export const RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+export const RDF_TYPE = RDF + "type";
+
+// Schema.org namespace (used in document metadata)
+export const SCHEMA = "https://schema.org/";
+export const SCHEMA_NAME = SCHEMA + "name";
+export const SCHEMA_DESCRIPTION = SCHEMA + "description";
+export const SCHEMA_AUTHOR = SCHEMA + "author";
+export const SCHEMA_KEYWORDS = SCHEMA + "keywords";
+
+// SKOS namespace
+export const SKOS = "http://www.w3.org/2004/02/skos/core#";
+export const SKOS_DEFINITION = SKOS + "definition";
diff --git a/ai-context/trustgraph-client/src/socket/service-call-multi.ts b/ai-context/trustgraph-client/src/socket/service-call-multi.ts
new file mode 100644
index 00000000..16e3e6ff
--- /dev/null
+++ b/ai-context/trustgraph-client/src/socket/service-call-multi.ts
@@ -0,0 +1,171 @@
+import { RequestMessage } from "../models/messages";
+
+// Constant defining the delay before attempting to reconnect a WebSocket
+// (2 seconds)
+export const SOCKET_RECONNECTION_TIMEOUT = 2000;
+
+// Forward declare Socket type to avoid circular dependency
+// Using a minimal interface that matches what BaseApi provides
+interface Socket {
+ ws?: WebSocket;
+ inflight: { [key: string]: ServiceCallMulti };
+ reopen: () => void;
+ getNextId?: () => string;
+ user?: string;
+}
+
+export class ServiceCallMulti {
+ constructor(
+ mid: string,
+ msg: RequestMessage,
+ success: (resp: unknown) => void,
+ error: (err: object | string) => void,
+ timeout: number,
+ retries: number,
+ socket: Socket,
+ receiver: (resp: unknown) => boolean,
+ ) {
+ this.mid = mid;
+ this.msg = msg;
+ this.success = success;
+ this.error = error;
+ this.timeout = timeout;
+ this.retries = retries;
+ this.socket = socket;
+ this.complete = false;
+ this.receiver = receiver;
+ }
+
+ mid: string;
+ msg: RequestMessage;
+ success: (resp: unknown) => void;
+ error: (err: object | string) => void;
+ receiver: (resp: unknown) => boolean;
+ timeoutId?: ReturnType;
+ timeout: number;
+ retries: number;
+ socket: Socket;
+ complete: boolean;
+
+ start() {
+ this.socket.inflight[this.mid] = this;
+ this.attempt();
+ }
+
+ onReceived(resp: object) {
+ if (this.complete == true)
+ console.log(this.mid, "should not happen, request is already complete");
+
+ const fin = this.receiver(resp);
+
+ if (fin) {
+ this.complete = true;
+
+ // console.log("Received for", this.mid);
+ clearTimeout(this.timeoutId);
+ this.timeoutId = undefined;
+ delete this.socket.inflight[this.mid];
+ this.success(resp);
+ }
+ }
+
+ /**
+ * Called when socket connects - immediately retry if we were waiting
+ */
+ retryNow() {
+ if (this.complete) return;
+
+ // Clear any pending backoff timer
+ clearTimeout(this.timeoutId);
+ this.timeoutId = undefined;
+
+ // Restore retry count since we didn't actually fail
+ this.retries++;
+
+ // Attempt immediately
+ this.attempt();
+ }
+
+ onTimeout() {
+ if (this.complete == true)
+ console.log(
+ this.mid,
+ "timeout should not happen, request is already complete",
+ );
+
+ console.log("Request", this.mid, "timed out");
+ clearTimeout(this.timeoutId);
+ this.attempt();
+ }
+
+ attempt() {
+ // console.log("attempt:", this.mid);
+
+ if (this.complete == true)
+ console.log(
+ this.mid,
+ "attempt should not be called, request is already complete",
+ );
+
+ this.retries--;
+
+ if (this.retries < 0) {
+ console.log("Request", this.mid, "ran out of retries");
+
+ clearTimeout(this.timeoutId);
+ delete this.socket.inflight[this.mid];
+
+ this.error("Ran out of retries");
+ return; // Exit early - no more attempts
+ }
+
+ // Check if WebSocket connection is available and ready
+ if (this.socket.ws && this.socket.ws.readyState === WebSocket.OPEN) {
+ try {
+ this.socket.ws.send(JSON.stringify(this.msg));
+ this.timeoutId = setTimeout(this.onTimeout.bind(this), this.timeout);
+
+ return;
+ } catch (e) {
+ console.log("Error:", e);
+ console.log("Message send failure, retry...");
+
+ // Calculate backoff delay with jitter
+ const backoffDelay = Math.min(
+ SOCKET_RECONNECTION_TIMEOUT * Math.pow(2, 3 - this.retries) +
+ Math.random() * 1000,
+ 30000, // Max 30 seconds
+ );
+
+ this.timeoutId = setTimeout(this.attempt.bind(this), backoffDelay);
+
+ console.log("Reopen...");
+ // Attempt to reopen the WebSocket connection
+ this.socket.reopen();
+ }
+ } else {
+ // No WebSocket connection available or not ready
+ // Check if socket is connecting
+ if (
+ this.socket.ws &&
+ this.socket.ws.readyState === WebSocket.CONNECTING
+ ) {
+ // Wait a bit longer for connection to establish
+ setTimeout(this.attempt.bind(this), 500);
+ } else {
+ // Socket is closed or closing, trigger reopen
+ console.log("Socket not ready, reopening...");
+ this.socket.reopen();
+
+ // Calculate backoff delay
+ const backoffDelay = Math.min(
+ SOCKET_RECONNECTION_TIMEOUT * Math.pow(2, 3 - this.retries) +
+ Math.random() * 1000,
+ 30000,
+ );
+
+ setTimeout(this.attempt.bind(this), backoffDelay);
+ }
+ }
+ }
+}
diff --git a/ai-context/trustgraph-client/src/socket/service-call.ts b/ai-context/trustgraph-client/src/socket/service-call.ts
new file mode 100644
index 00000000..4b5fe80b
--- /dev/null
+++ b/ai-context/trustgraph-client/src/socket/service-call.ts
@@ -0,0 +1,239 @@
+import { RequestMessage } from "../models/messages";
+
+// Constant defining the delay before attempting to reconnect a WebSocket
+// (2 seconds)
+export const SOCKET_RECONNECTION_TIMEOUT = 2000;
+
+// Forward declare Socket type to avoid circular dependency
+// Using a minimal interface that matches what BaseApi provides
+interface Socket {
+ ws?: WebSocket;
+ inflight: { [key: string]: ServiceCall };
+ reopen: () => void;
+ getNextId?: () => string;
+ user?: string;
+}
+
+/**
+ * ServiceCall represents a single request/response cycle over a WebSocket
+ * connection with built-in retry logic, timeout handling, and completion
+ * tracking.
+ *
+ * This class manages the lifecycle of a service call including:
+ * - Sending the initial request
+ * - Handling timeouts and retries
+ * - Managing completion state
+ * - Cleaning up resources
+ */
+export class ServiceCall {
+ constructor(
+ mid: string, // Message ID - unique identifier for this request
+ msg: RequestMessage, // The actual message/request to send
+ success: (resp: unknown) => void, // Callback function called on
+ // successful response
+ error: (err: object | string) => void, // Callback function called on error/failure
+ timeout: number, // Timeout duration in milliseconds
+ retries: number, // Number of retry attempts allowed
+ socket: Socket, // WebSocket instance to send the message through
+ ) {
+ this.mid = mid;
+ this.msg = msg;
+ this.success = success;
+ this.error = error;
+ this.timeout = timeout;
+ this.retries = retries;
+ this.socket = socket;
+ this.complete = false; // Track if this request has completed
+ }
+
+ // Properties
+ mid: string; // Message identifier
+ msg: RequestMessage; // The request message
+ success: (resp: unknown) => void; // Success callback
+ error: (err: object | string) => void; // Error callback
+ timeoutId?: ReturnType; // Reference to the active timeout timer
+ timeout: number; // Timeout duration in milliseconds
+ retries: number; // Remaining retry attempts
+ socket: Socket; // WebSocket connection reference
+ complete: boolean; // Flag indicating if request is complete
+
+ /**
+ * Initiates the service call by registering it with the socket's inflight
+ * requests and making the first attempt to send the message
+ */
+ start() {
+ // Register this request as "in-flight" so responses can be matched to it
+ this.socket.inflight[this.mid] = this;
+ // Make the first attempt to send the message
+ this.attempt();
+ }
+
+ /**
+ * Called when a response is received for this request
+ * Handles cleanup and calls the success or error callback based on response
+ *
+ * @param resp - The response object received from the server
+ */
+ onReceived(resp: object) {
+ // Defensive check - this shouldn't happen but log if it does
+ if (this.complete == true)
+ console.log(this.mid, "should not happen, request is already complete");
+
+ // Mark as complete to prevent duplicate processing
+ this.complete = true;
+
+ // Clean up timeout timer
+ clearTimeout(this.timeoutId);
+ this.timeoutId = undefined;
+
+ // Remove from inflight requests tracker
+ delete this.socket.inflight[this.mid];
+
+ // Check if the response contains an error (error can be directly in resp or nested under response)
+ let errorToHandle: unknown = null;
+
+ // Check for direct error in response
+ if (resp && typeof resp === "object" && "error" in resp) {
+ errorToHandle = (resp as Record).error;
+ }
+ // Check for nested error under response property
+ else if (resp && typeof resp === "object" && "response" in resp) {
+ const response = (resp as Record).response;
+ if (response && typeof response === "object" && "error" in response) {
+ errorToHandle = (response as Record).error;
+ }
+ }
+
+ if (errorToHandle) {
+ // Response contains an error - call error callback
+ const errorObj = errorToHandle as Record;
+ const errorMessage =
+ (typeof errorObj.message === "string" ? errorObj.message : null) ||
+ (typeof errorObj.type === "string" ? errorObj.type : null) ||
+ "Unknown error";
+ console.log(
+ "ServiceCall: API error detected in response:",
+ errorMessage,
+ "Full error:",
+ errorToHandle,
+ );
+ this.error(new Error(errorMessage));
+ return;
+ }
+
+ // Extract the response field from the message object
+ // The resp parameter is the full message: {id, response, complete}
+ // We need to pass just the response field to the success callback
+ const responseData = (resp as { response?: unknown }).response;
+ this.success(responseData);
+ }
+
+ /**
+ * Called when socket connects - immediately retry if we were waiting
+ */
+ retryNow() {
+ if (this.complete) return;
+
+ // Clear any pending backoff timer
+ clearTimeout(this.timeoutId);
+ this.timeoutId = undefined;
+
+ // Restore retry count since we didn't actually fail
+ this.retries++;
+
+ // Attempt immediately
+ this.attempt();
+ }
+
+ /**
+ * Called when the request times out
+ * Triggers another attempt if retries are available
+ */
+ onTimeout() {
+ // Defensive check - this shouldn't happen but log if it does
+ if (this.complete == true)
+ console.log(
+ this.mid,
+ "timeout should not happen, request is already complete",
+ );
+
+ console.log("Request", this.mid, "timed out");
+
+ // Clear the current timeout
+ clearTimeout(this.timeoutId);
+
+ // Try again (this will check retry count)
+ this.attempt();
+ }
+
+ /**
+ * Calculates exponential backoff delay with jitter
+ * @returns backoff delay in milliseconds
+ */
+ calculateBackoff() {
+ return Math.min(
+ SOCKET_RECONNECTION_TIMEOUT * Math.pow(2, 3 - this.retries) +
+ Math.random() * 1000,
+ 30000, // Max 30 seconds
+ );
+ }
+
+ /**
+ * Core retry logic - attempts to send the message over the WebSocket
+ * Handles retries and waits for BaseApi to handle reconnection
+ */
+ attempt() {
+ // Defensive check - this shouldn't be called on completed requests
+ if (this.complete == true)
+ console.log(
+ this.mid,
+ "attempt should not be called, request is already complete",
+ );
+
+ // Decrement retry counter
+ this.retries--;
+
+ // Check if we've exhausted all retries
+ if (this.retries < 0) {
+ console.log("Request", this.mid, "ran out of retries");
+
+ // Clean up and call error callback
+ clearTimeout(this.timeoutId);
+ delete this.socket.inflight[this.mid];
+ this.error("Ran out of retries");
+ return; // Exit early - no more attempts
+ }
+
+ // Check if WebSocket connection is available and ready
+ if (this.socket.ws && this.socket.ws.readyState === WebSocket.OPEN) {
+ try {
+ // Attempt to send the message as JSON
+ this.socket.ws.send(JSON.stringify(this.msg));
+
+ // Set up timeout for this attempt
+ this.timeoutId = setTimeout(this.onTimeout.bind(this), this.timeout);
+
+ return; // Success - message sent, waiting for response or timeout
+ } catch (e) {
+ // Handle send failure - wait for BaseApi to handle reconnection
+ console.log("Error:", e);
+ console.log(
+ "Message send failure, waiting for socket reconnection...",
+ );
+
+ // Schedule retry with backoff - let BaseApi handle the reconnection
+ this.timeoutId = setTimeout(
+ this.attempt.bind(this),
+ this.calculateBackoff(),
+ );
+ }
+ } else {
+ // No WebSocket connection available or not ready
+ // Let BaseApi handle reconnection, just wait and retry
+ console.log("Request", this.mid, "waiting for socket reconnection...");
+
+ // Use consistent backoff for all waiting scenarios
+ setTimeout(this.attempt.bind(this), this.calculateBackoff());
+ }
+ }
+}
diff --git a/ai-context/trustgraph-client/src/socket/trustgraph-socket.ts b/ai-context/trustgraph-client/src/socket/trustgraph-socket.ts
new file mode 100644
index 00000000..e3adf6b3
--- /dev/null
+++ b/ai-context/trustgraph-client/src/socket/trustgraph-socket.ts
@@ -0,0 +1,2353 @@
+// Import core types and classes for the TrustGraph API
+import { Triple, Term } from "../models/Triple";
+import { ServiceCallMulti } from "./service-call-multi";
+import { ServiceCall } from "./service-call";
+
+// Import all message types for different services
+import {
+ AgentRequest,
+ AgentResponse,
+ ConfigRequest,
+ ConfigResponse,
+ DocumentMetadata,
+ DocumentRagRequest,
+ DocumentRagResponse,
+ EmbeddingsRequest,
+ EmbeddingsResponse,
+ EntityMatch,
+ FlowRequest,
+ FlowResponse,
+ GraphEmbeddingsQueryRequest,
+ GraphEmbeddingsQueryResponse,
+ GraphRagRequest,
+ GraphRagResponse,
+ // KnowledgeRequest,
+ // KnowledgeResponse,
+ LibraryRequest,
+ LibraryResponse,
+ LoadDocumentRequest,
+ LoadDocumentResponse,
+ LoadTextRequest,
+ LoadTextResponse,
+ NlpQueryRequest,
+ NlpQueryResponse,
+ RowsQueryRequest,
+ RowsQueryResponse,
+ RowEmbeddingsQueryRequest,
+ RowEmbeddingsQueryResponse,
+ RowEmbeddingsMatch,
+ PromptRequest,
+ PromptResponse,
+ // ProcessingMetadata,
+ RequestMessage,
+ StructuredQueryRequest,
+ StructuredQueryResponse,
+ TextCompletionRequest,
+ TextCompletionResponse,
+ TriplesQueryRequest,
+ TriplesQueryResponse,
+ // Chunked upload types
+ ChunkedUploadDocumentMetadata,
+ BeginUploadRequest,
+ BeginUploadResponse,
+ UploadChunkRequest,
+ UploadChunkResponse,
+ CompleteUploadRequest,
+ CompleteUploadResponse,
+ GetUploadStatusRequest,
+ GetUploadStatusResponse,
+ AbortUploadRequest,
+ AbortUploadResponse,
+ ListUploadsRequest,
+ ListUploadsResponse,
+ UploadSession,
+ StreamDocumentRequest,
+ StreamDocumentResponse,
+ // EntityEmbeddings,
+ // Error,
+ // GraphEmbedding,
+ // Metadata,
+ // Request,
+ // Response,
+} from "../models/messages";
+
+// GraphRAG options interface for configurable parameters
+export interface GraphRagOptions {
+ entityLimit?: number;
+ tripleLimit?: number;
+ maxSubgraphSize?: number;
+ pathLength?: number;
+}
+
+// Metadata included in final streaming message
+export interface StreamingMetadata {
+ in_token?: number;
+ out_token?: number;
+ model?: string;
+}
+
+// Explainability event data
+export interface ExplainEvent {
+ explainId: string;
+ explainGraph: string; // Named graph where explain data is stored (e.g., urn:graph:retrieval)
+}
+
+// Configuration constants
+const SOCKET_RECONNECTION_TIMEOUT = 2000; // 2 seconds between reconnection
+// attempts
+const SOCKET_URL = "/api/socket"; // WebSocket endpoint path
+
+/**
+ * Socket interface defining all available operations for the TrustGraph API
+ * This provides a unified interface for various AI/ML and knowledge graph
+ * operations
+ */
+export interface Socket {
+ close: () => void;
+
+ // Text completion using AI models
+ textCompletion: (system: string, text: string) => Promise;
+
+ // Graph-based Retrieval Augmented Generation
+ graphRag: (text: string, options?: GraphRagOptions) => Promise;
+
+ // Agent interaction with streaming callbacks for different phases
+ // BREAKING CHANGE: Callbacks now receive (chunk, complete, metadata?) instead of full messages
+ agent: (
+ question: string,
+ think: (chunk: string, complete: boolean, metadata?: StreamingMetadata) => void,
+ observe: (chunk: string, complete: boolean, metadata?: StreamingMetadata) => void,
+ answer: (chunk: string, complete: boolean, metadata?: StreamingMetadata) => void,
+ error: (e: string) => void,
+ onExplain?: (event: ExplainEvent) => void,
+ ) => void;
+
+ // Streaming variants for RAG and completion services
+ graphRagStreaming: (
+ text: string,
+ receiver: (chunk: string, complete: boolean, metadata?: StreamingMetadata) => void,
+ onError: (error: string) => void,
+ options?: GraphRagOptions,
+ collection?: string,
+ ) => void;
+
+ documentRagStreaming: (
+ text: string,
+ receiver: (chunk: string, complete: boolean, metadata?: StreamingMetadata) => void,
+ onError: (error: string) => void,
+ docLimit?: number,
+ collection?: string,
+ onExplain?: (event: ExplainEvent) => void,
+ ) => void;
+
+ textCompletionStreaming: (
+ system: string,
+ text: string,
+ receiver: (chunk: string, complete: boolean, metadata?: StreamingMetadata) => void,
+ onError: (error: string) => void,
+ ) => void;
+
+ promptStreaming: (
+ id: string,
+ terms: Record,
+ receiver: (chunk: string, complete: boolean, metadata?: StreamingMetadata) => void,
+ onError: (error: string) => void,
+ ) => void;
+
+ // Generate embeddings for texts (batch)
+ embeddings: (texts: string[]) => Promise;
+
+ // Query graph using embedding vector
+ graphEmbeddingsQuery: (vec: number[], limit: number) => Promise;
+
+ // Query knowledge graph triples (subject-predicate-object)
+ triplesQuery: (
+ s?: Term, // Subject (optional)
+ p?: Term, // Predicate (optional)
+ o?: Term, // Object (optional)
+ limit?: number,
+ collection?: string,
+ graph?: string, // Named graph URI filter
+ ) => Promise;
+
+ // Load a document into the system
+ loadDocument: (
+ document: string, // Base64-encoded document
+ id?: string, // Optional document ID
+ metadata?: Triple[], // Optional metadata as triples
+ ) => Promise;
+
+ // Load plain text into the system
+ loadText: (text: string, id?: string, metadata?: Triple[]) => Promise;
+
+ // Load a document into the library with full metadata
+ loadLibraryDocument: (
+ document: string,
+ mimeType: string,
+ id?: string,
+ metadata?: Triple[],
+ ) => Promise;
+}
+
+/**
+ * Generates a random message ID using cryptographically secure random values
+ * @param length - Number of random characters to generate
+ * @returns Random string of specified length
+ */
+function makeid(length: number) {
+ const array = new Uint32Array(length);
+ crypto.getRandomValues(array);
+
+ const characters = "abcdefghijklmnopqrstuvwxyz1234567890";
+
+ return array.reduce(
+ (acc, current) => acc + characters[current % characters.length],
+ "",
+ );
+}
+
+/**
+ * BaseApi - Core WebSocket client for TrustGraph API
+ * Manages connection lifecycle, message routing, and provides base request
+ * functionality
+ */
+// Connection state interface for UI consumption
+export interface ConnectionState {
+ status:
+ | "connecting"
+ | "connected"
+ | "reconnecting"
+ | "failed"
+ | "authenticated"
+ | "unauthenticated";
+ hasApiKey: boolean;
+ reconnectAttempt?: number;
+ maxAttempts?: number;
+ nextRetryIn?: number;
+ lastError?: string;
+}
+
+export class BaseApi {
+ ws?: WebSocket; // WebSocket connection instance
+ tag: string; // Unique client identifier
+ id: number; // Counter for generating unique message IDs
+ token?: string; // Optional authentication token
+ user: string; // User identifier for API requests
+ socketUrl: string; // WebSocket URL
+ inflight: { [key: string]: ServiceCall } = {}; // Track active requests by
+ // message ID
+ reconnectAttempts: number = 0; // Track reconnection attempts
+ maxReconnectAttempts: number = 10; // Maximum reconnection attempts
+ reconnectTimer?: number; // Timer for reconnection attempts
+ reconnectionState: "idle" | "reconnecting" | "failed" = "idle"; // Connection state
+
+ // Connection state tracking for UI
+ private connectionStateListeners: ((state: ConnectionState) => void)[] = [];
+ private lastError?: string;
+
+ constructor(user: string, token?: string, socketUrl?: string) {
+ this.tag = makeid(16); // Generate unique client tag
+ this.id = 1; // Start message ID counter
+ this.token = token; // Store authentication token
+ this.user = user; // Store user identifier
+ this.socketUrl = socketUrl || SOCKET_URL; // Use provided URL or default
+
+ console.log(
+ "SOCKET: opening socket...",
+ token ? "with auth" : "without auth",
+ "user:",
+ user,
+ );
+ this.openSocket(); // Establish WebSocket connection
+ console.log("SOCKET: socket opened");
+ }
+
+ /**
+ * Subscribe to connection state changes for UI updates
+ */
+ onConnectionStateChange(listener: (state: ConnectionState) => void) {
+ this.connectionStateListeners.push(listener);
+ // Immediately send current state
+ listener(this.getConnectionState());
+
+ // Return unsubscribe function
+ return () => {
+ const index = this.connectionStateListeners.indexOf(listener);
+ if (index > -1) {
+ this.connectionStateListeners.splice(index, 1);
+ }
+ };
+ }
+
+ /**
+ * Get current connection state
+ */
+ private getConnectionState(): ConnectionState {
+ const hasApiKey = !!this.token;
+
+ // Determine status based on WebSocket state and reconnection state
+ let status: ConnectionState["status"];
+
+ if (!this.ws || this.ws.readyState === WebSocket.CLOSED) {
+ if (this.reconnectionState === "failed") {
+ status = "failed";
+ } else if (this.reconnectionState === "reconnecting") {
+ status = "reconnecting";
+ } else {
+ status = "connecting";
+ }
+ } else if (this.ws.readyState === WebSocket.CONNECTING) {
+ status = "connecting";
+ } else if (this.ws.readyState === WebSocket.OPEN) {
+ status = hasApiKey ? "authenticated" : "unauthenticated";
+ } else {
+ status = "connecting";
+ }
+
+ const state: ConnectionState = {
+ status,
+ hasApiKey,
+ lastError: this.lastError,
+ };
+
+ // Add reconnection details if applicable
+ if (status === "reconnecting") {
+ state.reconnectAttempt = this.reconnectAttempts;
+ state.maxAttempts = this.maxReconnectAttempts;
+ }
+
+ return state;
+ }
+
+ /**
+ * Notify all listeners of connection state changes
+ */
+ private notifyStateChange() {
+ const state = this.getConnectionState();
+ this.connectionStateListeners.forEach((listener) => {
+ try {
+ listener(state);
+ } catch (error) {
+ console.error("Error in connection state listener:", error);
+ }
+ });
+ }
+
+ /**
+ * Establishes WebSocket connection and sets up event handlers
+ */
+ openSocket() {
+ // Don't create multiple connections
+ if (
+ this.ws &&
+ (this.ws.readyState === WebSocket.CONNECTING ||
+ this.ws.readyState === WebSocket.OPEN)
+ ) {
+ return;
+ }
+
+ // Clean up old socket if exists
+ if (this.ws) {
+ this.ws.removeEventListener("message", this.onMessage);
+ this.ws.removeEventListener("close", this.onClose);
+ this.ws.removeEventListener("open", this.onOpen);
+ this.ws.removeEventListener("error", this.onError);
+ this.ws = undefined;
+ }
+
+ try {
+ // Build WebSocket URL with optional token parameter
+ const wsUrl = this.token
+ ? `${this.socketUrl}?token=${this.token}`
+ : this.socketUrl;
+ console.log(
+ "SOCKET: connecting to",
+ wsUrl.replace(/token=[^&]*/, "token=***"),
+ );
+ this.ws = new WebSocket(wsUrl);
+ } catch (e) {
+ console.error("[socket creation error]", e);
+ this.scheduleReconnect();
+ return;
+ }
+
+ // Bind event handlers to maintain proper 'this' context
+ this.onMessage = this.onMessage.bind(this);
+ this.onClose = this.onClose.bind(this);
+ this.onOpen = this.onOpen.bind(this);
+ this.onError = this.onError.bind(this);
+
+ // Attach event listeners
+ this.ws.addEventListener("message", this.onMessage);
+ this.ws.addEventListener("close", this.onClose);
+ this.ws.addEventListener("open", this.onOpen);
+ this.ws.addEventListener("error", this.onError);
+ }
+
+ // Handle incoming messages from server
+ onMessage(message: MessageEvent) {
+ if (!message.data) return;
+
+ try {
+ const obj = JSON.parse(message.data);
+
+ // Skip messages without ID (can't route them)
+ if (!obj.id) return;
+
+ // Route response to the corresponding inflight request
+ if (this.inflight[obj.id]) {
+ // Pass the whole message object so receiver can access 'complete' flag
+ this.inflight[obj.id].onReceived(obj);
+ }
+ } catch (e) {
+ console.error("[socket message parse error]", e);
+ }
+ }
+
+ // Handle connection closure - automatically attempt reconnection
+ onClose(event: CloseEvent) {
+ console.log("[socket close]", event.code, event.reason);
+ this.lastError = `Connection closed: ${event.reason || "Unknown reason"}`;
+ this.ws = undefined;
+ this.notifyStateChange();
+ this.scheduleReconnect();
+ }
+
+ // Handle successful connection
+ onOpen() {
+ console.log("[socket open]");
+ this.reconnectAttempts = 0; // Reset reconnection attempts on success
+ this.reconnectionState = "idle"; // Reset connection state
+ this.lastError = undefined; // Clear any previous errors
+
+ // Clear any pending reconnect timer
+ if (this.reconnectTimer) {
+ clearTimeout(this.reconnectTimer);
+ this.reconnectTimer = undefined;
+ }
+
+ // Notify UI of successful connection
+ this.notifyStateChange();
+
+ // Immediately retry any pending requests that were waiting for connection
+ for (const mid in this.inflight) {
+ this.inflight[mid].retryNow();
+ }
+ }
+
+ // Handle socket errors
+ onError(event: Event) {
+ console.error("[socket error]", event);
+ this.lastError = "Connection error occurred";
+ this.notifyStateChange();
+ }
+
+ /**
+ * Schedules a reconnection attempt with exponential backoff
+ */
+ scheduleReconnect() {
+ // Prevent concurrent reconnection attempts
+ if (this.reconnectionState === "reconnecting") {
+ console.log("[socket] Reconnection already in progress, skipping");
+ return;
+ }
+
+ // Don't schedule if already scheduled
+ if (this.reconnectTimer) return;
+
+ this.reconnectionState = "reconnecting";
+ this.reconnectAttempts++;
+ this.notifyStateChange(); // Notify UI of reconnection attempt
+
+ if (this.reconnectAttempts > this.maxReconnectAttempts) {
+ console.error("[socket] Max reconnection attempts reached");
+ this.reconnectionState = "failed";
+ this.lastError = "Max reconnection attempts exceeded";
+ this.notifyStateChange();
+ // Notify all pending requests of the failure
+ for (const mid in this.inflight) {
+ this.inflight[mid].error(new Error("WebSocket connection failed"));
+ }
+ return;
+ }
+
+ // Calculate exponential backoff with jitter
+ const backoffDelay = Math.min(
+ SOCKET_RECONNECTION_TIMEOUT * Math.pow(2, this.reconnectAttempts - 1) +
+ Math.random() * 1000,
+ 30000, // Max 30 seconds
+ );
+
+ console.log(
+ `[socket] Reconnecting in ${backoffDelay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`,
+ );
+
+ this.reconnectTimer = setTimeout(() => {
+ this.reconnectTimer = undefined;
+ this.reopen();
+ }, backoffDelay) as unknown as number;
+ }
+
+ /**
+ * Reopens the WebSocket connection (used after connection failures)
+ */
+ reopen() {
+ console.log("[socket reopen]");
+ // Check if we're already connected or connecting
+ if (
+ this.ws &&
+ (this.ws.readyState === WebSocket.OPEN ||
+ this.ws.readyState === WebSocket.CONNECTING)
+ ) {
+ return;
+ }
+ this.openSocket();
+ }
+
+ /**
+ * Closes the WebSocket connection and cleans up
+ */
+ close() {
+ // Clear reconnection timer
+ if (this.reconnectTimer) {
+ clearTimeout(this.reconnectTimer);
+ this.reconnectTimer = undefined;
+ }
+
+ // Clean up WebSocket
+ if (this.ws) {
+ // Remove event listeners to prevent memory leaks
+ this.ws.removeEventListener("message", this.onMessage);
+ this.ws.removeEventListener("close", this.onClose);
+ this.ws.removeEventListener("open", this.onOpen);
+ this.ws.removeEventListener("error", this.onError);
+
+ this.ws.close();
+ this.ws = undefined;
+ }
+
+ // Clear any remaining inflight requests
+ for (const mid in this.inflight) {
+ this.inflight[mid].error(new Error("Socket closed"));
+ }
+ this.inflight = {};
+ }
+
+ /**
+ * Generates the next unique message ID for requests
+ * Format: {clientTag}-{incrementingNumber}
+ */
+ getNextId() {
+ const mid = this.tag + "-" + this.id.toString();
+ this.id++;
+ return mid;
+ }
+
+ /**
+ * Core method for making service requests over WebSocket
+ * @param service - Name of the service to call
+ * @param request - Request payload
+ * @param timeout - Request timeout in milliseconds (default: 10000)
+ * @param retries - Number of retry attempts (default: 3)
+ * @param flow - Optional flow identifier
+ * @returns Promise resolving to the service response
+ */
+ makeRequest(
+ service: string,
+ request: RequestType,
+ timeout?: number,
+ retries?: number,
+ flow?: string,
+ ) {
+ const mid = this.getNextId();
+
+ // Set default values
+ if (timeout == undefined) timeout = 10000;
+ if (retries == undefined) retries = 3;
+
+ // Construct the request message
+ const msg: RequestMessage = {
+ id: mid,
+ service: service,
+ request: request,
+ };
+
+ // Add flow identifier if provided
+ if (flow) msg.flow = flow;
+
+ // Return a Promise that will be resolved/rejected by the ServiceCall
+ return new Promise((resolve, reject) => {
+ const call = new ServiceCall(
+ mid,
+ msg,
+ resolve as (resp: unknown) => void,
+ reject as (err: object | string) => void,
+ timeout,
+ retries,
+ this,
+ );
+
+ call.start();
+ // Commented out debug logging: console.log("-->", msg);
+ }).then((obj) => {
+ // Commented out success logging: console.log("Success for", mid);
+ return obj as ResponseType;
+ });
+ }
+
+ /**
+ * Makes a request that can receive multiple responses (streaming)
+ * Used for operations that return data in chunks
+ */
+ makeRequestMulti(
+ service: string,
+ request: RequestType,
+ receiver: (resp: unknown) => boolean, // Callback to handle each response chunk
+ timeout?: number,
+ retries?: number,
+ flow?: string,
+ ) {
+ const mid = this.getNextId();
+
+ // Set defaults
+ if (timeout == undefined) timeout = 10000;
+ if (retries == undefined) retries = 3;
+
+ // Construct request message
+ const msg: RequestMessage = {
+ id: mid,
+ service: service,
+ request: request,
+ };
+
+ if (flow) msg.flow = flow;
+
+ return new Promise((resolve, reject) => {
+ const call = new ServiceCallMulti(
+ mid,
+ msg,
+ resolve as (resp: unknown) => void,
+ reject as (err: object | string) => void,
+ timeout,
+ retries,
+ this as any, // eslint-disable-line @typescript-eslint/no-explicit-any
+ receiver,
+ );
+
+ call.start();
+ }).then((obj) => {
+ return obj as ResponseType;
+ });
+ }
+
+ /**
+ * Convenience method for making flow-specific requests
+ * Defaults to "default" flow if none specified
+ */
+ makeFlowRequest(
+ service: string,
+ request: RequestType,
+ timeout?: number,
+ retries?: number,
+ flow?: string,
+ ) {
+ if (!flow) flow = "default";
+
+ return this.makeRequest(
+ service,
+ request,
+ timeout,
+ retries,
+ flow,
+ );
+ }
+
+ // Factory methods for creating specialized API instances
+ librarian() {
+ return new LibrarianApi(this);
+ }
+
+ flows() {
+ return new FlowsApi(this);
+ }
+
+ flow(id: string) {
+ return new FlowApi(this, id);
+ }
+
+ knowledge() {
+ return new KnowledgeApi(this);
+ }
+
+ config() {
+ return new ConfigApi(this);
+ }
+
+ collectionManagement() {
+ return new CollectionManagementApi(this);
+ }
+}
+
+/**
+ * LibrarianApi - Manages document storage and retrieval
+ * Handles document lifecycle including upload, processing, and removal
+ */
+export class LibrarianApi {
+ api: BaseApi;
+
+ constructor(api: BaseApi) {
+ this.api = api;
+ }
+
+ /**
+ * Retrieves list of all documents in the system
+ */
+ getDocuments() {
+ return this.api
+ .makeRequest(
+ "librarian",
+ {
+ operation: "list-documents",
+ user: this.api.user,
+ },
+ 60000, // 60 second timeout for potentially large lists
+ )
+ .then((r) => r["document-metadatas"] || []);
+ }
+
+ /**
+ * Retrieves list of documents currently being processed
+ */
+ getProcessing() {
+ return this.api
+ .makeRequest(
+ "librarian",
+ {
+ operation: "list-processing",
+ user: this.api.user,
+ },
+ 60000,
+ )
+ .then((r) => r["processing-metadata"] || []);
+ }
+
+ /**
+ * Retrieves metadata for a single document by ID
+ * @param documentId - Document URI/ID to fetch
+ * @returns Document metadata including title, comments, tags, and RDF metadata
+ */
+ getDocumentMetadata(documentId: string): Promise {
+ return this.api
+ .makeRequest(
+ "librarian",
+ {
+ operation: "get-document-metadata",
+ "document-id": documentId,
+ user: this.api.user,
+ },
+ 30000,
+ )
+ .then((r) => r["document-metadata"] || null);
+ }
+
+ /**
+ * Uploads a document to the library with full metadata
+ * @param document - Base64-encoded document content
+ * @param id - Optional document identifier
+ * @param metadata - Optional metadata as triples
+ * @param mimeType - Document MIME type
+ * @param title - Document title
+ * @param comments - Additional comments
+ * @param tags - Document tags for categorization
+ */
+ loadDocument(
+ document: string, // base64-encoded doc
+ mimeType: string,
+ title: string,
+ comments: string,
+ tags: string[],
+ id?: string,
+ metadata?: Triple[],
+ ) {
+ return this.api.makeRequest(
+ "librarian",
+ {
+ operation: "add-document",
+ "document-metadata": {
+ id: id,
+ time: Math.floor(Date.now() / 1000), // Unix timestamp
+ kind: mimeType,
+ title: title,
+ comments: comments,
+ metadata: metadata,
+ user: this.api.user,
+ tags: tags,
+ },
+ content: document,
+ },
+ 30000, // 30 second timeout for document upload
+ );
+ }
+
+ /**
+ * Removes a document from the library
+ */
+ removeDocument(id: string, collection?: string) {
+ return this.api.makeRequest(
+ "librarian",
+ {
+ operation: "remove-document",
+ "document-id": id,
+ user: this.api.user,
+ collection: collection || "default",
+ },
+ 30000,
+ );
+ }
+
+ /**
+ * Adds a document to the processing queue
+ * @param id - Processing job identifier
+ * @param doc_id - Document to process
+ * @param flow - Processing flow to use
+ * @param collection - Collection to add processed data to
+ * @param tags - Tags for the processing job
+ */
+ addProcessing(
+ id: string,
+ doc_id: string,
+ flow: string,
+ collection?: string,
+ tags?: string[],
+ ) {
+ return this.api.makeRequest(
+ "librarian",
+ {
+ operation: "add-processing",
+ "processing-metadata": {
+ id: id,
+ "document-id": doc_id,
+ time: Math.floor(Date.now() / 1000),
+ flow: flow,
+ user: this.api.user,
+ collection: collection ? collection : "default",
+ tags: tags ? tags : [],
+ },
+ },
+ 30000,
+ );
+ }
+
+ // ========== Chunked Upload API ==========
+
+ /**
+ * Initialize a chunked upload session for large documents (>2MB)
+ * @param metadata - Document metadata including id, title, kind (MIME type), etc.
+ * @param totalSize - Total size of the document in bytes
+ * @param chunkSize - Optional chunk size (default: 5MB)
+ * @returns Upload session info including upload-id and total-chunks
+ */
+ beginUpload(
+ metadata: ChunkedUploadDocumentMetadata,
+ totalSize: number,
+ chunkSize?: number,
+ ): Promise {
+ return this.api
+ .makeRequest(
+ "librarian",
+ {
+ operation: "begin-upload",
+ "document-metadata": metadata,
+ "total-size": totalSize,
+ "chunk-size": chunkSize,
+ },
+ 30000,
+ )
+ .then((r) => {
+ if (r.error) {
+ throw new Error(r.error.message);
+ }
+ return r;
+ });
+ }
+
+ /**
+ * Upload a single chunk of a document
+ * Chunks can be uploaded in any order and in parallel
+ * @param uploadId - Upload session ID from beginUpload
+ * @param chunkIndex - Zero-based chunk index
+ * @param content - Base64-encoded chunk content
+ * @returns Progress info including chunks-received and bytes-received
+ */
+ uploadChunk(
+ uploadId: string,
+ chunkIndex: number,
+ content: string,
+ ): Promise {
+ return this.api
+ .makeRequest(
+ "librarian",
+ {
+ operation: "upload-chunk",
+ "upload-id": uploadId,
+ "chunk-index": chunkIndex,
+ content: content,
+ user: this.api.user,
+ },
+ 60000, // Longer timeout for chunk uploads
+ )
+ .then((r) => {
+ if (r.error) {
+ throw new Error(r.error.message);
+ }
+ return r;
+ });
+ }
+
+ /**
+ * Finalize a chunked upload after all chunks are received
+ * Triggers document processing
+ * @param uploadId - Upload session ID from beginUpload
+ * @returns Document ID and object ID
+ */
+ completeUpload(uploadId: string): Promise {
+ return this.api
+ .makeRequest(
+ "librarian",
+ {
+ operation: "complete-upload",
+ "upload-id": uploadId,
+ user: this.api.user,
+ },
+ 30000,
+ )
+ .then((r) => {
+ if (r.error) {
+ throw new Error(r.error.message);
+ }
+ return r;
+ });
+ }
+
+ /**
+ * Check upload progress (useful for resuming interrupted uploads)
+ * @param uploadId - Upload session ID
+ * @returns Status including received/missing chunks
+ */
+ getUploadStatus(uploadId: string): Promise {
+ return this.api
+ .makeRequest(
+ "librarian",
+ {
+ operation: "get-upload-status",
+ "upload-id": uploadId,
+ user: this.api.user,
+ },
+ 30000,
+ )
+ .then((r) => {
+ if (r.error) {
+ throw new Error(r.error.message);
+ }
+ return r;
+ });
+ }
+
+ /**
+ * Cancel an in-progress upload and clean up
+ * @param uploadId - Upload session ID to abort
+ */
+ abortUpload(uploadId: string): Promise {
+ return this.api
+ .makeRequest(
+ "librarian",
+ {
+ operation: "abort-upload",
+ "upload-id": uploadId,
+ user: this.api.user,
+ },
+ 30000,
+ )
+ .then((r) => {
+ if (r.error) {
+ throw new Error(r.error.message);
+ }
+ });
+ }
+
+ /**
+ * List pending upload sessions for the current user
+ * @returns Array of upload sessions with metadata and progress
+ */
+ listUploads(): Promise {
+ return this.api
+ .makeRequest(
+ "librarian",
+ {
+ operation: "list-uploads",
+ user: this.api.user,
+ },
+ 30000,
+ )
+ .then((r) => {
+ if (r.error) {
+ throw new Error(r.error.message);
+ }
+ return r["upload-sessions"] || [];
+ });
+ }
+
+ /**
+ * Stream a document in chunks for retrieval (streaming response)
+ * Sends one request, receives multiple chunk responses via callback
+ * @param documentId - Document ID to retrieve
+ * @param onChunk - Callback for each chunk: (content, chunkIndex, totalChunks, complete) => void
+ * @param onError - Callback for errors
+ * @param chunkSize - Optional chunk size (default: 1MB)
+ */
+ streamDocument(
+ documentId: string,
+ onChunk: (content: string, chunkIndex: number, totalChunks: number, complete: boolean) => void,
+ onError: (error: string) => void,
+ chunkSize?: number,
+ ): void {
+ const receiver = (message: unknown): boolean => {
+ const msg = message as { response?: StreamDocumentResponse; complete?: boolean; error?: string };
+
+ // Check for top-level error
+ if (msg.error) {
+ onError(msg.error);
+ return true;
+ }
+
+ const resp = msg.response;
+ if (!resp) {
+ return !!msg.complete;
+ }
+
+ // Check for response-level error
+ if (resp.error) {
+ onError(resp.error.message);
+ return true;
+ }
+
+ const complete = !!msg.complete;
+ onChunk(resp.content, resp["chunk-index"], resp["total-chunks"], complete);
+
+ return complete;
+ };
+
+ this.api.makeRequestMulti(
+ "librarian",
+ {
+ operation: "stream-document",
+ "document-id": documentId,
+ "chunk-size": chunkSize,
+ user: this.api.user,
+ },
+ receiver,
+ 300000, // 5 minute timeout for full document stream
+ );
+ }
+}
+
+/**
+ * FlowsApi - Manages processing flows and configuration
+ * Flows define how documents and data are processed through the system
+ */
+export class FlowsApi {
+ api: BaseApi;
+
+ constructor(api: BaseApi) {
+ this.api = api;
+ }
+
+ /**
+ * Retrieves list of available flows
+ */
+ getFlows() {
+ return this.api
+ .makeRequest(
+ "flow",
+ {
+ operation: "list-flows",
+ },
+ 60000,
+ )
+ .then((r) => r["flow-ids"] || []);
+ }
+
+ /**
+ * Retrieves definition of a specific flow
+ */
+ getFlow(id: string) {
+ return this.api
+ .makeRequest(
+ "flow",
+ {
+ operation: "get-flow",
+ "flow-id": id,
+ },
+ 60000,
+ )
+ .then((r) => JSON.parse(r.flow || "{}")); // Parse JSON flow definition
+ }
+
+ // Configuration management methods
+
+ /**
+ * Retrieves all configuration settings
+ */
+ getConfigAll() {
+ return this.api.makeRequest(
+ "config",
+ {
+ operation: "config",
+ },
+ 60000,
+ );
+ }
+
+ /**
+ * Retrieves specific configuration values by key
+ */
+ getConfig(keys: { type: string; key: string }[]) {
+ return this.api.makeRequest(
+ "config",
+ {
+ operation: "get",
+ keys: keys,
+ },
+ 60000,
+ );
+ }
+
+ /**
+ * Updates configuration values
+ */
+ putConfig(values: { type: string; key: string; value: string }[]) {
+ return this.api.makeRequest(
+ "config",
+ {
+ operation: "put",
+ values: values,
+ },
+ 60000,
+ );
+ }
+
+ /**
+ * Deletes configuration entries
+ */
+ deleteConfig(keys: { type: string; key: string }) {
+ return this.api.makeRequest(
+ "config",
+ {
+ operation: "delete",
+ keys: keys,
+ },
+ 30000,
+ );
+ }
+
+ // Prompt management - specialized config operations for AI prompts
+
+ /**
+ * Retrieves list of available prompt templates
+ */
+ getPrompts() {
+ return this.getConfigAll().then((r) => {
+ const config = r as Record<
+ string,
+ Record>
+ >;
+ return JSON.parse(config.config.prompt["template-index"]);
+ });
+ }
+
+ /**
+ * Retrieves a specific prompt template
+ */
+ getPrompt(id: string) {
+ return this.getConfigAll().then((r) => {
+ const config = r as Record<
+ string,
+ Record>
+ >;
+ return JSON.parse(config.config.prompt[`template.${id}`]);
+ });
+ }
+
+ /**
+ * Retrieves the system prompt configuration
+ */
+ getSystemPrompt() {
+ return this.getConfigAll().then((r) => {
+ const config = r as Record<
+ string,
+ Record>
+ >;
+ return JSON.parse(config.config.prompt.system);
+ });
+ }
+
+ // Flow blueprint management - templates for creating flows
+
+ /**
+ * Retrieves list of available flow blueprints (templates)
+ */
+ getFlowBlueprints() {
+ return this.api
+ .makeRequest(
+ "flow",
+ {
+ operation: "list-blueprints",
+ },
+ 60000,
+ )
+ .then((r) => r["blueprint-names"]);
+ }
+
+ /**
+ * Retrieves definition of a specific flow blueprint
+ */
+ getFlowBlueprint(name: string) {
+ return this.api
+ .makeRequest(
+ "flow",
+ {
+ operation: "get-blueprint",
+ "blueprint-name": name,
+ },
+ 60000,
+ )
+ .then((r) => JSON.parse(r["blueprint-definition"] || "{}"));
+ }
+
+ /**
+ * Deletes a flow blueprint
+ */
+ deleteFlowBlueprint(name: string) {
+ return this.api.makeRequest(
+ "flow",
+ {
+ operation: "delete-blueprint",
+ "blueprint-name": name,
+ },
+ 30000,
+ );
+ }
+
+ // Flow lifecycle management
+
+ /**
+ * Starts a new flow instance
+ */
+ startFlow(
+ id: string,
+ blueprint_name: string,
+ description: string,
+ parameters?: Record,
+ ) {
+ const request: FlowRequest = {
+ operation: "start-flow",
+ "flow-id": id,
+ "blueprint-name": blueprint_name,
+ description: description,
+ };
+
+ // Only include parameters if provided and not empty
+ if (parameters && Object.keys(parameters).length > 0) {
+ request.parameters = parameters;
+ }
+
+ return this.api
+ .makeRequest("flow", request, 30000)
+ .then((response) => {
+ if (response.error) {
+ let errorMessage = "Flow start failed";
+ if (
+ typeof response.error === "object" &&
+ response.error &&
+ "message" in response.error
+ ) {
+ errorMessage =
+ (response.error as { message?: string }).message || errorMessage;
+ } else if (typeof response.error === "string") {
+ errorMessage = response.error;
+ }
+ throw new Error(errorMessage);
+ }
+ return response;
+ });
+ }
+
+ /**
+ * Stops a running flow instance
+ */
+ stopFlow(id: string) {
+ return this.api.makeRequest(
+ "flow",
+ {
+ operation: "stop-flow",
+ "flow-id": id,
+ },
+ 30000,
+ );
+ }
+}
+
+/**
+ * FlowApi - Interface for interacting with a specific flow instance
+ * Provides flow-specific versions of core AI/ML operations
+ */
+export class FlowApi {
+ api: BaseApi;
+ flowId: string;
+
+ constructor(api: BaseApi, flowId: string) {
+ this.api = api;
+ this.flowId = flowId; // All requests will be routed through this flow
+ }
+
+ /**
+ * Performs text completion using AI models within this flow
+ */
+ textCompletion(system: string, text: string): Promise {
+ return this.api
+ .makeRequest(
+ "text-completion",
+ {
+ system: system, // System prompt/instructions
+ prompt: text, // User prompt
+ },
+ 30000,
+ undefined, // Use default retries
+ this.flowId, // Route through this flow
+ )
+ .then((r) => r.response);
+ }
+
+ /**
+ * Performs Graph RAG (Retrieval Augmented Generation) query
+ */
+ graphRag(text: string, options?: GraphRagOptions, collection?: string) {
+ return this.api
+ .makeRequest(
+ "graph-rag",
+ {
+ query: text,
+ user: this.api.user,
+ collection: collection || "default",
+ "entity-limit": options?.entityLimit,
+ "triple-limit": options?.tripleLimit,
+ "max-subgraph-size": options?.maxSubgraphSize,
+ "max-path-length": options?.pathLength,
+ },
+ 60000, // Longer timeout for complex graph operations
+ undefined,
+ this.flowId,
+ )
+ .then((r) => r.response);
+ }
+
+ /**
+ * Performs Document RAG (Retrieval Augmented Generation) query
+ */
+ documentRag(text: string, docLimit?: number, collection?: string) {
+ return this.api
+ .makeRequest(
+ "document-rag",
+ {
+ query: text,
+ user: this.api.user,
+ collection: collection || "default",
+ "doc-limit": docLimit || 20,
+ },
+ 60000, // Longer timeout for document operations
+ undefined,
+ this.flowId,
+ )
+ .then((r) => r.response);
+ }
+
+ /**
+ * Interacts with an AI agent that provides streaming responses
+ * BREAKING CHANGE: Callbacks now receive (chunk, complete, metadata?) instead of full messages
+ */
+ agent(
+ question: string,
+ think: (chunk: string, complete: boolean, metadata?: StreamingMetadata) => void,
+ observe: (chunk: string, complete: boolean, metadata?: StreamingMetadata) => void,
+ answer: (chunk: string, complete: boolean, metadata?: StreamingMetadata) => void,
+ error: (s: string) => void,
+ onExplain?: (event: ExplainEvent) => void,
+ ) {
+ const receiver = (message: unknown) => {
+ const msg = message as { response?: AgentResponse; complete?: boolean; error?: string };
+
+ // Check for top-level error
+ if (msg.error) {
+ error(msg.error);
+ return true;
+ }
+
+ const resp = msg.response || {};
+
+ // Check for errors in response
+ if (resp.chunk_type === "error" || resp.error) {
+ error(resp.error?.message || "Unknown agent error");
+ return true; // End streaming on error
+ }
+
+ // Handle explainability events (agent uses chunk_type="explain")
+ if ((resp.chunk_type === "explain" || resp.message_type === "explain") && resp.explain_id && resp.explain_graph) {
+ onExplain?.({
+ explainId: resp.explain_id,
+ explainGraph: resp.explain_graph,
+ });
+ return false;
+ }
+
+ // Handle streaming chunks by chunk_type
+ const content = resp.content || "";
+ const messageComplete = !!resp.end_of_message;
+ const dialogComplete = !!msg.complete;
+
+ // Extract metadata from final message
+ const metadata: StreamingMetadata | undefined = dialogComplete && (resp.in_token || resp.out_token || resp.model)
+ ? { in_token: resp.in_token, out_token: resp.out_token, model: resp.model }
+ : undefined;
+
+ switch (resp.chunk_type) {
+ case "thought":
+ think(content, messageComplete, metadata);
+ break;
+ case "observation":
+ observe(content, messageComplete, metadata);
+ break;
+ case "answer":
+ case "final-answer":
+ answer(content, messageComplete, metadata);
+ break;
+ case "action":
+ // Actions are typically not streamed incrementally, just logged
+ console.log("Agent action:", content);
+ break;
+ }
+
+ return dialogComplete; // End when backend signals complete
+ };
+
+ return this.api
+ .makeRequestMulti(
+ "agent",
+ {
+ question: question,
+ user: this.api.user,
+ streaming: true, // Always use streaming mode
+ },
+ receiver,
+ 120000,
+ 2,
+ this.flowId,
+ )
+ .catch((err) => {
+ const errorMessage =
+ err instanceof Error ? err.message : err?.toString() || "Unknown error";
+ error(`Agent request failed: ${errorMessage}`);
+ });
+ }
+
+ /**
+ * Performs Graph RAG query with streaming response
+ * @param text - Query text
+ * @param receiver - Called for each chunk with (chunk, complete) where complete=true on final chunk
+ * @param onError - Called on error
+ * @param options - Graph RAG options (including explainable flag)
+ * @param collection - Collection name
+ * @param onExplain - Optional callback for explainability events
+ */
+ graphRagStreaming(
+ text: string,
+ receiver: (chunk: string, complete: boolean, metadata?: StreamingMetadata) => void,
+ onError: (error: string) => void,
+ options?: GraphRagOptions,
+ collection?: string,
+ onExplain?: (event: ExplainEvent) => void,
+ ): void {
+ const recv = (message: unknown): boolean => {
+ const msg = message as { response?: GraphRagResponse; complete?: boolean; error?: string };
+
+ // Check for top-level error
+ if (msg.error) {
+ onError(msg.error);
+ return true;
+ }
+
+ const resp = (msg.response || {}) as GraphRagResponse;
+
+ // Check for response-level error
+ if (resp.error) {
+ onError(resp.error.message);
+ return true;
+ }
+
+ // Handle explainability events
+ if (resp.message_type === "explain" && resp.explain_id && resp.explain_graph) {
+ onExplain?.({
+ explainId: resp.explain_id,
+ explainGraph: resp.explain_graph,
+ });
+ // Don't return true - more messages may follow
+ return false;
+ }
+
+ // Handle chunk messages (default behavior)
+ const chunk = resp.response || resp.chunk || "";
+ const complete = !!resp.end_of_session || !!msg.complete;
+
+ // Extract metadata from final message
+ const metadata: StreamingMetadata | undefined = complete && (resp.in_token || resp.out_token || resp.model)
+ ? { in_token: resp.in_token, out_token: resp.out_token, model: resp.model }
+ : undefined;
+
+ receiver(chunk, complete, metadata);
+
+ return complete;
+ };
+
+ this.api.makeRequestMulti(
+ "graph-rag",
+ {
+ query: text,
+ user: this.api.user,
+ collection: collection || "default",
+ "entity-limit": options?.entityLimit,
+ "triple-limit": options?.tripleLimit,
+ "max-subgraph-size": options?.maxSubgraphSize,
+ "max-path-length": options?.pathLength,
+ streaming: true,
+ },
+ recv,
+ 60000,
+ undefined,
+ this.flowId,
+ );
+ }
+
+ /**
+ * Performs Document RAG query with streaming response
+ * @param text - Query text
+ * @param receiver - Called for each chunk with (chunk, complete) where complete=true on final chunk
+ * @param onError - Called on error
+ * @param docLimit - Maximum documents to retrieve
+ * @param collection - Collection name
+ */
+ documentRagStreaming(
+ text: string,
+ receiver: (chunk: string, complete: boolean, metadata?: StreamingMetadata) => void,
+ onError: (error: string) => void,
+ docLimit?: number,
+ collection?: string,
+ onExplain?: (event: ExplainEvent) => void,
+ ): void {
+ const recv = (message: unknown): boolean => {
+ const msg = message as { response?: DocumentRagResponse; complete?: boolean; error?: string };
+
+ // Check for top-level error
+ if (msg.error) {
+ onError(msg.error);
+ return true;
+ }
+
+ const resp = (msg.response || {}) as DocumentRagResponse;
+
+ // Check for response-level error
+ if (resp.error) {
+ onError(resp.error.message);
+ return true;
+ }
+
+ // Handle explainability events
+ if (resp.message_type === "explain" && resp.explain_id && resp.explain_graph) {
+ onExplain?.({
+ explainId: resp.explain_id,
+ explainGraph: resp.explain_graph,
+ });
+ return false;
+ }
+
+ const chunk = resp.response || resp.chunk || "";
+ const complete = !!resp.end_of_session || !!msg.complete;
+
+ // Extract metadata from final message
+ const metadata: StreamingMetadata | undefined = complete && (resp.in_token || resp.out_token || resp.model)
+ ? { in_token: resp.in_token, out_token: resp.out_token, model: resp.model }
+ : undefined;
+
+ receiver(chunk, complete, metadata);
+
+ return complete;
+ };
+
+ this.api.makeRequestMulti(
+ "document-rag",
+ {
+ query: text,
+ user: this.api.user,
+ collection: collection || "default",
+ "doc-limit": docLimit,
+ streaming: true,
+ },
+ recv,
+ 60000,
+ undefined,
+ this.flowId,
+ );
+ }
+
+ /**
+ * Performs text completion with streaming response
+ * @param system - System prompt
+ * @param text - User prompt
+ * @param receiver - Called for each chunk with (chunk, complete) where complete=true on final chunk
+ * @param onError - Called on error
+ */
+ textCompletionStreaming(
+ system: string,
+ text: string,
+ receiver: (chunk: string, complete: boolean, metadata?: StreamingMetadata) => void,
+ onError: (error: string) => void,
+ ): void {
+ const recv = (message: unknown): boolean => {
+ const msg = message as { response?: TextCompletionResponse; complete?: boolean; error?: string };
+
+ // Check for top-level error
+ if (msg.error) {
+ onError(msg.error);
+ return true;
+ }
+
+ const resp = (msg.response || {}) as TextCompletionResponse;
+
+ // Check for response-level error
+ if (resp.error) {
+ onError(resp.error.message);
+ return true;
+ }
+
+ // Text completion uses 'response' field for chunks
+ const chunk = resp.response || "";
+ const complete = !!msg.complete;
+
+ // Extract metadata from final message
+ const metadata: StreamingMetadata | undefined = complete && (resp.in_token || resp.out_token || resp.model)
+ ? { in_token: resp.in_token, out_token: resp.out_token, model: resp.model }
+ : undefined;
+
+ receiver(chunk, complete, metadata);
+
+ return complete;
+ };
+
+ this.api.makeRequestMulti(
+ "text-completion",
+ {
+ system: system,
+ prompt: text,
+ streaming: true,
+ },
+ recv,
+ 30000,
+ undefined,
+ this.flowId,
+ );
+ }
+
+ /**
+ * Executes a prompt template with streaming response
+ * @param id - Prompt template ID
+ * @param terms - Template variables
+ * @param receiver - Called for each chunk with (chunk, complete) where complete=true on final chunk
+ * @param onError - Called on error
+ */
+ promptStreaming(
+ id: string,
+ terms: Record,
+ receiver: (chunk: string, complete: boolean, metadata?: StreamingMetadata) => void,
+ onError: (error: string) => void,
+ ): void {
+ const recv = (message: unknown): boolean => {
+ const msg = message as { response?: PromptResponse; complete?: boolean; error?: string };
+
+ // Check for top-level error
+ if (msg.error) {
+ onError(msg.error);
+ return true;
+ }
+
+ const resp = (msg.response || {}) as PromptResponse;
+
+ // Check for response-level error
+ if (resp.error) {
+ onError(resp.error.message);
+ return true;
+ }
+
+ // Prompt service uses 'text' field for chunks
+ const chunk = resp.text || "";
+ const complete = !!msg.complete;
+
+ // Extract metadata from final message
+ const metadata: StreamingMetadata | undefined = complete && (resp.in_token || resp.out_token || resp.model)
+ ? { in_token: resp.in_token, out_token: resp.out_token, model: resp.model }
+ : undefined;
+
+ receiver(chunk, complete, metadata);
+
+ return complete;
+ };
+
+ this.api.makeRequestMulti(
+ "prompt",
+ {
+ id: id,
+ terms: terms,
+ streaming: true,
+ },
+ recv,
+ 30000,
+ undefined,
+ this.flowId,
+ );
+ }
+
+ /**
+ * Generates embeddings for multiple texts within this flow.
+ * Returns vectors[text_index][dimension_index] - one vector per input text.
+ */
+ embeddings(texts: string[]) {
+ return this.api
+ .makeRequest(
+ "embeddings",
+ {
+ texts: texts,
+ },
+ 30000,
+ undefined,
+ this.flowId,
+ )
+ .then((r) => r.vectors);
+ }
+
+ /**
+ * Queries the knowledge graph using a single embedding vector
+ */
+ graphEmbeddingsQuery(
+ vec: number[],
+ limit: number | undefined,
+ collection?: string,
+ ) {
+ return this.api
+ .makeRequest(
+ "graph-embeddings",
+ {
+ vector: vec,
+ limit: limit ? limit : 20, // Default to 20 results
+ user: this.api.user,
+ collection: collection || "default",
+ },
+ 30000,
+ undefined,
+ this.flowId,
+ )
+ .then((r) => r.entities);
+ }
+
+ /**
+ * Queries knowledge graph triples (subject-predicate-object relationships)
+ * All parameters are optional - omitted parameters act as wildcards
+ */
+ triplesQuery(
+ s?: Term,
+ p?: Term,
+ o?: Term,
+ limit?: number,
+ collection?: string,
+ graph?: string,
+ ) {
+ return this.api
+ .makeRequest(
+ "triples",
+ {
+ s: s, // Subject
+ p: p, // Predicate
+ o: o, // Object
+ g: graph, // Named graph URI filter
+ limit: limit ? limit : 20,
+ user: this.api.user,
+ collection: collection || "default",
+ },
+ 30000,
+ undefined,
+ this.flowId,
+ )
+ .then((r) => r.response);
+ }
+
+ /**
+ * Loads a document into this flow for processing
+ */
+ loadDocument(
+ document: string, // base64-encoded document
+ id?: string,
+ metadata?: Triple[],
+ ) {
+ return this.api.makeRequest(
+ "document-load",
+ {
+ id: id,
+ metadata: metadata,
+ data: document,
+ },
+ 30000,
+ undefined,
+ this.flowId,
+ );
+ }
+
+ /**
+ * Loads plain text into this flow for processing
+ */
+ loadText(
+ text: string, // Text content
+ id?: string,
+ metadata?: Triple[],
+ charset?: string, // Character encoding
+ ) {
+ return this.api.makeRequest(
+ "text-load",
+ {
+ id: id,
+ metadata: metadata,
+ text: text,
+ charset: charset,
+ },
+ 30000,
+ undefined,
+ this.flowId,
+ );
+ }
+
+ /**
+ * Executes a GraphQL query against structured row data
+ */
+ rowsQuery(
+ query: string,
+ collection?: string,
+ variables?: Record,
+ operationName?: string,
+ ) {
+ return this.api
+ .makeRequest(
+ "rows",
+ {
+ query: query,
+ user: this.api.user,
+ collection: collection || "default",
+ variables: variables,
+ operation_name: operationName,
+ },
+ 30000,
+ undefined,
+ this.flowId,
+ )
+ .then((r) => {
+ // Return the GraphQL response structure directly
+ const result: Record = {};
+ if (r.data !== undefined) result.data = r.data;
+ if (r.errors) result.errors = r.errors;
+ if (r.extensions) result.extensions = r.extensions;
+ return result;
+ });
+ }
+
+ /**
+ * Converts a natural language question to a GraphQL query
+ */
+ nlpQuery(question: string, maxResults?: number) {
+ return this.api
+ .makeRequest(
+ "nlp-query",
+ {
+ question: question,
+ max_results: maxResults || 100,
+ },
+ 30000,
+ undefined,
+ this.flowId,
+ )
+ .then((r) => r);
+ }
+
+ /**
+ * Executes a natural language question against structured data
+ * Combines NLP query conversion and GraphQL execution
+ */
+ structuredQuery(question: string, collection?: string) {
+ return this.api
+ .makeRequest(
+ "structured-query",
+ {
+ question: question,
+ user: this.api.user,
+ collection: collection || "default",
+ },
+ 30000,
+ undefined,
+ this.flowId,
+ )
+ .then((r) => {
+ // Return the response structure directly
+ const result: Record = {};
+ if (r.data !== undefined) result.data = r.data;
+ if (r.errors) result.errors = r.errors;
+ return result;
+ });
+ }
+
+ /**
+ * Performs semantic search on structured data indexes using embedding vectors
+ * @param vectors - Embedding vectors to search for
+ * @param schemaName - Name of the schema to search
+ * @param collection - Optional collection name
+ * @param indexName - Optional index name to filter results
+ * @param limit - Maximum number of results to return (default: 10)
+ */
+ rowEmbeddingsQuery(
+ vector: number[],
+ schemaName: string,
+ collection?: string,
+ indexName?: string,
+ limit?: number,
+ ): Promise {
+ const request: RowEmbeddingsQueryRequest = {
+ vector: vector,
+ schema_name: schemaName,
+ user: this.api.user,
+ collection: collection || "default",
+ limit: limit || 10,
+ };
+
+ if (indexName) {
+ request.index_name = indexName;
+ }
+
+ return this.api
+ .makeRequest(
+ "row-embeddings",
+ request,
+ 30000,
+ undefined,
+ this.flowId,
+ )
+ .then((r) => {
+ if (r.error) {
+ throw new Error(r.error.message);
+ }
+ return r.matches || [];
+ });
+ }
+}
+
+/**
+ * ConfigApi - Dedicated configuration management interface
+ * Handles system configuration, prompts, and token cost tracking
+ */
+export class ConfigApi {
+ api: BaseApi;
+
+ constructor(api: BaseApi) {
+ this.api = api;
+ }
+
+ /**
+ * Retrieves complete configuration
+ */
+ getConfigAll() {
+ return this.api.makeRequest(
+ "config",
+ {
+ operation: "config",
+ },
+ 60000,
+ );
+ }
+
+ /**
+ * Retrieves specific configuration entries
+ */
+ getConfig(keys: { type: string; key: string }[]) {
+ return this.api.makeRequest(
+ "config",
+ {
+ operation: "get",
+ keys: keys,
+ },
+ 60000,
+ );
+ }
+
+ /**
+ * Updates configuration values
+ */
+ putConfig(values: { type: string; key: string; value: string }[]) {
+ return this.api.makeRequest(
+ "config",
+ {
+ operation: "put",
+ values: values,
+ },
+ 60000,
+ );
+ }
+
+ /**
+ * Deletes configuration entries
+ */
+ deleteConfig(keys: { type: string; key: string }) {
+ return this.api.makeRequest(
+ "config",
+ {
+ operation: "delete",
+ keys: keys,
+ },
+ 30000,
+ );
+ }
+
+ // Specialized prompt management methods
+
+ /**
+ * Retrieves available prompt templates
+ */
+ getPrompts() {
+ return this.getConfigAll().then((r) => {
+ const config = r as Record<
+ string,
+ Record>
+ >;
+ return JSON.parse(config.config.prompt["template-index"]);
+ });
+ }
+
+ /**
+ * Retrieves a specific prompt template
+ */
+ getPrompt(id: string) {
+ return this.getConfigAll().then((r) => {
+ const config = r as Record<
+ string,
+ Record>
+ >;
+ return JSON.parse(config.config.prompt[`template.${id}`]);
+ });
+ }
+
+ /**
+ * Retrieves system prompt configuration
+ */
+ getSystemPrompt() {
+ return this.getConfigAll().then((r) => {
+ const config = r as Record<
+ string,
+ Record>
+ >;
+ return JSON.parse(config.config.prompt.system);
+ });
+ }
+
+ /**
+ * Lists available configuration types
+ */
+ list(type: string) {
+ return this.api
+ .makeRequest(
+ "config",
+ {
+ operation: "list",
+ type: type,
+ },
+ 60000,
+ )
+ .then((r) => r);
+ }
+
+ /**
+ * Retrieves all key/values for a specific type
+ */
+ getValues(type: string) {
+ return this.api
+ .makeRequest(
+ "config",
+ {
+ operation: "getvalues",
+ type: type,
+ },
+ 60000,
+ )
+ .then((r) => (r as RowsQueryResponse).values);
+ }
+
+ /**
+ * Retrieves token cost information for different AI models
+ * Useful for cost tracking and optimization
+ */
+ getTokenCosts() {
+ return this.api
+ .makeRequest(
+ "config",
+ {
+ operation: "getvalues",
+ type: "token-cost",
+ },
+ 60000,
+ )
+ .then((r) => {
+ // Parse JSON values and restructure data
+ const response = r as RowsQueryResponse;
+ return (response.values || []).map((x: unknown) => {
+ const item = x as Record;
+ return { key: item.key, value: JSON.parse(item.value) };
+ });
+ })
+ .then((r) =>
+ // Transform to more usable format
+ r.map((x: unknown) => {
+ const item = x as Record;
+ const value = item.value as Record;
+ return {
+ model: item.key,
+ input_price: value.input_price, // Cost per input token
+ output_price: value.output_price, // Cost per output token
+ };
+ }),
+ );
+ }
+}
+
+/**
+ * KnowledgeApi - Manages knowledge graph cores and data
+ * Knowledge cores appear to be collections of processed knowledge graph data
+ */
+export class KnowledgeApi {
+ api: BaseApi;
+
+ constructor(api: BaseApi) {
+ this.api = api;
+ }
+
+ /**
+ * Retrieves list of available knowledge graph cores
+ */
+ getKnowledgeCores() {
+ return this.api
+ .makeRequest(
+ "knowledge",
+ {
+ operation: "list-kg-cores",
+ user: this.api.user,
+ },
+ 60000,
+ )
+ .then((r) => r.ids || []);
+ }
+
+ /**
+ * Deletes a knowledge graph core
+ */
+ deleteKgCore(id: string, collection?: string) {
+ return this.api.makeRequest(
+ "knowledge",
+ {
+ operation: "delete-kg-core",
+ id: id,
+ user: this.api.user,
+ collection: collection || "default",
+ },
+ 30000,
+ );
+ }
+
+ /**
+ * Deletes a knowledge graph core
+ */
+ loadKgCore(id: string, flow: string, collection?: string) {
+ return this.api.makeRequest(
+ "knowledge",
+ {
+ operation: "load-kg-core",
+ id: id,
+ flow: flow,
+ user: this.api.user,
+ collection: collection || "default",
+ },
+ 30000,
+ );
+ }
+
+ /**
+ * Retrieves a knowledge graph core with streaming data
+ * Uses multi-request pattern for large datasets
+ * @param receiver - Callback function to handle streaming data chunks
+ */
+ getKgCore(
+ id: string,
+ collection: string | undefined,
+ receiver: (msg: unknown, eos: boolean) => void,
+ ) {
+ // Wrapper to handle end-of-stream detection
+ const recv = (msg: unknown) => {
+ const response = msg as Record;
+ if (response.eos) {
+ // End of stream - notify receiver and signal completion
+ receiver(msg, true);
+ return true;
+ } else {
+ // Regular message - continue streaming
+ receiver(msg, false);
+ return false;
+ }
+ };
+
+ return this.api.makeRequestMulti(
+ "knowledge",
+ {
+ operation: "get-kg-core",
+ id: id,
+ user: this.api.user,
+ collection: collection || "default",
+ },
+ recv, // Stream handler
+ 30000,
+ );
+ }
+}
+
+/**
+ * CollectionManagementApi - Manages collections for organizing documents
+ * Provides operations for listing, creating, updating, and deleting collections
+ */
+export class CollectionManagementApi {
+ api: BaseApi;
+
+ constructor(api: BaseApi) {
+ this.api = api;
+ }
+
+ /**
+ * Lists all collections for the current user with optional tag filtering
+ * @param tagFilter - Optional array of tags to filter collections
+ * @returns Promise resolving to array of collection metadata
+ */
+ listCollections(tagFilter?: string[]) {
+ const request: Record = {
+ operation: "list-collections",
+ user: this.api.user,
+ };
+
+ if (tagFilter && tagFilter.length > 0) {
+ request.tag_filter = tagFilter;
+ }
+
+ return this.api
+ .makeRequest<
+ Record,
+ Record
+ >("collection-management", request, 30000)
+ .then((r) => r.collections || []);
+ }
+
+ /**
+ * Creates or updates a collection for the current user
+ * @param collection - Collection ID (unique identifier)
+ * @param name - Display name for the collection
+ * @param description - Description of the collection
+ * @param tags - Array of tags for categorization
+ * @returns Promise resolving to updated collection metadata
+ */
+ updateCollection(
+ collection: string,
+ name?: string,
+ description?: string,
+ tags?: string[],
+ ) {
+ const request: Record = {
+ operation: "update-collection",
+ user: this.api.user,
+ collection,
+ };
+
+ if (name !== undefined) {
+ request.name = name;
+ }
+ if (description !== undefined) {
+ request.description = description;
+ }
+ if (tags !== undefined) {
+ request.tags = tags;
+ }
+
+ return this.api
+ .makeRequest<
+ Record,
+ Record
+ >("collection-management", request, 30000)
+ .then((r) => {
+ if (
+ r.collections &&
+ Array.isArray(r.collections) &&
+ r.collections.length > 0
+ ) {
+ return r.collections[0];
+ }
+ throw new Error("Failed to update collection");
+ });
+ }
+
+ /**
+ * Deletes a collection and all its data for the current user
+ * @param collection - Collection ID to delete
+ * @returns Promise resolving when deletion is complete
+ */
+ deleteCollection(collection: string) {
+ return this.api.makeRequest<
+ Record,
+ Record
+ >(
+ "collection-management",
+ {
+ operation: "delete-collection",
+ user: this.api.user,
+ collection,
+ },
+ 30000,
+ );
+ }
+}
+
+/**
+ * Factory function to create a new TrustGraph WebSocket connection
+ * This is the main entry point for using the TrustGraph API
+ * @param user - User identifier for API requests
+ * @param token - Optional authentication token for secure connections
+ * @param socketUrl - Optional WebSocket URL (defaults to /api/socket for browser, provide full URL for Node.js)
+ */
+export const createTrustGraphSocket = (
+ user: string,
+ token?: string,
+ socketUrl?: string,
+): BaseApi => {
+ return new BaseApi(user, token, socketUrl);
+};
diff --git a/ai-context/trustgraph-client/src/types.ts b/ai-context/trustgraph-client/src/types.ts
new file mode 100644
index 00000000..19bcb6bf
--- /dev/null
+++ b/ai-context/trustgraph-client/src/types.ts
@@ -0,0 +1,3 @@
+// Type definitions for TrustGraph client
+
+export {};
diff --git a/ai-context/trustgraph-client/test-graphrag.js b/ai-context/trustgraph-client/test-graphrag.js
new file mode 100755
index 00000000..308fef40
--- /dev/null
+++ b/ai-context/trustgraph-client/test-graphrag.js
@@ -0,0 +1,94 @@
+#!/usr/bin/env node
+
+/**
+ * Standalone test for GraphRAG streaming
+ * Tests the question "What is a cat?" using GraphRAG streaming mode
+ */
+
+import { createTrustGraphSocket } from './dist/index.esm.js';
+
+// Configuration
+const USER = 'trustgraph';
+const SOCKET_URL = 'ws://localhost:8088/api/v1/socket';
+const QUESTION = 'What is a cat?';
+
+console.log('GraphRAG Streaming Test');
+console.log('======================');
+console.log(`User: ${USER}`);
+console.log(`Socket URL: ${SOCKET_URL}`);
+console.log(`Question: "${QUESTION}"\n`);
+
+// Create socket connection
+const socket = createTrustGraphSocket(USER, undefined, SOCKET_URL);
+
+// Wait for connection to establish
+setTimeout(() => {
+ console.log('Starting GraphRAG query...\n');
+
+ let accumulated = '';
+ let chunkCount = 0;
+
+ // GraphRAG options
+ const options = {
+ entityLimit: 50,
+ tripleLimit: 30,
+ maxSubgraphSize: 1000,
+ pathLength: 2,
+ };
+
+ // Streaming receiver callback
+ const onChunk = (chunk, complete, metadata) => {
+ chunkCount++;
+ accumulated += chunk;
+
+ if (chunk) {
+ process.stdout.write(chunk);
+ }
+
+ if (complete) {
+ console.log('\n\n--- Streaming Complete ---');
+ console.log(`Total chunks received: ${chunkCount}`);
+ console.log(`Total characters: ${accumulated.length}`);
+
+ if (metadata) {
+ console.log('\nMetadata:');
+ if (metadata.model) console.log(` Model: ${metadata.model}`);
+ if (metadata.in_token) console.log(` Input tokens: ${metadata.in_token}`);
+ if (metadata.out_token) console.log(` Output tokens: ${metadata.out_token}`);
+ }
+
+ console.log('\n--- Full Response ---');
+ console.log(accumulated);
+
+ // Close socket and exit
+ socket.close();
+ process.exit(0);
+ }
+ };
+
+ // Error callback
+ const onError = (error) => {
+ console.error('\n\nERROR:', error);
+ socket.close();
+ process.exit(1);
+ };
+
+ // Execute GraphRAG streaming query
+ socket
+ .flow('default')
+ .graphRagStreaming(
+ QUESTION,
+ onChunk,
+ onError,
+ options,
+ 'default' // collection
+ );
+
+}, 1000); // Wait 1 second for connection
+
+// Handle process termination
+process.on('SIGINT', () => {
+ console.log('\n\nInterrupted. Closing socket...');
+ socket.close();
+ process.exit(0);
+});
diff --git a/ai-context/trustgraph-client/test-streaming.js b/ai-context/trustgraph-client/test-streaming.js
new file mode 100755
index 00000000..5ae6800a
--- /dev/null
+++ b/ai-context/trustgraph-client/test-streaming.js
@@ -0,0 +1,111 @@
+#!/usr/bin/env node
+
+/**
+ * Test script for TrustGraph streaming APIs
+ * Tests both streaming and non-streaming text completion
+ *
+ * Usage:
+ * node test-streaming.js
+ *
+ * Requirements:
+ * - TrustGraph backend running on http://localhost:8088
+ * - Built client library in ./dist/
+ */
+
+import { createTrustGraphSocket } from './dist/index.esm.js';
+
+const USER = "test-user";
+const SYSTEM_PROMPT = "You are a helpful AI assistant.";
+const TEST_PROMPT = "Explain what streaming is in one paragraph.";
+const SOCKET_URL = "ws://localhost:8888/api/socket";
+
+console.log("=".repeat(80));
+console.log("TrustGraph Streaming API Test");
+console.log("=".repeat(80));
+console.log(`Connecting to: ${SOCKET_URL}`);
+console.log(`User: ${USER}`);
+console.log("=".repeat(80));
+
+// Create client connection with explicit WebSocket URL for Node.js
+const client = createTrustGraphSocket(USER, undefined, SOCKET_URL);
+
+// Wait a bit for connection to establish
+await new Promise(resolve => setTimeout(resolve, 1000));
+
+console.log("\n[1/2] Testing NON-STREAMING text completion...");
+console.log("-".repeat(80));
+
+try {
+ const flowApi = client.flow("default");
+ const response = await flowApi.textCompletion(SYSTEM_PROMPT, TEST_PROMPT);
+
+ console.log("✓ Non-streaming response received:");
+ console.log(response);
+} catch (error) {
+ console.error("✗ Non-streaming failed:", error.message);
+}
+
+console.log("\n[2/2] Testing STREAMING text completion...");
+console.log("-".repeat(80));
+
+try {
+ const flowApi = client.flow("default");
+
+ let accumulated = "";
+ let chunkCount = 0;
+ const startTime = Date.now();
+
+ await new Promise((resolve, reject) => {
+ flowApi.textCompletionStreaming(
+ SYSTEM_PROMPT,
+ TEST_PROMPT,
+ (chunk, complete, metadata) => {
+ chunkCount++;
+ accumulated += chunk;
+
+ // Show progress indicator
+ if (chunk) {
+ process.stdout.write(chunk);
+ }
+
+ if (complete) {
+ const duration = Date.now() - startTime;
+ console.log("\n");
+ console.log("-".repeat(80));
+ console.log(`✓ Streaming complete!`);
+ console.log(` Chunks received: ${chunkCount}`);
+ console.log(` Total length: ${accumulated.length} chars`);
+ console.log(` Duration: ${duration}ms`);
+ console.log(` First chunk: ~${(startTime - Date.now() + duration) / chunkCount}ms`);
+
+ // Display token usage and model info if available
+ if (metadata) {
+ console.log("\n Metadata:");
+ if (metadata.model) console.log(` Model: ${metadata.model}`);
+ if (metadata.in_token !== undefined) console.log(` Input tokens: ${metadata.in_token}`);
+ if (metadata.out_token !== undefined) console.log(` Output tokens: ${metadata.out_token}`);
+ if (metadata.in_token && metadata.out_token) {
+ console.log(` Total tokens: ${metadata.in_token + metadata.out_token}`);
+ }
+ }
+
+ resolve();
+ }
+ },
+ (error) => {
+ console.error("\n✗ Streaming error:", error);
+ reject(new Error(error));
+ }
+ );
+ });
+} catch (error) {
+ console.error("✗ Streaming failed:", error.message);
+}
+
+console.log("\n" + "=".repeat(80));
+console.log("Test complete!");
+console.log("=".repeat(80));
+
+// Close connection
+client.close();
+process.exit(0);
diff --git a/ai-context/trustgraph-client/tsconfig.json b/ai-context/trustgraph-client/tsconfig.json
new file mode 100644
index 00000000..f69c5d78
--- /dev/null
+++ b/ai-context/trustgraph-client/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "ESNext",
+ "lib": ["ES2020", "DOM"],
+ "jsx": "react",
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true
+ },
+ "include": ["src"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/ai-context/trustgraph-client/vitest.config.ts b/ai-context/trustgraph-client/vitest.config.ts
new file mode 100644
index 00000000..1d10f07b
--- /dev/null
+++ b/ai-context/trustgraph-client/vitest.config.ts
@@ -0,0 +1,8 @@
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ test: {
+ globals: true,
+ environment: "happy-dom",
+ },
+});
diff --git a/ai-context/trustgraph-templates/.github/workflows/cla.yml b/ai-context/trustgraph-templates/.github/workflows/cla.yml
new file mode 100644
index 00000000..73f582cf
--- /dev/null
+++ b/ai-context/trustgraph-templates/.github/workflows/cla.yml
@@ -0,0 +1,25 @@
+name: CLA Assistant
+
+on:
+ issue_comment:
+ types: [created]
+ pull_request_target:
+ types: [opened, synchronize, reopened]
+
+permissions:
+ actions: write
+ contents: write
+ pull-requests: write
+ statuses: write
+
+jobs:
+ CLAssistant:
+ runs-on: ubuntu-latest
+ steps:
+ - name: CLA Assistant
+ uses: trustgraph-ai/contributor-license-agreement/action@main
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ PERSONAL_ACCESS_TOKEN: ${{ secrets.CLA_ASSISTANT_PAT }}
+ with:
+ allowlist: 'dependabot,dependabot[bot],github-actions,github-actions[bot]'
diff --git a/ai-context/trustgraph-templates/.github/workflows/deploy-prod.yaml b/ai-context/trustgraph-templates/.github/workflows/deploy-prod.yaml
new file mode 100644
index 00000000..cb902a52
--- /dev/null
+++ b/ai-context/trustgraph-templates/.github/workflows/deploy-prod.yaml
@@ -0,0 +1,76 @@
+
+name: Deploy to prod
+
+on:
+ workflow_dispatch:
+ push:
+ # Deploys on master branch
+ branches:
+ - master
+
+permissions:
+ contents: read
+ id-token: 'write'
+ packages: read
+
+jobs:
+
+ deploy:
+
+ name: Deploy to prod
+ runs-on: ubuntu-latest
+
+ steps:
+
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Get version
+ id: version
+ run: echo VERSION=sha-$(git rev-parse --short HEAD) >> $GITHUB_OUTPUT
+
+ # Python package version MUST be a semantic version, but also doesn't
+ # matter, so just setting to 0.0.0.
+ # The container version MUST change on every push to get Cloud Run
+ # to re-deploy, so is based on git hash.
+ - name: Build container
+ run: make PACKAGE_VERSION=0.0.0 VERSION=${{ steps.version.outputs.VERSION }}
+
+ - name: Log in to the container registry
+ uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - id: auth
+ name: Authenticate with Google Cloud
+ uses: google-github-actions/auth@v2
+ with:
+ token_format: access_token
+ workload_identity_provider: projects/351149249312/locations/global/workloadIdentityPools/deploy/providers/github
+ service_account: deploy@trustgraph-ai.iam.gserviceaccount.com
+ access_token_lifetime: 900s
+ create_credentials_file: true
+
+ - name: Login to Artifact Registry
+ uses: redhat-actions/podman-login@v1
+ with:
+ registry: europe-west1-docker.pkg.dev
+ username: oauth2accesstoken
+ password: ${{ steps.auth.outputs.access_token }}
+
+ - name: Install Pulumi
+ run: cd pulumi && npm install
+
+ - name: Applying infrastructure 🚀🙏
+ uses: pulumi/actions@v3
+ with:
+ command: up
+ stack-name: prod
+ work-dir: pulumi
+ cloud-url: gs://trustgraph-ai-deploy/config-svc
+ env:
+ PULUMI_CONFIG_PASSPHRASE: ""
+ IMAGE_VERSION: ${{ steps.version.outputs.VERSION }}
+
diff --git a/ai-context/trustgraph-templates/.github/workflows/pull-request.yaml b/ai-context/trustgraph-templates/.github/workflows/pull-request.yaml
new file mode 100644
index 00000000..dbe64b15
--- /dev/null
+++ b/ai-context/trustgraph-templates/.github/workflows/pull-request.yaml
@@ -0,0 +1,31 @@
+
+name: Test pull request
+
+on:
+ pull_request:
+
+permissions:
+ contents: read
+
+jobs:
+
+ container-push:
+
+ name: Run tests
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Setup environment
+ run: |
+ python3 -m venv env
+ . env/bin/activate
+ pip install -e .[dev]
+
+ - name: Run pytest tests
+ run: |
+ . env/bin/activate
+ pytest -v --tb=short
+
diff --git a/ai-context/trustgraph-templates/.github/workflows/undeploy-prod.yaml b/ai-context/trustgraph-templates/.github/workflows/undeploy-prod.yaml
new file mode 100644
index 00000000..d5cd2628
--- /dev/null
+++ b/ai-context/trustgraph-templates/.github/workflows/undeploy-prod.yaml
@@ -0,0 +1,45 @@
+
+name: Undeploy to prod
+
+on:
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ id-token: 'write'
+
+jobs:
+
+ deploy:
+
+ name: Undeploy to prod
+ runs-on: ubuntu-latest
+
+ steps:
+
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - id: auth
+ name: Authenticate with Google Cloud
+ uses: google-github-actions/auth@v2
+ with:
+ token_format: access_token
+ workload_identity_provider: projects/514167726704/locations/global/workloadIdentityPools/deploy/providers/deploy
+ service_account: deploy@kalntera-demo.iam.gserviceaccount.com
+ access_token_lifetime: 900s
+ create_credentials_file: true
+
+ - name: Install Pulumi
+ run: cd pulumi && npm install
+
+ - name: Destroy infrastructure ☠🔥
+ uses: pulumi/actions@v3
+ with:
+ command: destroy
+ stack-name: prod
+ work-dir: pulumi
+ cloud-url: gs://trustgraph-deploy/config-ui
+ env:
+ PULUMI_CONFIG_PASSPHRASE: ""
+
diff --git a/ai-context/trustgraph-templates/.gitignore b/ai-context/trustgraph-templates/.gitignore
new file mode 100644
index 00000000..6a5e4738
--- /dev/null
+++ b/ai-context/trustgraph-templates/.gitignore
@@ -0,0 +1,22 @@
+*~
+__pycache__
+*.pyc
+*.pyo
+*.egg-info/
+build/
+dist/
+*.egg
+.pytest_cache/
+.coverage
+htmlcov/
+.eggs/
+*.so
+*.dylib
+.Python
+env/
+venv/
+ENV/
+*.log
+node_modules/
+trustgraph_configurator/version.py
+INSTALLATION.md
diff --git a/ai-context/trustgraph-templates/Containerfile b/ai-context/trustgraph-templates/Containerfile
new file mode 100644
index 00000000..4eba036e
--- /dev/null
+++ b/ai-context/trustgraph-templates/Containerfile
@@ -0,0 +1,38 @@
+# --- STAGE 1: Build ---
+FROM python:3.14-slim AS build
+
+# Install build tools (Debian uses apt)
+RUN apt-get update && apt-get install -y \
+ build-essential \
+ golang \
+ git \
+ && rm -rf /var/lib/apt/lists/*
+
+RUN mkdir -p /root/wheels /root/build
+
+# Build wheels
+RUN pip wheel -w /root/wheels --no-deps gojsonnet
+
+COPY trustgraph_configurator/ /root/build/trustgraph_configurator/
+COPY pyproject.toml /root/build/pyproject.toml
+COPY README.md /root/build/README.md
+
+RUN (cd /root/build && pip wheel -w /root/wheels --no-deps .)
+
+# --- STAGE 2: Runtime ---
+FROM python:3.14-slim
+
+# No need to install libstdc++ manually, it's included in python-slim
+RUN apt-get update && apt-get install -y \
+ && rm -rf /var/lib/apt/lists/*
+
+# aiohttp and others will be pulled from PyPI or handled via wheels
+COPY --from=build /root/wheels /root/wheels
+
+# Install your wheels plus regular dependencies
+RUN pip install --no-cache-dir /root/wheels/* aiohttp pyyaml tabulate && \
+ rm -rf /root/wheels
+
+CMD ["tg-config-svc"]
+EXPOSE 8080
+
diff --git a/ai-context/trustgraph-templates/DIALOG-FLOW.md b/ai-context/trustgraph-templates/DIALOG-FLOW.md
new file mode 100644
index 00000000..e70ca46b
--- /dev/null
+++ b/ai-context/trustgraph-templates/DIALOG-FLOW.md
@@ -0,0 +1,168 @@
+# TrustGraph Configuration Dialog Flow
+
+## Overview
+
+A configuration wizard system that guides users through TrustGraph deployment setup. Outputs deployment configuration and contextual documentation.
+
+Supports two interfaces:
+- **Web UI**: Step-by-step wizard with visual cards
+- **CLI**: Interactive terminal prompts
+
+## Architecture
+
+```
+┌─────────────────────┐
+│ trustgraph-flow │ State machine defining wizard steps
+│ .yaml │ and transitions
+└──────────┬──────────┘
+ │
+ ▼
+┌─────────────────────┐
+│ Flow Engine │ Executes state machine, collects user input,
+│ (Web UI / CLI) │ manages state and backtracking
+└──────────┬──────────┘
+ │
+ ▼
+ ┌──────────────┐
+ │ Wizard State │ Simple key/value object
+ │ (JSON) │ e.g., { platform: "gke", model_deployment: "ollama", ... }
+ └──────┬───────┘
+ │
+ ┌─────┴─────┐
+ ▼ ▼
+┌─────────┐ ┌─────────────┐
+│ Output │ │Documentation│
+│Transform│ │ Assembler │
+└────┬────┘ └──────┬──────┘
+ │ │
+ ▼ ▼
+┌─────────┐ ┌─────────────┐
+│Component│ │ README.md │
+│ Array │ │ / Checklist │
+│ (JSON) │ │ (JSON) │
+└─────────┘ └─────────────┘
+```
+
+## Key Design Decisions
+
+| Decision | Choice | Rationale |
+|----------|--------|-----------|
+| Flow structure | State machine | Explicit transitions, supports conditional branching |
+| Expression language | JSONata | Single language for conditions and transforms |
+| Documentation storage | Manifest + markdown fragments | Clean separation, easy to maintain |
+| Platform variants | `in ['docker-compose', 'podman-compose']` | Explicit grouping, no hidden logic |
+| Template variables | `{{var}}` syntax | Simple, familiar |
+
+## File Structure
+
+```
+config-dialog-flow/
+├── dialog-flow-schema.json # Schema: flow definitions
+├── docs-manifest-schema.json # Schema: documentation manifests
+├── trustgraph-flow.yaml # Flow: wizard state machine
+├── trustgraph-output.jsonata # Transform: state → components
+├── trustgraph-docs.yaml # Manifest: state → documentation
+└── docs/ # Fragments: markdown content
+ ├── platform/
+ ├── model/
+ ├── storage/
+ ├── gateway/
+ ├── deploy/
+ └── features/
+```
+
+## Data Flow
+
+1. **Flow engine** loads `trustgraph-flow.yaml`
+2. User progresses through steps, engine collects state
+3. On completion:
+ - **Output transform** evaluates `trustgraph-output.jsonata` against state → component array
+ - **Doc assembler** evaluates `trustgraph-docs.yaml` conditions against state → filtered instructions → loads markdown fragments → outputs README or checklist JSON
+
+## Implementation Phases
+
+### Phase 1: Core Flow Engine
+
+- [ ] Parse YAML flow definition
+- [ ] Implement state machine executor
+- [ ] Handle step rendering (title, description, input)
+- [ ] Evaluate JSONata transition conditions
+- [ ] Manage wizard state (set values, navigate)
+- [ ] Implement backtracking (previous step, state rollback)
+
+### Phase 2: Input Types
+
+- [ ] Select (single choice from options)
+- [ ] Toggle (boolean)
+- [ ] Number (with min/max/step)
+- [ ] Text (free input)
+- [ ] Skip single-option steps (UX decision)
+
+### Phase 3: Output Generation
+
+- [ ] Load JSONata transform
+- [ ] Evaluate against wizard state
+- [ ] Produce component array JSON
+- [ ] Package as downloadable ZIP (existing functionality)
+
+### Phase 4: Documentation Assembly
+
+- [ ] Parse documentation manifest
+- [ ] Evaluate `when` conditions (JSONata)
+- [ ] Filter to matching instructions
+- [ ] Load markdown fragments
+- [ ] Substitute `{{var}}` placeholders
+- [ ] Output as README.md (CLI) or checklist JSON (Web)
+
+### Phase 5: CLI Interface
+
+- [ ] Terminal prompt for each step
+- [ ] Numbered option selection
+- [ ] Progress indicator (`[3/12]`)
+- [ ] Back command
+- [ ] Review summary before generate
+
+### Phase 6: Web Interface
+
+- [ ] Step-by-step card UI
+- [ ] Progress bar
+- [ ] Option cards with icons/descriptions
+- [ ] Back navigation
+- [ ] Review screen with edit capability
+- [ ] Generate button
+
+## Expression Language
+
+All conditions use JSONata:
+
+```
+platform = 'docker-compose'
+platform in ['gke', 'eks', 'aks', 'minikube']
+model_deployment = 'ollama' and platform in ['docker-compose', 'podman-compose']
+version < '1.6.0'
+ocr.enabled = true
+```
+
+## State Shape
+
+```
+{
+ "version": "1.8.18",
+ "platform": "gke",
+ "k8s": { "namespace": "trustgraph" },
+ "graph_store": "cassandra",
+ "vector_db": "qdrant",
+ "object_store": "cassandra",
+ "model_deployment": "ollama",
+ "max_output_tokens": 2048,
+ "ocr": { "enabled": true, "engine": "tesseract" },
+ "embeddings": { "enabled": false }
+}
+```
+
+## Out of Scope (Future)
+
+- Dual model mode (separate main/RAG models)
+- Advanced settings (concurrency, chunking, memory profiles)
+- Configuration import/export
+- Validation beyond type checking
diff --git a/ai-context/trustgraph-templates/Makefile b/ai-context/trustgraph-templates/Makefile
new file mode 100644
index 00000000..ef0cdfec
--- /dev/null
+++ b/ai-context/trustgraph-templates/Makefile
@@ -0,0 +1,22 @@
+
+PACKAGE_VERSION=0.0.0
+VERSION=0.0.0
+
+all: container
+
+package: update-package-versions
+ python3 -m build --sdist --outdir pkgs
+
+update-package-versions:
+ echo __version__ = \"${PACKAGE_VERSION}\" > trustgraph_configurator/version.py
+
+CONTAINER=localhost/config-svc
+DOCKER=podman
+
+container:
+ ${DOCKER} build -f Containerfile -t ${CONTAINER}:${VERSION} \
+ --format docker
+
+# On port 8081
+run-container:
+ ${DOCKER} run -i -t -p 8081:8080 ${CONTAINER}:${VERSION}
diff --git a/ai-context/trustgraph-templates/README.md b/ai-context/trustgraph-templates/README.md
new file mode 100644
index 00000000..a83bbbbc
--- /dev/null
+++ b/ai-context/trustgraph-templates/README.md
@@ -0,0 +1,311 @@
+# TrustGraph Configuration Templates
+
+TrustGraph configurator is a Python-based tool that generates deployment configurations for TrustGraph AI systems. It supports multiple deployment platforms and provides templated configurations with versioning support.
+
+## Overview
+
+The configurator uses Jsonnet templates to generate deployment configurations for various platforms including Docker Compose, Podman, and multiple Kubernetes environments. It packages the generated configurations into ZIP files containing all necessary deployment resources.
+
+## Configuration Process
+
+The TrustGraph configuration system uses a multi-stage pipeline to generate deployment packages:
+
+```
+┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
+│ Dialog Flow │ │ JSONata │ │ Configuration │ │ Deployment │
+│ Configuration │────▶│ Transform │────▶│ Service │────▶│ Package │
+│ │ │ │ │ │ │ │
+│ (state object) │ │ (config object) │ │ (templates) │ │ (ZIP file) │
+└─────────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘
+ │
+ │ ┌─────────────────┐ ┌─────────────────┐
+ └──────────────▶│ Documentation │────▶│ Installation │
+ │ Flow │ │ Guide │
+ └─────────────────┘ └─────────────────┘
+```
+
+### 1. Dialog Flow Configuration
+
+The dialog flow file (`trustgraph-flow.yaml`) describes configuration steps in a technology-neutral way. A UI wizard walks users through these steps, collecting choices about platform, model provider, storage backends, and features. The output is a **state object** - a simple key/value map representing all user selections.
+
+### 2. JSONata Transform
+
+The JSONata transform file (`trustgraph-output.jsonata`) converts the state object into a **configuration object**. This object understands how to invoke TrustGraph templates and contains the structured parameters needed by the template system.
+
+### 3. Configuration Service
+
+The configuration service receives the configuration object, invokes the appropriate Jsonnet templates, and runs the package builder. The output is a **deployment package** - a ZIP file containing all deployment resources (docker-compose.yaml or Kubernetes manifests, plus supporting files).
+
+### 4. Documentation Flow
+
+The original state object can also be used with the documentation manifest (`trustgraph-docs.yaml`) to generate a customised **installation guide** based on the user's specific configuration choices.
+
+## Installation
+
+The configurator is distributed as a Python package. To use it:
+
+```bash
+export PYTHONPATH=.
+# or install the package
+pip install -e .
+```
+
+## Usage
+
+### List Available Configurations
+
+To see all available templates and platforms:
+
+```bash
+tg-show-config-params
+```
+
+This will display:
+- Available platforms (docker-compose, podman-compose, various Kubernetes options)
+- Available templates with versions and stability status
+- Latest version and latest stable version
+
+### Generate Configuration
+
+To generate a configuration package:
+
+```bash
+scripts/tg-build-deployment --template --version \
+ --input config.json --output output.zip --platform
+```
+
+Example:
+```bash
+scripts/tg-build-deployment --template 1.1 --version 1.1.9 \
+ --input config.json --output deployment.zip --platform docker-compose
+```
+
+#### Output to stdout
+
+To output only the TrustGraph configuration:
+```bash
+scripts/tg-build-deployment --template 1.1 --latest-stable \
+ --input config.json -O > trustgraph-config.json
+```
+
+To output only the platform resources (docker-compose.yaml or resources.yaml):
+```bash
+# For Docker Compose
+scripts/tg-build-deployment --template 1.1 --latest-stable \
+ --input config.json --platform docker-compose -R > docker-compose.yaml
+
+# For Kubernetes
+scripts/tg-build-deployment --template 1.1 --latest-stable \
+ --input config.json --platform gcp-k8s -R > resources.yaml
+```
+
+### Configuration Service API
+
+You can also run the configurator as a REST API service:
+
+```bash
+scripts/tg-config-svc
+```
+
+This starts a web service on port 8080 that provides:
+- REST API endpoints for configuration generation
+- Programmatic access to version information
+- Web-based configuration generation
+
+The service provides the same functionality as the command-line tool but through HTTP endpoints (see API Service section below for details).
+
+### Command Line Options
+
+- `-i, --input`: Input configuration file (default: config.json)
+- `-o, --output`: Output ZIP file (default: output.zip)
+- `-t, --template`: Template name (e.g., "1.1", "1.0", "0.23")
+- `-v, --version`: Specific version to use
+- `-p, --platform`: Target platform (default: docker-compose)
+- `--latest`: Use the latest available version
+- `--latest-stable`: Use the latest stable version
+- `-O, --output-tg-config`: Output only TrustGraph configuration to stdout (no ZIP file)
+- `-R, --output-resources`: Output only platform resources (docker-compose.yaml or resources.yaml) to stdout (no ZIP file)
+
+### Available Platforms
+
+- `docker-compose`: Local Docker deployment using docker-compose
+- `podman-compose`: Local Podman deployment using podman-compose
+- `minikube-k8s`: Minikube Kubernetes cluster
+- `gcp-k8s`: Google Cloud Kubernetes (GKE)
+- `aks-k8s`: Azure Kubernetes Service (AKS)
+- `eks-k8s`: AWS Elastic Kubernetes Service (EKS)
+- `scw-k8s`: Scaleway Kubernetes
+
+## Python Architecture
+
+### Module Structure
+
+The `trustgraph_configurator` package consists of several key modules:
+
+#### Core Modules
+
+1. **generator.py** (`Generator` class)
+ - Processes Jsonnet templates using the `_jsonnet` library
+ - Evaluates configuration snippets with custom import callbacks
+ - Returns processed JSON configurations
+
+2. **packager.py** (`Packager` class)
+ - Main orchestrator for configuration generation
+ - Handles template and resource file loading
+ - Generates platform-specific deployment packages
+ - Creates ZIP archives with all necessary files
+ - Supports both Docker Compose and Kubernetes outputs
+
+3. **index.py** (`Index` class)
+ - Manages template and platform metadata
+ - Reads from `templates/index.json`
+ - Provides version sorting and comparison
+ - Offers methods to get latest/stable versions
+
+4. **api.py** (`Api` class)
+ - REST API service for configuration generation
+ - Endpoints for version information and generation
+ - Validates input JSON before processing
+ - Returns generated configurations as binary data
+
+5. **service.py**
+ - Simple wrapper to run the API service
+ - Configures logging and starts the web server on port 8080
+
+6. **run.py**
+ - Command-line interface implementation
+ - Argument parsing and validation
+ - Reads input configuration and writes output ZIP
+
+7. **list.py**
+ - Command-line tool to list available configurations
+ - Displays platforms, templates, and versions in tabular format
+
+### How Components Interact
+
+```
+User Input (config.json)
+ ↓
+run.py (CLI) or api.py (REST)
+ ↓
+Packager (orchestrator)
+ ├─→ Index (metadata/versions)
+ ├─→ Generator (Jsonnet processing)
+ └─→ Resource files (templates/)
+ ↓
+ Platform-specific generation
+ (Docker Compose or Kubernetes)
+ ↓
+ ZIP archive (output.zip)
+```
+
+### Key Design Patterns
+
+1. **Template Resolution**: The `Packager.fetch()` method implements a sophisticated file resolution system:
+ - Special handling for `trustgraph/config.json` and `version.jsonnet`
+ - Fallback search paths for templates and resources
+ - Version-specific template directories
+
+2. **Platform Abstraction**: Different platforms are handled through:
+ - Platform-specific Jsonnet templates (e.g., `config-to-docker-compose.jsonnet`)
+ - Conditional logic in `Packager.generate()`
+ - Unified output format (ZIP archives)
+
+3. **Version Management**: The system supports:
+ - Multiple template versions with different features
+ - Stability levels (alpha, beta, stable)
+ - Automatic version selection (latest/latest-stable)
+
+### Configuration Flow
+
+1. User provides a JSON configuration file
+2. Packager validates and loads the appropriate template version
+3. Generator processes Jsonnet templates with the configuration
+4. Platform-specific resources are added (Grafana dashboards, Prometheus config)
+5. Everything is packaged into a ZIP file for deployment
+
+### API Service
+
+The REST API service (`tg-config-svc`) provides programmatic access to the configurator functionality. Start the service with:
+
+```bash
+scripts/tg-config-svc
+```
+
+The service runs on port 8080 and provides the following endpoints:
+
+```
+POST /api/generate/{platform}/{template} # Generate configuration
+GET /api/latest # Get latest version info
+GET /api/latest-stable # Get latest stable version info
+GET /api/versions # List all available versions
+```
+
+#### Dialog Flow Resources
+
+These endpoints serve the dialog flow resources described in the Configuration Process section:
+
+```
+GET /api/dialog-flow # Dialog flow state machine (YAML)
+GET /api/config-prepare # JSONata transform for config preparation
+GET /api/docs-manifest # Documentation manifest (YAML)
+GET /api/docs/{path} # Documentation markdown fragments
+```
+
+Example usage:
+```bash
+# Generate configuration via API
+curl -X POST http://localhost:8080/api/generate/docker-compose/1.1 \
+ -H "Content-Type: application/json" \
+ -d @config.json \
+ --output deployment.zip
+
+# Fetch dialog flow configuration
+curl http://localhost:8080/api/dialog-flow
+
+# Fetch a documentation fragment
+curl http://localhost:8080/api/docs/platform/docker-compose.md
+```
+
+## Output Structure
+
+### Docker Compose Output
+
+The generated ZIP file contains:
+```
+docker-compose.yaml # Main deployment file
+trustgraph/config.json # TrustGraph configuration
+grafana/ # Grafana dashboards and provisioning
+prometheus/ # Prometheus configuration
+```
+
+### Kubernetes Output
+
+The generated ZIP file contains:
+```
+resources.yaml # All Kubernetes resources in a single file
+```
+
+## Development
+
+To extend or modify the configurator:
+
+1. Templates are in `trustgraph_configurator/templates//`
+2. Add new platforms by creating appropriate Jsonnet templates
+3. Update `templates/index.json` for new versions
+4. Resources (dashboards, configs) go in `trustgraph_configurator/resources//`
+5. Dialog flow resources are in `trustgraph_configurator/resources/dialog/`:
+ - `trustgraph-flow.yaml` - Dialog flow state machine
+ - `trustgraph-output.jsonata` - State-to-config transform
+ - `trustgraph-docs.yaml` - Documentation manifest
+ - `docs/` - Markdown documentation fragments
+
+## Error Handling
+
+The configurator includes error handling for:
+- Missing or invalid templates
+- Malformed input JSON
+- File resolution failures
+- Platform-specific generation errors
+
+Errors are logged with appropriate context for debugging.
diff --git a/ai-context/trustgraph-templates/TEST_STRATEGY.md b/ai-context/trustgraph-templates/TEST_STRATEGY.md
new file mode 100644
index 00000000..5896c3d5
--- /dev/null
+++ b/ai-context/trustgraph-templates/TEST_STRATEGY.md
@@ -0,0 +1,320 @@
+# TrustGraph Configurator Test Strategy
+
+## Executive Summary
+
+The TrustGraph configurator's primary risk is shipping broken configurations that fail to deploy properly. Unlike application crashes (which are obvious and easily fixable), configuration errors can cause silent failures, deployment issues, or runtime problems that are difficult to debug and significantly impact user experience.
+
+This test strategy prioritizes **configuration correctness** and **deployability** over code coverage metrics.
+
+## Risk Assessment
+
+### High Impact Risks
+1. **Template Syntax Errors**: Jsonnet compilation failures that prevent configuration generation
+2. **Invalid Configuration Structure**: Generated configs that don't match expected schemas
+3. **Missing Dependencies**: Component configurations that reference non-existent services
+4. **Platform-Specific Issues**: Configurations that work on one platform but fail on another
+5. **Version Incompatibilities**: Template changes that break existing user configurations
+
+### Medium Impact Risks
+1. **Performance Issues**: Inefficient template processing
+2. **Resource Misconfigurations**: Incorrect memory/CPU limits
+3. **Security Misconfigurations**: Missing security settings or exposed secrets
+
+### Low Impact Risks
+1. **CLI Argument Parsing**: Easily debuggable and obvious failures
+2. **Logging Issues**: Don't affect configuration correctness
+3. **API Error Handling**: Obvious failures with clear error messages
+
+## Testing Strategy
+
+### 1. Configuration Validation Tests (Critical)
+
+#### Template Compilation Tests
+- **Objective**: Ensure all Jsonnet templates compile without errors
+- **Scope**: All templates in all versions (0.21, 0.22, 0.23, 1.0, 1.1)
+- **Implementation**:
+ ```bash
+ # Test all platform configurations for each template version
+ for version in 0.21 0.22 0.23 1.0 1.1; do
+ for platform in docker-compose podman-compose minikube-k8s gcp-k8s aks-k8s eks-k8s scw-k8s; do
+ ./scripts/tg-build-deployment --template $version --platform $platform --input test-configs/minimal.json -O > /dev/null
+ done
+ done
+ ```
+
+#### Schema Validation Tests
+- **Objective**: Validate generated configurations against expected schemas
+- **Docker Compose**: Validate against docker-compose schema
+- **Kubernetes**: Validate against Kubernetes resource schemas
+- **TrustGraph Config**: Validate against TrustGraph configuration schema
+
+#### Component Integration Tests
+- **Objective**: Ensure component dependencies are correctly resolved
+- **Test Cases**:
+ - LLM + Embeddings combinations (OpenAI + HF, Ollama + Ollama, etc.)
+ - RAG pipelines with all storage backends
+ - Graph databases with different embedding stores
+ - OCR + document processing chains
+
+### 2. Platform-Specific Deployment Tests (Critical)
+
+#### Docker Compose Validation
+- **Objective**: Ensure generated docker-compose.yaml files are valid and deployable
+- **Test Environment**: Local Docker daemon
+- **Test Process**:
+ ```bash
+ # Generate configuration
+ ./scripts/tg-build-deployment --template 1.1 --platform docker-compose --input test-config.json -R > docker-compose.yaml
+
+ # Validate syntax
+ docker-compose config -q
+
+ # Test deployment (dry-run)
+ docker-compose up --no-start
+
+ # Test actual deployment with minimal config
+ docker-compose up -d
+ timeout 60 docker-compose ps
+ docker-compose down
+ ```
+
+#### Kubernetes Validation
+- **Objective**: Ensure generated Kubernetes manifests are valid and deployable
+- **Test Environment**: Minikube or kind cluster
+- **Test Process**:
+ ```bash
+ # Generate configuration
+ ./scripts/tg-build-deployment --template 1.1 --platform gcp-k8s --input test-config.json -R > resources.yaml
+
+ # Validate syntax
+ kubectl apply --dry-run=client -f resources.yaml
+
+ # Test deployment
+ kubectl apply -f resources.yaml
+ kubectl wait --for=condition=Ready pod --all --timeout=300s
+ kubectl delete -f resources.yaml
+ ```
+
+### 3. Configuration Matrix Tests (High Priority)
+
+#### Test Configuration Profiles
+Create comprehensive test configurations covering:
+
+1. **Minimal Configuration**
+ ```json
+ {
+ "llm": {"engine": "openai", "model": "gpt-3.5-turbo"},
+ "embeddings": {"engine": "hf", "model": "sentence-transformers/all-MiniLM-L6-v2"}
+ }
+ ```
+
+2. **Complex RAG Configuration**
+ ```json
+ {
+ "llm": {"engine": "openai"},
+ "embeddings": {"engine": "hf"},
+ "vector_store": {"engine": "qdrant"},
+ "graph_store": {"engine": "neo4j"},
+ "document_store": {"engine": "cassandra"},
+ "rag": {"enabled": true, "chunking": "recursive"}
+ }
+ ```
+
+3. **Multi-Service Configuration**
+ ```json
+ {
+ "llm": [
+ {"engine": "openai", "model": "gpt-4"},
+ {"engine": "ollama", "model": "llama2"}
+ ],
+ "embeddings": {"engine": "fastembed"},
+ "vector_store": {"engine": "milvus"},
+ "monitoring": {"grafana": true, "prometheus": true}
+ }
+ ```
+
+4. **Cloud-Specific Configurations**
+ - AWS Bedrock + EKS
+ - Azure OpenAI + AKS
+ - Google Vertex AI + GKE
+
+#### Cross-Platform Matrix
+Test each configuration profile against all supported platforms:
+- docker-compose
+- podman-compose
+- minikube-k8s
+- gcp-k8s
+- aks-k8s
+- eks-k8s
+- scw-k8s
+
+### 4. Version Compatibility Tests (Medium Priority)
+
+#### Backward Compatibility
+- **Objective**: Ensure new template versions don't break existing configurations
+- **Process**:
+ 1. Collect real-world configuration examples
+ 2. Test against new template versions
+ 3. Validate that outputs remain functionally equivalent
+
+#### Forward Compatibility
+- **Objective**: Ensure template changes don't break when new features are added
+- **Process**:
+ 1. Test configurations with unknown/future parameters
+ 2. Verify graceful degradation or clear error messages
+
+### 5. Integration Tests (Medium Priority)
+
+#### End-to-End Deployment Tests
+- **Objective**: Verify complete deployment workflows
+- **Test Cases**:
+ - Generate config → Deploy → Verify services start → Run basic functionality test
+ - Test with monitoring enabled (Grafana/Prometheus accessible)
+ - Test with different storage backends
+
+#### Resource Generation Tests
+- **Objective**: Verify auxiliary resources are correctly generated
+- **Test Cases**:
+ - Grafana dashboards are valid JSON
+ - Prometheus config is valid YAML
+ - All required secrets/configmaps are created
+
+### 6. Regression Tests (High Priority)
+
+#### Template Change Validation
+- **Objective**: Detect when template changes break existing configurations
+- **Process**:
+ 1. Maintain golden configurations for each version
+ 2. Generate outputs before and after changes
+ 3. Compare outputs for breaking changes
+ 4. Flag any structural differences for manual review
+
+#### Component Regression Tests
+- **Objective**: Ensure component updates don't break integrations
+- **Test Cases**:
+ - Component parameter changes
+ - New component additions
+ - Component deprecations
+
+## Test Implementation Framework
+
+### Test Infrastructure
+
+#### Test Configuration Repository
+```
+tests/
+├── configs/
+│ ├── minimal.json
+│ ├── complex-rag.json
+│ ├── multi-service.json
+│ └── cloud-specific/
+├── golden/
+│ ├── 1.1/
+│ │ ├── docker-compose/
+│ │ └── k8s/
+│ └── 1.0/
+├── schemas/
+│ ├── docker-compose.json
+│ ├── k8s-resources.json
+│ └── trustgraph-config.json
+└── scripts/
+ ├── test-all-platforms.sh
+ ├── validate-schemas.sh
+ └── deploy-test.sh
+```
+
+#### Automated Test Pipeline
+```bash
+#!/bin/bash
+# test-all-platforms.sh
+
+set -e
+
+VERSIONS="0.21 0.22 0.23 1.0 1.1"
+PLATFORMS="docker-compose podman-compose minikube-k8s gcp-k8s aks-k8s eks-k8s scw-k8s"
+CONFIGS="minimal.json complex-rag.json multi-service.json"
+
+echo "Testing configuration generation..."
+for version in $VERSIONS; do
+ for platform in $PLATFORMS; do
+ for config in $CONFIGS; do
+ echo "Testing $version/$platform/$config"
+
+ # Test TrustGraph config generation
+ ./scripts/tg-build-deployment --template $version --platform $platform \
+ --input tests/configs/$config -O > /tmp/tg-config.json
+
+ # Validate TrustGraph config
+ ./tests/scripts/validate-tg-config.sh /tmp/tg-config.json
+
+ # Test resource generation
+ ./scripts/tg-build-deployment --template $version --platform $platform \
+ --input tests/configs/$config -R > /tmp/resources.yaml
+
+ # Validate resources
+ ./tests/scripts/validate-resources.sh /tmp/resources.yaml $platform
+
+ echo "✓ $version/$platform/$config passed"
+ done
+ done
+done
+```
+
+### Test Execution Strategy
+
+#### Continuous Integration
+1. **Pre-commit**: Template syntax validation
+2. **Pull Request**: Full configuration matrix tests
+3. **Release**: Deployment tests + regression tests
+
+#### Performance Testing
+- Template processing time benchmarks
+- Memory usage during large configuration generation
+- Concurrent API request handling
+
+#### Security Testing
+- Validate no secrets are exposed in generated configs
+- Test secret injection mechanisms
+- Validate security-related component configurations
+
+## Test Metrics and Success Criteria
+
+### Primary Success Metrics
+1. **Configuration Validity**: 100% of generated configurations must be syntactically valid
+2. **Deployment Success**: 95% of generated configurations must deploy successfully
+3. **Regression Prevention**: Zero breaking changes to existing configurations without explicit versioning
+
+### Secondary Success Metrics
+1. **Test Coverage**: All template versions × all platforms × all major components
+2. **Performance**: Configuration generation < 10 seconds for complex configs
+3. **Documentation**: All test failures must have clear error messages and remediation steps
+
+## Maintenance and Updates
+
+### Test Maintenance
+- Update test configurations when new features are added
+- Refresh golden configurations when intentional changes are made
+- Review and update schemas when component APIs change
+
+### Test Infrastructure Updates
+- Add new platforms when supported
+- Update validation tools when dependencies change
+- Maintain test environments (Docker, Kubernetes clusters)
+
+## Risk Mitigation
+
+### High-Risk Changes
+Any changes to the following require full test suite execution:
+- Jsonnet template files
+- Component definitions
+- Engine implementations
+- Version configuration changes
+
+### Emergency Procedures
+- Rollback plan for broken template releases
+- Hotfix process for critical configuration issues
+- Communication plan for notifying users of breaking changes
+
+## Conclusion
+
+This test strategy prioritizes what matters most: ensuring users receive working, deployable configurations. By focusing on configuration correctness over code coverage, we can prevent the high-impact failures that would significantly affect user experience while maintaining development velocity for lower-risk changes.
\ No newline at end of file
diff --git a/ai-context/trustgraph-templates/config-standalone-pulsar.jsonnet b/ai-context/trustgraph-templates/config-standalone-pulsar.jsonnet
new file mode 100644
index 00000000..0ab4d03a
--- /dev/null
+++ b/ai-context/trustgraph-templates/config-standalone-pulsar.jsonnet
@@ -0,0 +1,45 @@
+[
+ {
+ "name": "triple-store-cassandra",
+ "parameters": {}
+ },
+ {
+ "name": "object-store-cassandra",
+ "parameters": {}
+ },
+ {
+ "name": "pulsar-standalone",
+ "parameters": {}
+ },
+ {
+ "name": "vector-store-qdrant",
+ "parameters": {}
+ },
+ {
+ "name": "grafana",
+ "parameters": {}
+ },
+ {
+ "name": "trustgraph-base",
+ "parameters": {}
+ },
+ {
+ "name": "override-recursive-chunker",
+ "parameters": {
+ "chunk-size": 2000,
+ "chunk-overlap": 100
+ }
+ },
+ {
+ "name": "embeddings-fastembed",
+ "parameters": {
+ "embeddings-model": "sentence-transformers/all-MiniLM-L6-v2"
+ }
+ },
+ {
+ "name": "vertexai",
+ "parameters": {
+ "max-output-tokens": 8192
+ }
+ }
+]
diff --git a/ai-context/trustgraph-templates/docs/garage-deployment.md b/ai-context/trustgraph-templates/docs/garage-deployment.md
new file mode 100644
index 00000000..d00999bc
--- /dev/null
+++ b/ai-context/trustgraph-templates/docs/garage-deployment.md
@@ -0,0 +1,244 @@
+# Garage S3-Compatible Object Storage
+
+## Overview
+
+Garage is a lightweight, self-hosted S3-compatible object storage system used as an alternative to Ceph for storing documents and other objects in the TrustGraph system. It provides a simpler deployment model while maintaining S3 API compatibility.
+
+## Features
+
+- **S3-Compatible API**: Full compatibility with AWS S3 SDK and CLI tools
+- **Lightweight**: Minimal resource requirements compared to Ceph
+- **Simple Deployment**: Single container with no complex dependencies
+- **Distributed**: Supports multi-node clusters (though configured for single-node by default)
+
+## Architecture
+
+### Components
+
+1. **Garage Daemon** (`garage`)
+ - Main storage service container
+ - Provides S3 API on port 3900
+ - Admin API on port 3903
+ - RPC communication on port 3901
+
+2. **Init Container** (`garage-init`)
+ - Runs once to initialize the cluster
+ - Configures cluster layout
+ - Creates S3 access credentials
+ - Uses remote RPC to communicate with daemon (no shared volumes)
+
+### Storage
+
+Garage uses two separate volumes:
+
+- **Metadata Volume** (`garage-meta`): Stores cluster metadata and LMDB database
+- **Data Volume** (`garage-data`): Stores actual object data
+
+## Configuration Parameters
+
+All configuration is done via Jsonnet parameters with the `garage-` prefix:
+
+### S3 Credentials
+
+```jsonnet
+"garage-access-key":: "GK000000000000000000000001",
+"garage-secret-key":: "b171f00be9be4c32c734f4c05fe64c527a8ab5eb823b376cfa8c2531f70fc427",
+```
+
+**Format Requirements:**
+- **Access Key ID**: Must start with `GK` followed by exactly 24 hex characters (0-9, a-f)
+- **Secret Key**: Must be exactly 64 hex characters
+
+**Generate Secure Credentials:**
+
+```bash
+# Generate Access Key ID (GK + 24 hex chars)
+echo "GK$(openssl rand -hex 12)"
+
+# Generate Secret Key (64 hex chars)
+openssl rand -hex 32
+```
+
+### Cluster Configuration
+
+```jsonnet
+"garage-rpc-secret":: "bbba746a9e289bad64a9e7a36a4299dac8d6e0b8cc2a6c2937fe756df4492008",
+"garage-admin-token":: "batts-rockhearted-unpartially",
+"garage-region":: "garage",
+"garage-replication-factor":: "1",
+```
+
+- **rpc-secret**: 64 hex characters for node-to-node RPC authentication
+- **admin-token**: Bearer token for Admin API access
+- **region**: S3 region name
+- **replication-factor**: Number of data replicas (set to 1 for single-node, 3+ for production)
+
+### Storage Volumes
+
+```jsonnet
+"garage-meta-size":: "5G",
+"garage-data-size":: "100G",
+```
+
+Both values can be overridden using the `.with()` function:
+
+```jsonnet
+.with("meta-size", "10G")
+.with("data-size", "500G")
+```
+
+## Integration with Librarian
+
+The librarian component automatically connects to Garage for object storage:
+
+```jsonnet
+"--object-store-endpoint", url.object_store,
+"--object-store-access-key", $["garage-access-key"],
+"--object-store-secret-key", $["garage-secret-key"],
+```
+
+The object store endpoint is defined in `values/url.jsonnet`:
+
+```jsonnet
+object_store: "http://garage:3900",
+```
+
+## Testing Garage with S3 Clients
+
+### Using AWS CLI
+
+```bash
+# Set credentials
+export AWS_ACCESS_KEY_ID="GK000000000000000000000001"
+export AWS_SECRET_ACCESS_KEY="b171f00be9be4c32c734f4c05fe64c527a8ab5eb823b376cfa8c2531f70fc427"
+export AWS_ENDPOINT_URL="http://localhost:3900"
+export AWS_DEFAULT_REGION="garage"
+
+# Create a bucket
+aws s3 mb s3://test-bucket
+
+# Upload a file
+echo "Hello from Garage!" > test.txt
+aws s3 cp test.txt s3://test-bucket/
+
+# List files
+aws s3 ls s3://test-bucket/
+
+# Download a file
+aws s3 cp s3://test-bucket/test.txt downloaded.txt
+
+# Verify
+cat downloaded.txt
+```
+
+### Using s3cmd
+
+Create configuration file `~/.s3cfg`:
+
+```ini
+[default]
+access_key = GK000000000000000000000001
+secret_key = b171f00be9be4c32c734f4c05fe64c527a8ab5eb823b376cfa8c2531f70fc427
+host_base = localhost:3900
+host_bucket = localhost:3900
+use_https = False
+```
+
+Then use s3cmd:
+
+```bash
+# List buckets
+s3cmd ls
+
+# Create bucket
+s3cmd mb s3://my-bucket
+
+# Upload file
+s3cmd put file.txt s3://my-bucket/
+
+# Download file
+s3cmd get s3://my-bucket/file.txt
+```
+
+## Deployment Details
+
+### Initialization Process
+
+The init container performs these steps:
+
+1. **Wait for Garage daemon** - Polls `/health` endpoint until daemon is ready
+2. **Get Node ID** - Queries `/v2/GetNodeInfo?node=self` via Admin API
+3. **Configure Layout** - Assigns node to cluster with specified capacity via RPC
+4. **Apply Layout** - Activates the cluster layout
+5. **Import Credentials** - Creates S3 access key with provided credentials
+6. **Grant Permissions** - Enables bucket creation for the key
+
+All operations are **idempotent** - the init container can be restarted safely and will skip already-configured items.
+
+### Network Architecture
+
+- **S3 API**: Port 3900 (HTTP)
+- **RPC**: Port 3901 (internal cluster communication)
+- **Web UI**: Port 3902 (optional web interface)
+- **Admin API**: Port 3903 (cluster management)
+- **K2V API**: Port 3904 (key-value store)
+
+### No Shared Volumes
+
+The init container communicates with the Garage daemon entirely over the network:
+
+- Admin API (HTTP) for status queries
+- RPC (via garage CLI `-h` and `-s` flags) for cluster management
+
+This design works across all orchestrators (Kubernetes, Docker Compose) without requiring shared volume mounts.
+
+## Version Information
+
+Current deployment uses **Garage v2.1.0**
+
+- Image: `docker.io/dxflrs/garage:v2.1.0`
+- Documentation: https://garagehq.deuxfleurs.fr/documentation/
+
+## Troubleshooting
+
+### Init Container Fails
+
+Check logs: `podman logs `
+
+Common issues:
+- **403 Forbidden**: Check `garage-admin-token` is correct
+- **Invalid layout version**: Cluster already initialized, init will retry
+- **Node ID null**: Admin API not responding, check daemon logs
+
+### S3 Access Denied
+
+Verify credentials format:
+- Access Key ID must start with `GK` + 24 hex chars
+- Secret Key must be 64 hex chars
+- Credentials must match what was imported during init
+
+### Check Garage Status
+
+```bash
+# Query cluster status
+curl -H "Authorization: Bearer " \
+ http://localhost:3903/v2/GetClusterStatus
+
+# Check health
+curl http://localhost:3903/health
+```
+
+## Production Considerations
+
+1. **Generate Secure Credentials**: Never use default credentials in production
+2. **Set Replication Factor**: Use 3+ for redundancy in multi-node clusters
+3. **Increase Storage Size**: Adjust `garage-data-size` based on expected usage
+4. **Secure Admin Token**: Use a strong random token for `garage-admin-token`
+5. **Monitor Storage**: Watch disk usage on data volume
+6. **Backup Metadata**: The metadata volume contains critical cluster state
+
+## References
+
+- [Garage Documentation](https://garagehq.deuxfleurs.fr/documentation/)
+- [Garage Admin API v2](https://garagehq.deuxfleurs.fr/api/garage-admin-v2.json)
+- [Quick Start Guide](https://garagehq.deuxfleurs.fr/documentation/quick-start/)
diff --git a/ai-context/trustgraph-templates/docs/tech-specs/tests.md b/ai-context/trustgraph-templates/docs/tech-specs/tests.md
new file mode 100644
index 00000000..d695486a
--- /dev/null
+++ b/ai-context/trustgraph-templates/docs/tech-specs/tests.md
@@ -0,0 +1,416 @@
+# Test Specification
+
+## Test Categories
+
+### Unit Tests
+Test Python modules in isolation:
+- **Generator** - Jsonnet template processing, import callbacks
+- **Packager** - Zip file creation, configuration assembly
+- **API** - Template listing, version resolution
+- **CLI** - Argument parsing, error handling, exit codes
+
+### Integration Tests
+Test full CLI workflow:
+- Template compilation across version/platform/config matrix
+- Output file generation (TrustGraph config + platform resources)
+- Error propagation and reporting
+
+### Validation Tests
+Verify correctness of generated outputs:
+- Syntax validation (JSON/YAML parsing)
+- Schema validation (structure compliance)
+- Semantic validation (cross-references, consistency)
+- Regression testing (golden files)
+
+## Test Matrix
+
+**Dimensions:**
+- Versions: 1.6, 1.7, 1.8
+- Platforms: docker-compose, podman-compose, minikube-k8s, gcp-k8s, aks-k8s, eks-k8s, scw-k8s, ovh-k8s
+- Configs: minimal.json, complex-rag.json, multi-service.json, cloud-aws.json
+
+**Total combinations:** 3 versions × 8 platforms × 4 configs = 96 combinations per output type = 192 tests
+
+## Validation Approaches
+
+### Syntax Validation
+- **JSON**: Parse with `json.loads()`, check no exceptions
+- **YAML**: Parse with `yaml.safe_load()`, check no exceptions
+- **Docker Compose**: Validate with `docker-compose config`
+- **Kubernetes**: Validate with `kubectl apply --dry-run=client`
+
+### Schema Validation
+- **TrustGraph Config**: Define JSON schema, validate with `jsonschema`
+ - Required fields: services, modules, parameters
+ - Type checking for all configuration values
+ - Enum validation for fixed sets (llm providers, platforms)
+
+- **Kubernetes**: Check required fields
+ - apiVersion, kind, metadata present
+ - metadata.name, metadata.namespace defined
+ - spec structure matches resource kind
+
+- **Docker Compose**: Check required fields
+ - services defined with image/build
+ - Valid port mappings
+ - Valid volume definitions
+
+### Semantic Validation
+
+#### Kubernetes Resources
+- **Label/Selector matching**: Deployment selectors match pod labels
+- **Volume references**: volumeMounts reference defined volumes
+- **Service targeting**: Service selectors match deployment labels
+- **Port consistency**: containerPort matches service targetPort
+- **ConfigMap/Secret references**: Referenced resources exist in manifest
+
+#### Docker Compose
+- **Service dependencies**: depends_on references valid services
+- **Volume references**: Volume names in bind mounts are defined
+- **Network references**: Networks used by services are defined
+- **Port conflicts**: No duplicate host port bindings
+- **Environment variable references**: ${VAR} expansions are resolvable
+
+#### TrustGraph Config
+- **Service references**: Configured services reference valid modules
+- **Parameter validation**: Module parameters match schema
+- **Storage consistency**: Graph/object/vector stores configured correctly
+- **LLM configuration**: Valid model IDs, API configurations
+
+### Golden File Testing
+Store reference outputs for each test case:
+- **Location**: `tests/golden/{version}/{platform}/{config}/`
+- **Files**:
+ - `tg-config.json` - Reference TrustGraph configuration
+ - `resources.yaml` - Reference platform resources
+- **Comparison**: Use `pytest-golden` for automatic diff generation
+- **Updates**: Explicit flag to regenerate golden files when intentional changes occur
+
+## Test Cases
+
+### Compilation Tests
+For each (version, platform, config) combination:
+1. Run `tg-build-deployment -t {version} -p {platform} -i {config} -O`
+2. Assert exit code = 0
+3. Assert stdout contains valid JSON
+4. Parse and validate TrustGraph config structure
+
+5. Run `tg-build-deployment -t {version} -p {platform} -i {config} -R`
+6. Assert exit code = 0
+7. Assert stdout contains valid YAML
+8. Parse and validate resource manifest structure
+
+### Error Handling Tests
+- **Invalid config**: Malformed JSON input → exit code 1, error to stderr
+- **Missing file**: Non-existent config file → exit code 1, error to stderr
+- **Invalid template**: Non-existent version → exit code 1, error to stderr
+- **Invalid platform**: Non-existent platform → exit code 1, error to stderr
+- **Template errors**: Jsonnet compilation errors → exit code 1, error to stderr
+
+### CLI Interface Tests
+- **Argument parsing**: Valid/invalid argument combinations
+- **Help output**: `-h` flag displays usage
+- **Version display**: Version flag shows package version
+- **Output modes**: `-O` and `-R` flags produce correct output types
+- **Default values**: Missing optional args use documented defaults
+
+### Module Unit Tests
+
+#### Generator
+- `process(config)` - Valid jsonnet → parsed JSON
+- `process(config)` - Invalid jsonnet → raises exception
+- Import callback mechanism works correctly
+- Template loading from package resources
+
+#### Packager
+- `write(config, output)` - Creates valid zip file
+- `write_tg_config(config)` - Outputs TrustGraph config to stdout
+- `write_resources(config)` - Outputs platform resources to stdout
+- Template version resolution (--latest, --latest-stable)
+- Platform-specific template selection
+
+## Test Execution Methods
+
+### Direct Function Call (Primary Method)
+Most tests call the Python entry point function directly rather than invoking the subprocess:
+
+```python
+from trustgraph_configurator import run
+import sys
+import json
+
+def test_basic_compilation(monkeypatch, capsys):
+ """Test compilation by calling run() directly"""
+ # Mock sys.argv with CLI arguments
+ monkeypatch.setattr(sys, 'argv', [
+ 'tg-build-deployment',
+ '-t', '1.8',
+ '-p', 'docker-compose',
+ '-i', 'tests/configs/minimal.json',
+ '-O'
+ ])
+
+ # Call the entry point directly
+ run.run()
+
+ # Capture and validate output
+ captured = capsys.readouterr()
+ config = json.loads(captured.out)
+ assert 'services' in config
+```
+
+**Advantages:**
+- **Fast**: No subprocess overhead (100x+ faster)
+- **Easy stdout/stderr capture**: Use pytest's `capsys` fixture
+- **Easy mocking**: Use `monkeypatch` for arguments, environment, file system
+- **Better debugging**: Direct code path, breakpoints work naturally
+- **Exit code testing**: Catch `SystemExit` exception to verify exit codes
+
+```python
+def test_error_handling(monkeypatch):
+ """Test that errors exit with code 1"""
+ monkeypatch.setattr(sys, 'argv', [
+ 'tg-build-deployment',
+ '-i', 'nonexistent.json'
+ ])
+
+ with pytest.raises(SystemExit) as exc_info:
+ run.run()
+
+ assert exc_info.value.code == 1
+```
+
+### Subprocess Invocation (Smoke Tests)
+A small number of tests (1-2) should invoke the actual CLI executable to verify installation:
+
+```python
+import subprocess
+
+def test_cli_executable_installed():
+ """Verify the installed CLI entry point works"""
+ result = subprocess.run(
+ ['tg-build-deployment', '--help'],
+ capture_output=True,
+ text=True
+ )
+ assert result.returncode == 0
+ assert 'usage:' in result.stdout
+
+def test_cli_version_command():
+ """Verify version command works from CLI"""
+ result = subprocess.run(
+ ['tg-build-deployment', '--version'],
+ capture_output=True,
+ text=True
+ )
+ assert result.returncode == 0
+```
+
+**Purpose:**
+- Verify `pyproject.toml` entry point configuration is correct
+- Verify CLI is accessible in PATH after installation
+- End-to-end smoke test
+
+**Limitations:**
+- Slower (subprocess overhead)
+- Harder to mock/patch
+- Less detailed error information
+
+### Fixture for Direct Execution
+Create a reusable fixture for calling configurator:
+
+```python
+# tests/conftest.py
+import pytest
+import sys
+from io import StringIO
+
+@pytest.fixture
+def run_configurator(monkeypatch, capsys):
+ """Fixture to run configurator with given arguments"""
+ def _run(args):
+ """
+ Run configurator with args list.
+ Returns (stdout, stderr, exit_code)
+ """
+ from trustgraph_configurator import run
+
+ monkeypatch.setattr(sys, 'argv', ['tg-build-deployment'] + args)
+
+ exit_code = 0
+ try:
+ run.run()
+ except SystemExit as e:
+ exit_code = e.code or 0
+
+ captured = capsys.readouterr()
+ return captured.out, captured.err, exit_code
+
+ return _run
+```
+
+**Usage:**
+```python
+def test_with_fixture(run_configurator):
+ stdout, stderr, code = run_configurator([
+ '-t', '1.8',
+ '-p', 'docker-compose',
+ '-i', 'tests/configs/minimal.json',
+ '-O'
+ ])
+ assert code == 0
+ assert json.loads(stdout)
+```
+
+## Test Infrastructure
+
+### Pytest Configuration
+```toml
+[tool.pytest.ini_options]
+testpaths = ["tests"]
+python_files = ["test_*.py"]
+python_classes = ["Test*"]
+python_functions = ["test_*"]
+addopts = [
+ "-v",
+ "--strict-markers",
+ "--cov=trustgraph_configurator",
+ "--cov-report=term-missing",
+ "--cov-report=html",
+]
+markers = [
+ "unit: Unit tests",
+ "integration: Integration tests",
+ "validation: Output validation tests",
+ "slow: Slow-running tests",
+]
+```
+
+### Fixtures (`tests/conftest.py`)
+- `test_config_dir` - Path to tests/configs/
+- `test_configs` - Dict of loaded test configurations
+- `temp_output_dir` - Temporary directory for test outputs
+- `run_configurator` - Function to execute configurator CLI
+- `golden_dir` - Path to golden file directory for test case
+
+### Parametrization
+Use `pytest.mark.parametrize` for matrix testing:
+```python
+@pytest.mark.parametrize("version", ["1.6", "1.7", "1.8"])
+@pytest.mark.parametrize("platform", ["docker-compose", "minikube-k8s", ...])
+@pytest.mark.parametrize("config", ["minimal.json", "complex-rag.json", ...])
+def test_compilation(version, platform, config, run_configurator):
+ ...
+```
+
+### Parallel Execution
+Use pytest-xdist for parallel test execution:
+```bash
+pytest -n auto # Use all CPU cores
+```
+
+## Test File Organization
+
+```
+tests/
+├── conftest.py # Shared fixtures
+├── unit/
+│ ├── test_generator.py
+│ ├── test_packager.py
+│ ├── test_api.py
+│ └── test_run.py
+├── integration/
+│ ├── test_compilation.py # Template compilation matrix
+│ ├── test_cli.py # CLI interface tests
+│ └── test_errors.py # Error handling tests
+├── validation/
+│ ├── test_syntax.py # Syntax validation
+│ ├── test_schema.py # Schema validation
+│ ├── test_semantics_k8s.py
+│ ├── test_semantics_docker.py
+│ └── test_semantics_tg.py
+├── configs/ # Test input configs (existing)
+├── schemas/ # JSON schemas for validation
+│ ├── trustgraph-config.schema.json
+│ ├── kubernetes-deployment.schema.json
+│ └── docker-compose.schema.json
+├── golden/ # Reference outputs
+│ └── {version}/{platform}/{config}/
+│ ├── tg-config.json
+│ └── resources.yaml
+└── validators/ # Validation helper modules
+ ├── kubernetes.py
+ ├── docker_compose.py
+ └── trustgraph.py
+```
+
+## Development Dependencies
+
+Add to pyproject.toml:
+```toml
+[project.optional-dependencies]
+dev = [
+ "pytest>=7.0",
+ "pytest-xdist>=3.0", # Parallel execution
+ "pytest-cov>=4.0", # Coverage reporting
+ "pytest-golden>=0.2", # Golden file testing
+ "jsonschema>=4.0", # Schema validation
+ "pyyaml>=6.0", # Already in main deps
+]
+```
+
+Install for development:
+```bash
+pip install -e .[dev]
+```
+
+## CI/CD Integration
+
+Update `.github/workflows/pull-request.yaml`:
+```yaml
+- name: Install dependencies
+ run: |
+ python3 -m venv env
+ . env/bin/activate
+ pip install -e .[dev]
+
+- name: Run tests
+ run: |
+ . env/bin/activate
+ pytest -n auto --cov --cov-report=xml
+
+- name: Upload coverage
+ uses: codecov/codecov-action@v3
+ with:
+ file: ./coverage.xml
+```
+
+## Running Tests
+
+```bash
+# All tests
+pytest
+
+# Specific category
+pytest tests/unit/
+pytest tests/integration/
+pytest -m validation
+
+# Specific test file
+pytest tests/unit/test_generator.py
+
+# Parallel execution
+pytest -n auto
+
+# With coverage
+pytest --cov=trustgraph_configurator --cov-report=html
+
+# Update golden files
+pytest --update-golden
+
+# Verbose output
+pytest -v
+
+# Stop on first failure
+pytest -x
+```
diff --git a/ai-context/trustgraph-templates/docs/templates-structure.md b/ai-context/trustgraph-templates/docs/templates-structure.md
new file mode 100644
index 00000000..95bb04b8
--- /dev/null
+++ b/ai-context/trustgraph-templates/docs/templates-structure.md
@@ -0,0 +1,188 @@
+# How TrustGraph Templates Are Structured
+
+## Overview
+
+The TrustGraph template system is a Jsonnet-based configuration framework that generates deployment configurations for multiple platforms (Docker Compose, Kubernetes, etc.) from a single JSON configuration file. The system uses a component-based architecture with abstraction layers for different deployment targets.
+
+## Directory Structure
+
+```
+trustgraph_configurator/
+├── packager.py # Main entry point
+├── generator.py # Jsonnet processor wrapper
+├── templates/
+│ └── 1.3/ # Template version
+│ ├── components.jsonnet # Component registry
+│ ├── config-to-docker-compose.jsonnet # Docker Compose generator
+│ ├── config-to-tg-configuration.jsonnet # TrustGraph config extractor
+│ ├── components/ # Component definitions
+│ ├── engine/ # Platform-specific engines
+│ ├── util/ # Utility functions
+│ ├── prompts/ # LLM prompt templates
+│ └── values/ # Shared configuration values
+└── resources/
+ └── 1.3/ # Static resource files
+ ├── grafana/ # Grafana dashboards/configs
+ └── prometheus/ # Prometheus configs
+```
+
+## Core Concepts
+
+### 1. Components
+
+Components are the building blocks of the system. Each component:
+- Defines configuration parameters with defaults
+- Implements a `create` function that generates platform-specific resources
+- Can compose with other components through Jsonnet's object composition (`+`)
+- Lives in `components/` directory
+
+Example component structure (simplified `ollama.jsonnet`):
+```jsonnet
+{
+ // Parameter with default value
+ "ollama-model":: "gemma2:9b",
+
+ // Service definition
+ "text-completion" +: {
+ create:: function(engine)
+ // Use engine abstraction to create resources
+ local container = engine.container("text-completion")
+ .with_image(...)
+ .with_command([...]);
+
+ engine.resources([container, ...])
+ },
+
+ // Custom parameter setter
+ with:: function(key, value)
+ self + { ["ollama-" + key]:: value }
+}
+```
+
+### 2. Engines
+
+Engines provide platform-specific implementations for resource creation. Each engine implements:
+- `container()` - Create container definitions
+- `service()` - Create service definitions
+- `volume()` - Create volume definitions
+- `resources()` - Aggregate resources for output
+
+The engine abstraction allows components to be platform-agnostic. Available engines:
+- `docker-compose.jsonnet` - Docker Compose format
+- `noop.jsonnet` - No-op engine for configuration extraction only
+- Kubernetes engines (various cloud providers)
+
+### 3. Configuration Flow
+
+```
+config.json → decode → patterns → engine.create() → resources → output
+```
+
+1. **Input**: `config.json` contains a list of components to enable:
+```json
+[
+ {"name": "trustgraph-base", "parameters": {}},
+ {"name": "ollama", "parameters": {"model": "mixtral"}}
+]
+```
+
+2. **Decode**: The `decode-config.jsonnet` utility:
+ - Loads each component from the registry
+ - Applies parameters using the `with_params` function
+ - Merges all components into a single "patterns" object
+
+3. **Engine Processing**: Platform-specific files like `config-to-docker-compose.jsonnet`:
+ - Call each component's `create(engine)` function
+ - Fold results into final resource structure
+ - Output platform-specific configuration
+
+### 4. Configuration Extraction
+
+The `config-to-tg-configuration.jsonnet` file extracts the TrustGraph runtime configuration from components. This includes:
+- Prompt templates
+- Flow definitions
+- Model token costs
+- Agent tools
+- MCP server configurations
+
+This configuration is embedded into the deployment separately from the infrastructure resources.
+
+## Processing Pipeline
+
+### Entry Point: `packager.py`
+
+The Packager class orchestrates the entire process:
+
+1. **Template Selection**: Determines version and template based on user input
+2. **Resource Generation**:
+ - For Docker Compose: Calls `config-to-docker-compose.jsonnet`
+ - For Kubernetes: Calls `config-to--k8s.jsonnet`
+3. **Configuration Generation**: Calls `config-to-tg-configuration.jsonnet` for runtime config
+4. **Packaging**: Creates ZIP file with all generated files and static resources
+
+### Jsonnet Processing: `generator.py`
+
+Simple wrapper around the Jsonnet library that:
+- Evaluates Jsonnet templates
+- Provides custom import callback for file resolution
+- Returns parsed JSON output
+
+## Component Composition
+
+Components can be composed in several ways:
+
+### 1. Base Component Extension
+Many components extend `trustgraph-base` which provides core services:
+```jsonnet
+local trustgraph = import "components/trustgraph.jsonnet";
+// Inherits all trustgraph services
+{} + trustgraph + myCustomizations
+```
+
+### 2. Field Merging
+Components use `+:` to merge with existing fields:
+```jsonnet
+"text-completion" +: {
+ // Adds to existing text-completion definition
+ create:: function(engine) ...
+}
+```
+
+### 3. Parameter Injection
+The `with` pattern allows runtime parameter injection:
+```jsonnet
+with:: function(key, value)
+ self + { [key]:: value }
+```
+
+## Hidden Fields and Configuration
+
+Jsonnet's `::` operator creates hidden fields that aren't included in JSON output by default. The template system uses this for:
+- Default values that can be overridden
+- Internal helper functions
+- Configuration that needs special extraction
+
+For example, `trustgraph-base` has a hidden `configuration::` field containing runtime config that's extracted separately by `config-to-tg-configuration.jsonnet`.
+
+## Platform Abstraction
+
+The engine pattern provides clean separation between:
+- **Component logic** - What services/containers to create
+- **Platform specifics** - How to represent them (Docker Compose YAML, K8s manifests, etc.)
+
+This allows the same component definitions to generate configurations for multiple platforms without modification.
+
+## Static Resources
+
+Files in `resources/` are copied directly to the output package. These include:
+- Grafana dashboard definitions
+- Prometheus configuration
+- Other platform-specific configs that don't need templating
+
+## Best Practices
+
+1. **Component Independence**: Components should be self-contained and not depend on specific ordering
+2. **Parameter Namespacing**: Use prefixes (e.g., `ollama-model`) to avoid conflicts
+3. **Hidden Fields for Defaults**: Use `::` for overridable defaults
+4. **Engine Abstraction**: Always use engine methods rather than creating platform-specific structures directly
+5. **Composition Over Inheritance**: Use Jsonnet's object composition (`+`) rather than complex inheritance hierarchies
\ No newline at end of file
diff --git a/ai-context/trustgraph-templates/examples/intel-battlemage-vllm.json b/ai-context/trustgraph-templates/examples/intel-battlemage-vllm.json
new file mode 100644
index 00000000..fd7f8a6c
--- /dev/null
+++ b/ai-context/trustgraph-templates/examples/intel-battlemage-vllm.json
@@ -0,0 +1,59 @@
+[
+ {
+ "name": "triple-store-cassandra",
+ "parameters": {}
+ },
+ {
+ "name": "object-store-cassandra",
+ "parameters": {}
+ },
+ {
+ "name": "pulsar",
+ "parameters": {}
+ },
+ {
+ "name": "vector-store-qdrant",
+ "parameters": {}
+ },
+ {
+ "name": "grafana",
+ "parameters": {}
+ },
+ {
+ "name": "trustgraph-base",
+ "parameters": {
+ "text-completion-concurrency": 16,
+ "text-completion-rag-concurrency": 16,
+ "prompt-concurrency": 16,
+ "prompt-rag-concurrency": 16,
+ "kg-extraction-concurrency": 16,
+ "embeddings-concurrency": 16,
+ }
+ },
+ {
+ "name": "override-recursive-chunker",
+ "parameters": {
+ "chunk-size": 2000,
+ "chunk-overlap": 100
+ }
+ },
+ {
+ "name": "embeddings-fastembed",
+ "parameters": {}
+ },
+ {
+ "name": "vllm",
+ "parameters": {
+ "max-output-tokens": 8192
+ }
+ },
+ {
+ "name": "hosting-intel-battlemage-vllm",
+ "parameters": {
+ "model": "mistralai/Mistral-Nemo-Instruct-2407",
+ "cpus": "32.0",
+ "memory": "48G",
+ "hf-token": "",
+ }
+ },
+]
diff --git a/ai-context/trustgraph-templates/pulumi/Pulumi.prod.yaml b/ai-context/trustgraph-templates/pulumi/Pulumi.prod.yaml
new file mode 100644
index 00000000..cc14f03c
--- /dev/null
+++ b/ai-context/trustgraph-templates/pulumi/Pulumi.prod.yaml
@@ -0,0 +1,14 @@
+encryptionsalt: v1:vQGk98eEeYI=:v1:tHg+f1b66tEydgA9:J1RGVNI0FssyjSXVhcKU7bfBofNFTg==
+config:
+ config-svc:artifact-name: config-svc
+ config-svc:artifact-repo: europe-west1-docker.pkg.dev/trustgraph-ai/config-svc
+ config-svc:artifact-repo-region: europe-west1
+ config-svc:cloud-run-region: europe-west1
+ config-svc:domain: app.trustgraph.ai
+ config-svc:environment: prod
+ config-svc:gcp-project: trustgraph-ai
+ config-svc:gcp-region: europe-west1
+ config-svc:hostname: config-svc.app.trustgraph.ai
+ config-svc:managed-zone: app
+ config-svc:max-scale: "1"
+ config-svc:min-scale: "0"
diff --git a/ai-context/trustgraph-templates/pulumi/Pulumi.yaml b/ai-context/trustgraph-templates/pulumi/Pulumi.yaml
new file mode 100644
index 00000000..9898a5b1
--- /dev/null
+++ b/ai-context/trustgraph-templates/pulumi/Pulumi.yaml
@@ -0,0 +1,3 @@
+name: config-svc
+runtime: nodejs
+description: Config service
diff --git a/ai-context/trustgraph-templates/pulumi/index.ts b/ai-context/trustgraph-templates/pulumi/index.ts
new file mode 100644
index 00000000..394dfd68
--- /dev/null
+++ b/ai-context/trustgraph-templates/pulumi/index.ts
@@ -0,0 +1,323 @@
+
+import * as pulumi from "@pulumi/pulumi";
+import * as gcp from "@pulumi/gcp";
+import { local } from "@pulumi/command";
+import * as fs from 'fs';
+
+const cfg = new pulumi.Config();
+
+function get(tag : string) {
+
+ let val = cfg.get(tag);
+
+ if (!val) {
+ console.log("ERROR: The '" + tag + "' config is mandatory");
+ throw "The '" + tag + "' config is mandatory";
+ }
+
+ return val;
+
+}
+
+const imageVersion = process.env.IMAGE_VERSION;
+if (!imageVersion)
+ throw Error("IMAGE_VERSION not defined");
+
+const repo = get("artifact-repo");
+const artifactRepoRegion = get("artifact-repo-region");
+const artifactName = get("artifact-name");
+const hostname = get("hostname");
+const managedZone = get("managed-zone");
+const project = get("gcp-project");
+const region = get("gcp-region");
+const cloudRunRegion = get("cloud-run-region");
+const environment = get("environment");
+const domain = get("domain");
+const minScale = get("min-scale");
+const maxScale = get("max-scale");
+
+const provider = new gcp.Provider(
+ "gcp",
+ {
+ project: project,
+ region: region,
+ }
+);
+
+const artifactRepo = new gcp.artifactregistry.Repository(
+ "artifact-repo",
+ {
+ description: "repository for " + environment,
+ format: "DOCKER",
+ location: artifactRepoRegion,
+ repositoryId: artifactName,
+ cleanupPolicies: [
+ {
+ id: "keep-minimum-versions",
+ action: "KEEP",
+ mostRecentVersions: {
+ keepCount: 5,
+ },
+ }
+ ],
+ },
+ {
+ provider: provider,
+ }
+);
+
+const localImageName = "localhost/config-svc:" + imageVersion;
+
+const imageName = repo + "/config-svc:" + imageVersion;
+
+const taggedImage = new local.Command(
+ "podman-tag-command",
+ {
+ create: "podman tag " + localImageName + " " + imageName,
+ }
+);
+
+const image = new local.Command(
+ "podman-push-command",
+ {
+ create: "podman push " + imageName,
+ },
+ {
+ dependsOn: [taggedImage, artifactRepo],
+ }
+);
+
+const svcAccount = new gcp.serviceaccount.Account(
+ "service-account",
+ {
+ accountId: "config-svc-" + environment,
+ displayName: "Config service",
+ description: "Config service",
+ },
+ {
+ provider: provider,
+ }
+);
+
+const service = new gcp.cloudrun.Service(
+ "service",
+ {
+ name: "config-svc-" + environment,
+ location: cloudRunRegion,
+ template: {
+ metadata: {
+ labels: {
+ version: "v" + imageVersion.replace(/\./g, "-"),
+ },
+ annotations: {
+
+ // Scale attributes
+ "autoscaling.knative.dev/minScale": minScale,
+ "autoscaling.knative.dev/maxScale": maxScale,
+
+ // 2nd generation. Need to specify at least 512MB RAM.
+ // Going back to gen1 because faster cold starts
+ "run.googleapis.com/execution-environment": "gen1",
+
+ }
+ },
+ spec: {
+ containerConcurrency: 100,
+ timeoutSeconds: 300,
+ serviceAccountName: svcAccount.email,
+ containers: [
+ {
+ image: imageName,
+ ports: [
+ {
+ "name": "http1", // Must be http1 or h2c.
+ "containerPort": 8080,
+ }
+ ],
+ resources: {
+ limits: {
+ cpu: "1000m",
+ memory: "512Mi",
+ }
+ },
+ }
+ ],
+ },
+ },
+ },
+ {
+ provider: provider,
+ dependsOn: [image],
+ }
+);
+
+const allUsersPolicy = gcp.organizations.getIAMPolicy(
+ {
+ bindings: [{
+ role: "roles/run.invoker",
+ members: ["allUsers"],
+ }],
+ },
+ {
+ provider: provider,
+ }
+);
+
+const noAuthPolicy = new gcp.cloudrun.IamPolicy(
+ "no-auth-policy",
+ {
+ location: service.location,
+ project: service.project,
+ service: service.name,
+ policyData: allUsersPolicy.then(pol => pol.policyData),
+ },
+ {
+ provider: provider,
+ }
+);
+
+////////////////////////////////////////////////////////////////////////////
+
+const domainMapping = new gcp.cloudrun.DomainMapping(
+ "domain-mapping",
+ {
+ name: hostname,
+ location: cloudRunRegion,
+ metadata: {
+ namespace: project,
+ },
+ spec: {
+ routeName: service.name,
+ }
+ },
+ {
+ provider: provider
+ }
+);
+
+const zone = gcp.dns.getManagedZoneOutput(
+ {
+ name: managedZone,
+ },
+ {
+ provider: provider,
+ }
+);
+
+////////////////////////////////////////////////////////////////////////////
+
+domainMapping.statuses.apply(
+ ss => ss[0].resourceRecords
+).apply(
+ rrs => {
+ if (rrs) {
+
+ let mapping : { [k : string] : string[] } = {};
+
+ for(var i = 0; i < rrs.length; i++) {
+ if (rrs[i].rrdata) {
+
+ const rr = rrs[i].rrdata;
+ const tp = rrs[i].type;
+
+ if (!rr || !tp) continue;
+
+ if (mapping[tp])
+ mapping[tp].push(rr);
+ else
+ mapping[tp] = [rr];
+
+ }
+ }
+
+ for (let tp in mapping) {
+
+ const recordSet = new gcp.dns.RecordSet(
+ "resource-record-" + tp,
+ {
+ name: hostname + ".",
+ managedZone: zone.name,
+ type: tp,
+ ttl: 300,
+ rrdatas: mapping[tp],
+ },
+ {
+ provider: provider,
+ }
+ );
+
+ }
+
+ }
+
+ }
+);
+
+////////////////////////////////////////////////////////////////////////////
+
+const serviceMon = new gcp.monitoring.GenericService(
+ "service-monitoring",
+ {
+ basicService: {
+ serviceLabels: {
+ service_name: service.name,
+ location: cloudRunRegion,
+ },
+ serviceType: "CLOUD_RUN",
+ },
+ displayName: "Config service (" + environment + ")",
+ serviceId: "config-service-" + environment + "-mon",
+ userLabels: {
+ "service": service.name,
+ "application": "config-svc",
+ "environment": environment,
+ },
+ },
+ {
+ provider: provider,
+ }
+);
+
+const latencySlo = new gcp.monitoring.Slo(
+ "latency-slo",
+ {
+ service: serviceMon.serviceId,
+ sloId: "config-service-" + environment + "-latency-slo",
+ displayName: "Config service latency (" + environment + ")",
+ goal: 0.95,
+ rollingPeriodDays: 5,
+ basicSli: {
+ latency: {
+ threshold: "2s"
+ }
+ },
+ },
+ {
+ provider: provider,
+ }
+);
+
+const availabilitySlo = new gcp.monitoring.Slo(
+ "availability-slo",
+ {
+ service: serviceMon.serviceId,
+ sloId: "config-service-" + environment + "-availability-slo",
+ displayName: "Config service availability (" + environment + ")",
+ goal: 0.95,
+ rollingPeriodDays: 5,
+ windowsBasedSli: {
+ windowPeriod: "3600s",
+ goodTotalRatioThreshold: {
+ basicSliPerformance: {
+ availability: {
+ }
+ },
+ threshold: 0.9,
+ }
+ }
+ },
+ {
+ provider: provider,
+ }
+);
+
diff --git a/ai-context/trustgraph-templates/pulumi/package-lock.json b/ai-context/trustgraph-templates/pulumi/package-lock.json
new file mode 100644
index 00000000..6cd217dd
--- /dev/null
+++ b/ai-context/trustgraph-templates/pulumi/package-lock.json
@@ -0,0 +1,4695 @@
+{
+ "name": "pulumi",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "pulumi",
+ "dependencies": {
+ "@pulumi/command": "^1.1.3",
+ "@pulumi/gcp": "^9.10.0",
+ "@pulumi/pulumi": "^3.216.0"
+ }
+ },
+ "node_modules/@gar/promise-retry": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.2.tgz",
+ "integrity": "sha512-Lm/ZLhDZcBECta3TmCQSngiQykFdfw+QtI1/GYMsZd4l3nG+P8WLB16XuS7WaBGLQ+9E+cOcWQsth9cayuGt8g==",
+ "license": "MIT",
+ "dependencies": {
+ "retry": "^0.13.1"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@grpc/grpc-js": {
+ "version": "1.14.3",
+ "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz",
+ "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@grpc/proto-loader": "^0.8.0",
+ "@js-sdsl/ordered-map": "^4.4.2"
+ },
+ "engines": {
+ "node": ">=12.10.0"
+ }
+ },
+ "node_modules/@grpc/proto-loader": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz",
+ "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "lodash.camelcase": "^4.3.0",
+ "long": "^5.0.0",
+ "protobufjs": "^7.5.3",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@isaacs/fs-minipass": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
+ "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^7.0.4"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@isaacs/string-locale-compare": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@isaacs/string-locale-compare/-/string-locale-compare-1.1.0.tgz",
+ "integrity": "sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ==",
+ "license": "ISC"
+ },
+ "node_modules/@js-sdsl/ordered-map": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
+ "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/js-sdsl"
+ }
+ },
+ "node_modules/@logdna/tail-file": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@logdna/tail-file/-/tail-file-2.2.0.tgz",
+ "integrity": "sha512-XGSsWDweP80Fks16lwkAUIr54ICyBs6PsI4mpfTLQaWgEJRtY9xEV+PeyDpJ+sJEGZxqINlpmAwe/6tS1pP8Ng==",
+ "license": "SEE LICENSE IN LICENSE",
+ "engines": {
+ "node": ">=10.3.0"
+ }
+ },
+ "node_modules/@npmcli/agent": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz",
+ "integrity": "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==",
+ "license": "ISC",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "http-proxy-agent": "^7.0.0",
+ "https-proxy-agent": "^7.0.1",
+ "lru-cache": "^11.2.1",
+ "socks-proxy-agent": "^8.0.3"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/agent/node_modules/lru-cache": {
+ "version": "11.2.6",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz",
+ "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/@npmcli/arborist": {
+ "version": "9.4.0",
+ "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-9.4.0.tgz",
+ "integrity": "sha512-4Bm8hNixJG/sii1PMnag0V9i/sGOX9VRzFrUiZMSBJpGlLR38f+Btl85d07G9GL56xO0l0OZjvrGNYsDYp0xKA==",
+ "license": "ISC",
+ "dependencies": {
+ "@isaacs/string-locale-compare": "^1.1.0",
+ "@npmcli/fs": "^5.0.0",
+ "@npmcli/installed-package-contents": "^4.0.0",
+ "@npmcli/map-workspaces": "^5.0.0",
+ "@npmcli/metavuln-calculator": "^9.0.2",
+ "@npmcli/name-from-folder": "^4.0.0",
+ "@npmcli/node-gyp": "^5.0.0",
+ "@npmcli/package-json": "^7.0.0",
+ "@npmcli/query": "^5.0.0",
+ "@npmcli/redact": "^4.0.0",
+ "@npmcli/run-script": "^10.0.0",
+ "bin-links": "^6.0.0",
+ "cacache": "^20.0.1",
+ "common-ancestor-path": "^2.0.0",
+ "hosted-git-info": "^9.0.0",
+ "json-stringify-nice": "^1.1.4",
+ "lru-cache": "^11.2.1",
+ "minimatch": "^10.0.3",
+ "nopt": "^9.0.0",
+ "npm-install-checks": "^8.0.0",
+ "npm-package-arg": "^13.0.0",
+ "npm-pick-manifest": "^11.0.1",
+ "npm-registry-fetch": "^19.0.0",
+ "pacote": "^21.0.2",
+ "parse-conflict-json": "^5.0.1",
+ "proc-log": "^6.0.0",
+ "proggy": "^4.0.0",
+ "promise-all-reject-late": "^1.0.0",
+ "promise-call-limit": "^3.0.1",
+ "semver": "^7.3.7",
+ "ssri": "^13.0.0",
+ "treeverse": "^3.0.0",
+ "walk-up-path": "^4.0.0"
+ },
+ "bin": {
+ "arborist": "bin/index.js"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/arborist/node_modules/@npmcli/git": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.2.tgz",
+ "integrity": "sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg==",
+ "license": "ISC",
+ "dependencies": {
+ "@gar/promise-retry": "^1.0.0",
+ "@npmcli/promise-spawn": "^9.0.0",
+ "ini": "^6.0.0",
+ "lru-cache": "^11.2.1",
+ "npm-pick-manifest": "^11.0.1",
+ "proc-log": "^6.0.0",
+ "semver": "^7.3.5",
+ "which": "^6.0.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/arborist/node_modules/@npmcli/package-json": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.5.tgz",
+ "integrity": "sha512-iVuTlG3ORq2iaVa1IWUxAO/jIp77tUKBhoMjuzYW2kL4MLN1bi/ofqkZ7D7OOwh8coAx1/S2ge0rMdGv8sLSOQ==",
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/git": "^7.0.0",
+ "glob": "^13.0.0",
+ "hosted-git-info": "^9.0.0",
+ "json-parse-even-better-errors": "^5.0.0",
+ "proc-log": "^6.0.0",
+ "semver": "^7.5.3",
+ "spdx-expression-parse": "^4.0.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/arborist/node_modules/@npmcli/promise-spawn": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz",
+ "integrity": "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "which": "^6.0.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/arborist/node_modules/glob": {
+ "version": "13.0.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
+ "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "minimatch": "^10.2.2",
+ "minipass": "^7.1.3",
+ "path-scurry": "^2.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@npmcli/arborist/node_modules/hosted-git-info": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz",
+ "integrity": "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==",
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^11.1.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/arborist/node_modules/ini": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz",
+ "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==",
+ "license": "ISC",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/arborist/node_modules/isexe": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz",
+ "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@npmcli/arborist/node_modules/json-parse-even-better-errors": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz",
+ "integrity": "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/arborist/node_modules/lru-cache": {
+ "version": "11.2.6",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz",
+ "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/@npmcli/arborist/node_modules/npm-pick-manifest": {
+ "version": "11.0.3",
+ "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-11.0.3.tgz",
+ "integrity": "sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ==",
+ "license": "ISC",
+ "dependencies": {
+ "npm-install-checks": "^8.0.0",
+ "npm-normalize-package-bin": "^5.0.0",
+ "npm-package-arg": "^13.0.0",
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/arborist/node_modules/path-scurry": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
+ "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^11.0.0",
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@npmcli/arborist/node_modules/proc-log": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz",
+ "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==",
+ "license": "ISC",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/arborist/node_modules/spdx-expression-parse": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz",
+ "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==",
+ "license": "MIT",
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/@npmcli/arborist/node_modules/which": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz",
+ "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^4.0.0"
+ },
+ "bin": {
+ "node-which": "bin/which.js"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/fs": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz",
+ "integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==",
+ "license": "ISC",
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/git": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-6.0.3.tgz",
+ "integrity": "sha512-GUYESQlxZRAdhs3UhbB6pVRNUELQOHXwK9ruDkwmCv2aZ5y0SApQzUJCg02p3A7Ue2J5hxvlk1YI53c00NmRyQ==",
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/promise-spawn": "^8.0.0",
+ "ini": "^5.0.0",
+ "lru-cache": "^10.0.1",
+ "npm-pick-manifest": "^10.0.0",
+ "proc-log": "^5.0.0",
+ "promise-retry": "^2.0.1",
+ "semver": "^7.3.5",
+ "which": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@npmcli/installed-package-contents": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-4.0.0.tgz",
+ "integrity": "sha512-yNyAdkBxB72gtZ4GrwXCM0ZUedo9nIbOMKfGjt6Cu6DXf0p8y1PViZAKDC8q8kv/fufx0WTjRBdSlyrvnP7hmA==",
+ "license": "ISC",
+ "dependencies": {
+ "npm-bundled": "^5.0.0",
+ "npm-normalize-package-bin": "^5.0.0"
+ },
+ "bin": {
+ "installed-package-contents": "bin/index.js"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/map-workspaces": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/@npmcli/map-workspaces/-/map-workspaces-5.0.3.tgz",
+ "integrity": "sha512-o2grssXo1e774E5OtEwwrgoszYRh0lqkJH+Pb9r78UcqdGJRDRfhpM8DvZPjzNLLNYeD/rNbjOKM3Ss5UABROw==",
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/name-from-folder": "^4.0.0",
+ "@npmcli/package-json": "^7.0.0",
+ "glob": "^13.0.0",
+ "minimatch": "^10.0.3"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/map-workspaces/node_modules/@npmcli/git": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.2.tgz",
+ "integrity": "sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg==",
+ "license": "ISC",
+ "dependencies": {
+ "@gar/promise-retry": "^1.0.0",
+ "@npmcli/promise-spawn": "^9.0.0",
+ "ini": "^6.0.0",
+ "lru-cache": "^11.2.1",
+ "npm-pick-manifest": "^11.0.1",
+ "proc-log": "^6.0.0",
+ "semver": "^7.3.5",
+ "which": "^6.0.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/map-workspaces/node_modules/@npmcli/package-json": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.5.tgz",
+ "integrity": "sha512-iVuTlG3ORq2iaVa1IWUxAO/jIp77tUKBhoMjuzYW2kL4MLN1bi/ofqkZ7D7OOwh8coAx1/S2ge0rMdGv8sLSOQ==",
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/git": "^7.0.0",
+ "glob": "^13.0.0",
+ "hosted-git-info": "^9.0.0",
+ "json-parse-even-better-errors": "^5.0.0",
+ "proc-log": "^6.0.0",
+ "semver": "^7.5.3",
+ "spdx-expression-parse": "^4.0.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/map-workspaces/node_modules/@npmcli/promise-spawn": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz",
+ "integrity": "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "which": "^6.0.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/map-workspaces/node_modules/glob": {
+ "version": "13.0.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
+ "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "minimatch": "^10.2.2",
+ "minipass": "^7.1.3",
+ "path-scurry": "^2.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@npmcli/map-workspaces/node_modules/hosted-git-info": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz",
+ "integrity": "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==",
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^11.1.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/map-workspaces/node_modules/ini": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz",
+ "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==",
+ "license": "ISC",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/map-workspaces/node_modules/isexe": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz",
+ "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@npmcli/map-workspaces/node_modules/json-parse-even-better-errors": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz",
+ "integrity": "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/map-workspaces/node_modules/lru-cache": {
+ "version": "11.2.6",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz",
+ "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/@npmcli/map-workspaces/node_modules/npm-pick-manifest": {
+ "version": "11.0.3",
+ "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-11.0.3.tgz",
+ "integrity": "sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ==",
+ "license": "ISC",
+ "dependencies": {
+ "npm-install-checks": "^8.0.0",
+ "npm-normalize-package-bin": "^5.0.0",
+ "npm-package-arg": "^13.0.0",
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/map-workspaces/node_modules/path-scurry": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
+ "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^11.0.0",
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@npmcli/map-workspaces/node_modules/proc-log": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz",
+ "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==",
+ "license": "ISC",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/map-workspaces/node_modules/spdx-expression-parse": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz",
+ "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==",
+ "license": "MIT",
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/@npmcli/map-workspaces/node_modules/which": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz",
+ "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^4.0.0"
+ },
+ "bin": {
+ "node-which": "bin/which.js"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/metavuln-calculator": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/@npmcli/metavuln-calculator/-/metavuln-calculator-9.0.3.tgz",
+ "integrity": "sha512-94GLSYhLXF2t2LAC7pDwLaM4uCARzxShyAQKsirmlNcpidH89VA4/+K1LbJmRMgz5gy65E/QBBWQdUvGLe2Frg==",
+ "license": "ISC",
+ "dependencies": {
+ "cacache": "^20.0.0",
+ "json-parse-even-better-errors": "^5.0.0",
+ "pacote": "^21.0.0",
+ "proc-log": "^6.0.0",
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/metavuln-calculator/node_modules/json-parse-even-better-errors": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz",
+ "integrity": "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/metavuln-calculator/node_modules/proc-log": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz",
+ "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==",
+ "license": "ISC",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/name-from-folder": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@npmcli/name-from-folder/-/name-from-folder-4.0.0.tgz",
+ "integrity": "sha512-qfrhVlOSqmKM8i6rkNdZzABj8MKEITGFAY+4teqBziksCQAOLutiAxM1wY2BKEd8KjUSpWmWCYxvXr0y4VTlPg==",
+ "license": "ISC",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/node-gyp": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-5.0.0.tgz",
+ "integrity": "sha512-uuG5HZFXLfyFKqg8QypsmgLQW7smiRjVc45bqD/ofZZcR/uxEjgQU8qDPv0s9TEeMUiAAU/GC5bR6++UdTirIQ==",
+ "license": "ISC",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/package-json": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-6.2.0.tgz",
+ "integrity": "sha512-rCNLSB/JzNvot0SEyXqWZ7tX2B5dD2a1br2Dp0vSYVo5jh8Z0EZ7lS9TsZ1UtziddB1UfNUaMCc538/HztnJGA==",
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/git": "^6.0.0",
+ "glob": "^10.2.2",
+ "hosted-git-info": "^8.0.0",
+ "json-parse-even-better-errors": "^4.0.0",
+ "proc-log": "^5.0.0",
+ "semver": "^7.5.3",
+ "validate-npm-package-license": "^3.0.4"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@npmcli/promise-spawn": {
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-8.0.3.tgz",
+ "integrity": "sha512-Yb00SWaL4F8w+K8YGhQ55+xE4RUNdMHV43WZGsiTM92gS+lC0mGsn7I4hLug7pbao035S6bj3Y3w0cUNGLfmkg==",
+ "license": "ISC",
+ "dependencies": {
+ "which": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@npmcli/query": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@npmcli/query/-/query-5.0.0.tgz",
+ "integrity": "sha512-8TZWfTQOsODpLqo9SVhVjHovmKXNpevHU0gO9e+y4V4fRIOneiXy0u0sMP9LmS71XivrEWfZWg50ReH4WRT4aQ==",
+ "license": "ISC",
+ "dependencies": {
+ "postcss-selector-parser": "^7.0.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/redact": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-4.0.0.tgz",
+ "integrity": "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==",
+ "license": "ISC",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/run-script": {
+ "version": "10.0.4",
+ "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.4.tgz",
+ "integrity": "sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg==",
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/node-gyp": "^5.0.0",
+ "@npmcli/package-json": "^7.0.0",
+ "@npmcli/promise-spawn": "^9.0.0",
+ "node-gyp": "^12.1.0",
+ "proc-log": "^6.0.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/run-script/node_modules/@npmcli/git": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.2.tgz",
+ "integrity": "sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg==",
+ "license": "ISC",
+ "dependencies": {
+ "@gar/promise-retry": "^1.0.0",
+ "@npmcli/promise-spawn": "^9.0.0",
+ "ini": "^6.0.0",
+ "lru-cache": "^11.2.1",
+ "npm-pick-manifest": "^11.0.1",
+ "proc-log": "^6.0.0",
+ "semver": "^7.3.5",
+ "which": "^6.0.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/run-script/node_modules/@npmcli/package-json": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.5.tgz",
+ "integrity": "sha512-iVuTlG3ORq2iaVa1IWUxAO/jIp77tUKBhoMjuzYW2kL4MLN1bi/ofqkZ7D7OOwh8coAx1/S2ge0rMdGv8sLSOQ==",
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/git": "^7.0.0",
+ "glob": "^13.0.0",
+ "hosted-git-info": "^9.0.0",
+ "json-parse-even-better-errors": "^5.0.0",
+ "proc-log": "^6.0.0",
+ "semver": "^7.5.3",
+ "spdx-expression-parse": "^4.0.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/run-script/node_modules/@npmcli/promise-spawn": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz",
+ "integrity": "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "which": "^6.0.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/run-script/node_modules/glob": {
+ "version": "13.0.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
+ "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "minimatch": "^10.2.2",
+ "minipass": "^7.1.3",
+ "path-scurry": "^2.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@npmcli/run-script/node_modules/hosted-git-info": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz",
+ "integrity": "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==",
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^11.1.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/run-script/node_modules/ini": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz",
+ "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==",
+ "license": "ISC",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/run-script/node_modules/isexe": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz",
+ "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@npmcli/run-script/node_modules/json-parse-even-better-errors": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz",
+ "integrity": "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/run-script/node_modules/lru-cache": {
+ "version": "11.2.6",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz",
+ "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/@npmcli/run-script/node_modules/npm-pick-manifest": {
+ "version": "11.0.3",
+ "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-11.0.3.tgz",
+ "integrity": "sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ==",
+ "license": "ISC",
+ "dependencies": {
+ "npm-install-checks": "^8.0.0",
+ "npm-normalize-package-bin": "^5.0.0",
+ "npm-package-arg": "^13.0.0",
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/run-script/node_modules/path-scurry": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
+ "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^11.0.0",
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@npmcli/run-script/node_modules/proc-log": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz",
+ "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==",
+ "license": "ISC",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/run-script/node_modules/spdx-expression-parse": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz",
+ "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==",
+ "license": "MIT",
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/@npmcli/run-script/node_modules/which": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz",
+ "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^4.0.0"
+ },
+ "bin": {
+ "node-which": "bin/which.js"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@opentelemetry/api": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
+ "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/@opentelemetry/api-logs": {
+ "version": "0.55.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.55.0.tgz",
+ "integrity": "sha512-3cpa+qI45VHYcA5c0bHM6VHo9gicv3p5mlLHNG3rLyjQU8b7e0st1rWtrUn3JbZ3DwwCfhKop4eQ9UuYlC6Pkg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@opentelemetry/context-async-hooks": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.30.1.tgz",
+ "integrity": "sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/core": {
+ "version": "1.28.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.28.0.tgz",
+ "integrity": "sha512-ZLwRMV+fNDpVmF2WYUdBHlq0eOWtEaUJSusrzjGnBt7iSRvfjFE3RXYUZJrqou/wIDWV0DwQ5KIfYe9WXg9Xqw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/semantic-conventions": "1.27.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/exporter-trace-otlp-grpc": {
+ "version": "0.55.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.55.0.tgz",
+ "integrity": "sha512-ohIkCLn2Wc3vhhFuf1bH8kOXHMEdcWiD847x7f3Qfygc+CGiatGLzQYscTcEYsWGMV22gVwB/kVcNcx5a3o8gA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@grpc/grpc-js": "^1.7.1",
+ "@opentelemetry/core": "1.28.0",
+ "@opentelemetry/otlp-grpc-exporter-base": "0.55.0",
+ "@opentelemetry/otlp-transformer": "0.55.0",
+ "@opentelemetry/resources": "1.28.0",
+ "@opentelemetry/sdk-trace-base": "1.28.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/resources": {
+ "version": "1.28.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.28.0.tgz",
+ "integrity": "sha512-cIyXSVJjGeTICENN40YSvLDAq4Y2502hGK3iN7tfdynQLKWb3XWZQEkPc+eSx47kiy11YeFAlYkEfXwR1w8kfw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "1.28.0",
+ "@opentelemetry/semantic-conventions": "1.27.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/sdk-trace-base": {
+ "version": "1.28.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.28.0.tgz",
+ "integrity": "sha512-ceUVWuCpIao7Y5xE02Xs3nQi0tOGmMea17ecBdwtCvdo9ekmO+ijc9RFDgfifMl7XCBf41zne/1POM3LqSTZDA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "1.28.0",
+ "@opentelemetry/resources": "1.28.0",
+ "@opentelemetry/semantic-conventions": "1.27.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/exporter-zipkin": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-1.30.1.tgz",
+ "integrity": "sha512-6S2QIMJahIquvFaaxmcwpvQQRD/YFaMTNoIxrfPIPOeITN+a8lfEcPDxNxn8JDAaxkg+4EnXhz8upVDYenoQjA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "1.30.1",
+ "@opentelemetry/resources": "1.30.1",
+ "@opentelemetry/sdk-trace-base": "1.30.1",
+ "@opentelemetry/semantic-conventions": "1.28.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.0.0"
+ }
+ },
+ "node_modules/@opentelemetry/exporter-zipkin/node_modules/@opentelemetry/core": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz",
+ "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/semantic-conventions": "1.28.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/exporter-zipkin/node_modules/@opentelemetry/semantic-conventions": {
+ "version": "1.28.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz",
+ "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation": {
+ "version": "0.55.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.55.0.tgz",
+ "integrity": "sha512-YDCMlaQRZkziLL3t6TONRgmmGxDx6MyQDXRD0dknkkgUZtOK5+8MWft1OXzmNu6XfBOdT12MKN5rz+jHUkafKQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api-logs": "0.55.0",
+ "@types/shimmer": "^1.2.0",
+ "import-in-the-middle": "^1.8.1",
+ "require-in-the-middle": "^7.1.1",
+ "semver": "^7.5.2",
+ "shimmer": "^1.2.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-grpc": {
+ "version": "0.55.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-grpc/-/instrumentation-grpc-0.55.0.tgz",
+ "integrity": "sha512-n2ZH4pRwOy0Vhag/3eKqiyDBwcpUnGgJI9iiIRX7vivE0FMncaLazWphNFezRRaM/LuKwq1TD8pVUvieP68mow==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "0.55.0",
+ "@opentelemetry/semantic-conventions": "1.27.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/otlp-exporter-base": {
+ "version": "0.55.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.55.0.tgz",
+ "integrity": "sha512-iHQI0Zzq3h1T6xUJTVFwmFl5Dt5y1es+fl4kM+k5T/3YvmVyeYkSiF+wHCg6oKrlUAJfk+t55kaAu3sYmt7ZYA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "1.28.0",
+ "@opentelemetry/otlp-transformer": "0.55.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/otlp-grpc-exporter-base": {
+ "version": "0.55.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.55.0.tgz",
+ "integrity": "sha512-gebbjl9FiSp52igWXuGjcWQKfB6IBwFGt5z1VFwTcVZVeEZevB6bJIqoFrhH4A02m7OUlpJ7l4EfRi3UtkNANQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@grpc/grpc-js": "^1.7.1",
+ "@opentelemetry/core": "1.28.0",
+ "@opentelemetry/otlp-exporter-base": "0.55.0",
+ "@opentelemetry/otlp-transformer": "0.55.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/otlp-transformer": {
+ "version": "0.55.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.55.0.tgz",
+ "integrity": "sha512-kVqEfxtp6mSN2Dhpy0REo1ghP4PYhC1kMHQJ2qVlO99Pc+aigELjZDfg7/YKmL71gR6wVGIeJfiql/eXL7sQPA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api-logs": "0.55.0",
+ "@opentelemetry/core": "1.28.0",
+ "@opentelemetry/resources": "1.28.0",
+ "@opentelemetry/sdk-logs": "0.55.0",
+ "@opentelemetry/sdk-metrics": "1.28.0",
+ "@opentelemetry/sdk-trace-base": "1.28.0",
+ "protobufjs": "^7.3.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": {
+ "version": "1.28.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.28.0.tgz",
+ "integrity": "sha512-cIyXSVJjGeTICENN40YSvLDAq4Y2502hGK3iN7tfdynQLKWb3XWZQEkPc+eSx47kiy11YeFAlYkEfXwR1w8kfw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "1.28.0",
+ "@opentelemetry/semantic-conventions": "1.27.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-trace-base": {
+ "version": "1.28.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.28.0.tgz",
+ "integrity": "sha512-ceUVWuCpIao7Y5xE02Xs3nQi0tOGmMea17ecBdwtCvdo9ekmO+ijc9RFDgfifMl7XCBf41zne/1POM3LqSTZDA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "1.28.0",
+ "@opentelemetry/resources": "1.28.0",
+ "@opentelemetry/semantic-conventions": "1.27.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/propagator-b3": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-1.30.1.tgz",
+ "integrity": "sha512-oATwWWDIJzybAZ4pO76ATN5N6FFbOA1otibAVlS8v90B4S1wClnhRUk7K+2CHAwN1JKYuj4jh/lpCEG5BAqFuQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "1.30.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/propagator-b3/node_modules/@opentelemetry/core": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz",
+ "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/semantic-conventions": "1.28.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/propagator-b3/node_modules/@opentelemetry/semantic-conventions": {
+ "version": "1.28.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz",
+ "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@opentelemetry/propagator-jaeger": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.30.1.tgz",
+ "integrity": "sha512-Pj/BfnYEKIOImirH76M4hDaBSx6HyZ2CXUqk+Kj02m6BB80c/yo4BdWkn/1gDFfU+YPY+bPR2U0DKBfdxCKwmg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "1.30.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/propagator-jaeger/node_modules/@opentelemetry/core": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz",
+ "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/semantic-conventions": "1.28.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/propagator-jaeger/node_modules/@opentelemetry/semantic-conventions": {
+ "version": "1.28.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz",
+ "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@opentelemetry/resources": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.30.1.tgz",
+ "integrity": "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "1.30.1",
+ "@opentelemetry/semantic-conventions": "1.28.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/core": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz",
+ "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/semantic-conventions": "1.28.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/semantic-conventions": {
+ "version": "1.28.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz",
+ "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-logs": {
+ "version": "0.55.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.55.0.tgz",
+ "integrity": "sha512-TSx+Yg/d48uWW6HtjS1AD5x6WPfLhDWLl/WxC7I2fMevaiBuKCuraxTB8MDXieCNnBI24bw9ytyXrDCswFfWgA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api-logs": "0.55.0",
+ "@opentelemetry/core": "1.28.0",
+ "@opentelemetry/resources": "1.28.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.4.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": {
+ "version": "1.28.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.28.0.tgz",
+ "integrity": "sha512-cIyXSVJjGeTICENN40YSvLDAq4Y2502hGK3iN7tfdynQLKWb3XWZQEkPc+eSx47kiy11YeFAlYkEfXwR1w8kfw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "1.28.0",
+ "@opentelemetry/semantic-conventions": "1.27.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-metrics": {
+ "version": "1.28.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.28.0.tgz",
+ "integrity": "sha512-43tqMK/0BcKTyOvm15/WQ3HLr0Vu/ucAl/D84NO7iSlv6O4eOprxSHa3sUtmYkaZWHqdDJV0AHVz/R6u4JALVQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "1.28.0",
+ "@opentelemetry/resources": "1.28.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": {
+ "version": "1.28.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.28.0.tgz",
+ "integrity": "sha512-cIyXSVJjGeTICENN40YSvLDAq4Y2502hGK3iN7tfdynQLKWb3XWZQEkPc+eSx47kiy11YeFAlYkEfXwR1w8kfw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "1.28.0",
+ "@opentelemetry/semantic-conventions": "1.27.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace-base": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.30.1.tgz",
+ "integrity": "sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "1.30.1",
+ "@opentelemetry/resources": "1.30.1",
+ "@opentelemetry/semantic-conventions": "1.28.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/core": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz",
+ "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/semantic-conventions": "1.28.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/semantic-conventions": {
+ "version": "1.28.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz",
+ "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace-node": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.30.1.tgz",
+ "integrity": "sha512-cBjYOINt1JxXdpw1e5MlHmFRc5fgj4GW/86vsKFxJCJ8AL4PdVtYH41gWwl4qd4uQjqEL1oJVrXkSy5cnduAnQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/context-async-hooks": "1.30.1",
+ "@opentelemetry/core": "1.30.1",
+ "@opentelemetry/propagator-b3": "1.30.1",
+ "@opentelemetry/propagator-jaeger": "1.30.1",
+ "@opentelemetry/sdk-trace-base": "1.30.1",
+ "semver": "^7.5.2"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/core": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz",
+ "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/semantic-conventions": "1.28.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/semantic-conventions": {
+ "version": "1.28.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz",
+ "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@opentelemetry/semantic-conventions": {
+ "version": "1.27.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz",
+ "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@protobufjs/aspromise": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+ "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/base64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
+ "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/codegen": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
+ "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/eventemitter": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
+ "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/fetch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
+ "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.1",
+ "@protobufjs/inquire": "^1.1.0"
+ }
+ },
+ "node_modules/@protobufjs/float": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
+ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/inquire": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
+ "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/path": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
+ "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/pool": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
+ "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/utf8": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
+ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@pulumi/command": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@pulumi/command/-/command-1.2.1.tgz",
+ "integrity": "sha512-mutNDIUYP67yCBYOVIidQyxuTwZDY9v/sx9EGbgIv4PXfyfolOKGgGLeoHEbI1lxRwaw2wbTZ3VNIynDnA5VKA==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@pulumi/pulumi": "^3.142.0"
+ }
+ },
+ "node_modules/@pulumi/gcp": {
+ "version": "9.14.0",
+ "resolved": "https://registry.npmjs.org/@pulumi/gcp/-/gcp-9.14.0.tgz",
+ "integrity": "sha512-5QZelNAR1VkvbP9AFPCJUv2jQTgTAe0B/ed197ZucOpuL0HR/m27ggElW3RxE96qP5GXtd9KArU5SFcoVZ05yA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@npmcli/package-json": "^6.2.0",
+ "@pulumi/pulumi": "^3.142.0",
+ "@types/express": "^4.16.0"
+ }
+ },
+ "node_modules/@pulumi/pulumi": {
+ "version": "3.225.0",
+ "resolved": "https://registry.npmjs.org/@pulumi/pulumi/-/pulumi-3.225.0.tgz",
+ "integrity": "sha512-dqlc+d7kd6srAEyLxhO/lHRj0AWSvaMYNbP2BWafXZuzqp/2zg0Ro+OPE2/dQbyJQwW3bD250DLzEU94qInlcw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@grpc/grpc-js": "^1.10.1",
+ "@logdna/tail-file": "^2.0.6",
+ "@npmcli/arborist": "^9.0.0",
+ "@opentelemetry/api": "^1.9",
+ "@opentelemetry/exporter-trace-otlp-grpc": "^0.55",
+ "@opentelemetry/exporter-zipkin": "^1.28",
+ "@opentelemetry/instrumentation": "^0.55",
+ "@opentelemetry/instrumentation-grpc": "^0.55",
+ "@opentelemetry/resources": "^1.28",
+ "@opentelemetry/sdk-trace-base": "^1.28",
+ "@opentelemetry/sdk-trace-node": "^1.28",
+ "@types/google-protobuf": "^3.15.5",
+ "@types/semver": "^7.5.6",
+ "@types/tmp": "^0.2.6",
+ "execa": "^5.1.0",
+ "fdir": "^6.5.0",
+ "google-protobuf": "^3.21.4",
+ "got": "^11.8.6",
+ "ini": "^2.0.0",
+ "js-yaml": "^3.14.2",
+ "minimist": "^1.2.6",
+ "normalize-package-data": "^6.0.0",
+ "package-directory": "^8.1.0",
+ "picomatch": "^3.0.1",
+ "require-from-string": "^2.0.1",
+ "semver": "^7.5.2",
+ "source-map-support": "^0.5.6",
+ "tmp": "^0.2.4",
+ "upath": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "peerDependencies": {
+ "ts-node": ">= 7.0.1 < 12",
+ "typescript": ">= 3.8.3 < 6"
+ },
+ "peerDependenciesMeta": {
+ "ts-node": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@pulumi/pulumi/node_modules/ini": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz",
+ "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@sigstore/bundle": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-4.0.0.tgz",
+ "integrity": "sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@sigstore/protobuf-specs": "^0.5.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@sigstore/core": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-3.1.0.tgz",
+ "integrity": "sha512-o5cw1QYhNQ9IroioJxpzexmPjfCe7gzafd2RY3qnMpxr4ZEja+Jad/U8sgFpaue6bOaF+z7RVkyKVV44FN+N8A==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@sigstore/protobuf-specs": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.0.tgz",
+ "integrity": "sha512-MM8XIwUjN2bwvCg1QvrMtbBmpcSHrkhFSCu1D11NyPvDQ25HEc4oG5/OcQfd/Tlf/OxmKWERDj0zGE23jQaMwA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@sigstore/sign": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-4.1.0.tgz",
+ "integrity": "sha512-Vx1RmLxLGnSUqx/o5/VsCjkuN5L7y+vxEEwawvc7u+6WtX2W4GNa7b9HEjmcRWohw/d6BpATXmvOwc78m+Swdg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@sigstore/bundle": "^4.0.0",
+ "@sigstore/core": "^3.1.0",
+ "@sigstore/protobuf-specs": "^0.5.0",
+ "make-fetch-happen": "^15.0.3",
+ "proc-log": "^6.1.0",
+ "promise-retry": "^2.0.1"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@sigstore/sign/node_modules/proc-log": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz",
+ "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==",
+ "license": "ISC",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@sigstore/tuf": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-4.0.1.tgz",
+ "integrity": "sha512-OPZBg8y5Vc9yZjmWCHrlWPMBqW5yd8+wFNl+thMdtcWz3vjVSoJQutF8YkrzI0SLGnkuFof4HSsWUhXrf219Lw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@sigstore/protobuf-specs": "^0.5.0",
+ "tuf-js": "^4.1.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@sigstore/verify": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-3.1.0.tgz",
+ "integrity": "sha512-mNe0Iigql08YupSOGv197YdHpPPr+EzDZmfCgMc7RPNaZTw5aLN01nBl6CHJOh3BGtnMIj83EeN4butBchc8Ag==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@sigstore/bundle": "^4.0.0",
+ "@sigstore/core": "^3.1.0",
+ "@sigstore/protobuf-specs": "^0.5.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@sindresorhus/is": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
+ "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/is?sponsor=1"
+ }
+ },
+ "node_modules/@szmarczak/http-timer": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
+ "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
+ "license": "MIT",
+ "dependencies": {
+ "defer-to-connect": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@tufjs/canonical-json": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz",
+ "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@tufjs/models": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-4.1.0.tgz",
+ "integrity": "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww==",
+ "license": "MIT",
+ "dependencies": {
+ "@tufjs/canonical-json": "2.0.0",
+ "minimatch": "^10.1.1"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@types/body-parser": {
+ "version": "1.19.6",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
+ "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/cacheable-request": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
+ "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-cache-semantics": "*",
+ "@types/keyv": "^3.1.4",
+ "@types/node": "*",
+ "@types/responselike": "^1.0.0"
+ }
+ },
+ "node_modules/@types/connect": {
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+ "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/express": {
+ "version": "4.17.25",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz",
+ "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^4.17.33",
+ "@types/qs": "*",
+ "@types/serve-static": "^1"
+ }
+ },
+ "node_modules/@types/express-serve-static-core": {
+ "version": "4.19.8",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz",
+ "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*",
+ "@types/send": "*"
+ }
+ },
+ "node_modules/@types/google-protobuf": {
+ "version": "3.15.12",
+ "resolved": "https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.15.12.tgz",
+ "integrity": "sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/http-cache-semantics": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
+ "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==",
+ "license": "MIT"
+ },
+ "node_modules/@types/http-errors": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
+ "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/keyv": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
+ "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/mime": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
+ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "25.3.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz",
+ "integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.18.0"
+ }
+ },
+ "node_modules/@types/qs": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
+ "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/range-parser": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
+ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/responselike": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
+ "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/semver": {
+ "version": "7.7.1",
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz",
+ "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/serve-static": {
+ "version": "1.15.10",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz",
+ "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-errors": "*",
+ "@types/node": "*",
+ "@types/send": "<1"
+ }
+ },
+ "node_modules/@types/serve-static/node_modules/@types/send": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz",
+ "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mime": "^1",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/shimmer": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz",
+ "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/tmp": {
+ "version": "0.2.6",
+ "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.6.tgz",
+ "integrity": "sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==",
+ "license": "MIT"
+ },
+ "node_modules/abbrev": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz",
+ "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==",
+ "license": "ISC",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-import-attributes": {
+ "version": "1.9.5",
+ "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz",
+ "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^8"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "license": "MIT",
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/bin-links": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/bin-links/-/bin-links-6.0.0.tgz",
+ "integrity": "sha512-X4CiKlcV2GjnCMwnKAfbVWpHa++65th9TuzAEYtZoATiOE2DQKhSp4CJlyLoTqdhBKlXjpXjCTYPNNFS33Fi6w==",
+ "license": "ISC",
+ "dependencies": {
+ "cmd-shim": "^8.0.0",
+ "npm-normalize-package-bin": "^5.0.0",
+ "proc-log": "^6.0.0",
+ "read-cmd-shim": "^6.0.0",
+ "write-file-atomic": "^7.0.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/bin-links/node_modules/proc-log": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz",
+ "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==",
+ "license": "ISC",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
+ "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "license": "MIT"
+ },
+ "node_modules/cacache": {
+ "version": "20.0.3",
+ "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.3.tgz",
+ "integrity": "sha512-3pUp4e8hv07k1QlijZu6Kn7c9+ZpWWk4j3F8N3xPuCExULobqJydKYOTj1FTq58srkJsXvO7LbGAH4C0ZU3WGw==",
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/fs": "^5.0.0",
+ "fs-minipass": "^3.0.0",
+ "glob": "^13.0.0",
+ "lru-cache": "^11.1.0",
+ "minipass": "^7.0.3",
+ "minipass-collect": "^2.0.1",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.4",
+ "p-map": "^7.0.2",
+ "ssri": "^13.0.0",
+ "unique-filename": "^5.0.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/cacache/node_modules/glob": {
+ "version": "13.0.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
+ "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "minimatch": "^10.2.2",
+ "minipass": "^7.1.3",
+ "path-scurry": "^2.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/cacache/node_modules/lru-cache": {
+ "version": "11.2.6",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz",
+ "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/cacache/node_modules/path-scurry": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
+ "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^11.0.0",
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/cacheable-lookup": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
+ "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.6.0"
+ }
+ },
+ "node_modules/cacheable-request": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
+ "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
+ "license": "MIT",
+ "dependencies": {
+ "clone-response": "^1.0.2",
+ "get-stream": "^5.1.0",
+ "http-cache-semantics": "^4.0.0",
+ "keyv": "^4.0.0",
+ "lowercase-keys": "^2.0.0",
+ "normalize-url": "^6.0.1",
+ "responselike": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cacheable-request/node_modules/get-stream": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+ "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+ "license": "MIT",
+ "dependencies": {
+ "pump": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/chownr": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
+ "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/cjs-module-lexer": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz",
+ "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==",
+ "license": "MIT"
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/cliui/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/cliui/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/cliui/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/clone-response": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
+ "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
+ "license": "MIT",
+ "dependencies": {
+ "mimic-response": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cmd-shim": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-8.0.0.tgz",
+ "integrity": "sha512-Jk/BK6NCapZ58BKUxlSI+ouKRbjH1NLZCgJkYoab+vEHUY3f6OzpNBN9u7HFSv9J6TRDGs4PLOHezoKGaFRSCA==",
+ "license": "ISC",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "license": "MIT"
+ },
+ "node_modules/common-ancestor-path": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-2.0.0.tgz",
+ "integrity": "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/cross-spawn/node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "license": "ISC"
+ },
+ "node_modules/cross-spawn/node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decompress-response": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mimic-response": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/decompress-response/node_modules/mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/defer-to-connect": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
+ "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "license": "MIT"
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "license": "MIT"
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+ "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+ "license": "MIT",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/env-paths": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+ "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/err-code": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
+ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
+ "license": "MIT"
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "license": "BSD-2-Clause",
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/execa": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
+ "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.0",
+ "human-signals": "^2.1.0",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.1",
+ "onetime": "^5.1.2",
+ "signal-exit": "^3.0.3",
+ "strip-final-newline": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/exponential-backoff": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz",
+ "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/find-up-simple": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz",
+ "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/foreground-child/node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/fs-minipass": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz",
+ "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==",
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^7.0.3"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/glob": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "license": "MIT"
+ },
+ "node_modules/glob/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/glob/node_modules/minimatch": {
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/google-protobuf": {
+ "version": "3.21.4",
+ "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.4.tgz",
+ "integrity": "sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ==",
+ "license": "(BSD-3-Clause AND Apache-2.0)"
+ },
+ "node_modules/got": {
+ "version": "11.8.6",
+ "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
+ "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
+ "license": "MIT",
+ "dependencies": {
+ "@sindresorhus/is": "^4.0.0",
+ "@szmarczak/http-timer": "^4.0.5",
+ "@types/cacheable-request": "^6.0.1",
+ "@types/responselike": "^1.0.0",
+ "cacheable-lookup": "^5.0.3",
+ "cacheable-request": "^7.0.2",
+ "decompress-response": "^6.0.0",
+ "http2-wrapper": "^1.0.0-beta.5.2",
+ "lowercase-keys": "^2.0.0",
+ "p-cancelable": "^2.0.0",
+ "responselike": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/got?sponsor=1"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "license": "ISC"
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hosted-git-info": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.1.0.tgz",
+ "integrity": "sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==",
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^10.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/http-cache-semantics": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
+ "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/http2-wrapper": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
+ "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
+ "license": "MIT",
+ "dependencies": {
+ "quick-lru": "^5.1.1",
+ "resolve-alpn": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=10.19.0"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/human-signals": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
+ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.17.0"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/ignore-walk": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-8.0.0.tgz",
+ "integrity": "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==",
+ "license": "ISC",
+ "dependencies": {
+ "minimatch": "^10.0.3"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/import-in-the-middle": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.15.0.tgz",
+ "integrity": "sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "acorn": "^8.14.0",
+ "acorn-import-attributes": "^1.9.5",
+ "cjs-module-lexer": "^1.2.2",
+ "module-details-from-path": "^1.0.3"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/ini": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz",
+ "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==",
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/ip-address": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
+ "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz",
+ "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/js-yaml": {
+ "version": "3.14.2",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
+ "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "license": "MIT"
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz",
+ "integrity": "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/json-stringify-nice": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/json-stringify-nice/-/json-stringify-nice-1.1.4.tgz",
+ "integrity": "sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw==",
+ "license": "ISC",
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/jsonparse": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
+ "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==",
+ "engines": [
+ "node >= 0.2.0"
+ ],
+ "license": "MIT"
+ },
+ "node_modules/just-diff": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/just-diff/-/just-diff-6.0.2.tgz",
+ "integrity": "sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA==",
+ "license": "MIT"
+ },
+ "node_modules/just-diff-apply": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/just-diff-apply/-/just-diff-apply-5.5.0.tgz",
+ "integrity": "sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw==",
+ "license": "MIT"
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/lodash.camelcase": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
+ "license": "MIT"
+ },
+ "node_modules/long": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
+ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/lowercase-keys": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
+ "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "license": "ISC"
+ },
+ "node_modules/make-fetch-happen": {
+ "version": "15.0.4",
+ "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.4.tgz",
+ "integrity": "sha512-vM2sG+wbVeVGYcCm16mM3d5fuem9oC28n436HjsGO3LcxoTI8LNVa4rwZDn3f76+cWyT4GGJDxjTYU1I2nr6zw==",
+ "license": "ISC",
+ "dependencies": {
+ "@gar/promise-retry": "^1.0.0",
+ "@npmcli/agent": "^4.0.0",
+ "cacache": "^20.0.1",
+ "http-cache-semantics": "^4.1.1",
+ "minipass": "^7.0.2",
+ "minipass-fetch": "^5.0.0",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.4",
+ "negotiator": "^1.0.0",
+ "proc-log": "^6.0.0",
+ "ssri": "^13.0.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/make-fetch-happen/node_modules/proc-log": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz",
+ "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==",
+ "license": "ISC",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "license": "MIT"
+ },
+ "node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/mimic-response": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
+ "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "10.2.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
+ "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
+ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/minipass-collect": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz",
+ "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==",
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^7.0.3"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/minipass-fetch": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.2.tgz",
+ "integrity": "sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==",
+ "license": "MIT",
+ "dependencies": {
+ "minipass": "^7.0.3",
+ "minipass-sized": "^2.0.0",
+ "minizlib": "^3.0.1"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ },
+ "optionalDependencies": {
+ "iconv-lite": "^0.7.2"
+ }
+ },
+ "node_modules/minipass-flush": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz",
+ "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==",
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minipass-flush/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-flush/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "license": "ISC"
+ },
+ "node_modules/minipass-pipeline": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz",
+ "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==",
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-pipeline/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-pipeline/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "license": "ISC"
+ },
+ "node_modules/minipass-sized": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-2.0.0.tgz",
+ "integrity": "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==",
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minizlib": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
+ "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
+ "license": "MIT",
+ "dependencies": {
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/module-details-from-path": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz",
+ "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==",
+ "license": "MIT"
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/node-gyp": {
+ "version": "12.2.0",
+ "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.2.0.tgz",
+ "integrity": "sha512-q23WdzrQv48KozXlr0U1v9dwO/k59NHeSzn6loGcasyf0UnSrtzs8kRxM+mfwJSf0DkX0s43hcqgnSO4/VNthQ==",
+ "license": "MIT",
+ "dependencies": {
+ "env-paths": "^2.2.0",
+ "exponential-backoff": "^3.1.1",
+ "graceful-fs": "^4.2.6",
+ "make-fetch-happen": "^15.0.0",
+ "nopt": "^9.0.0",
+ "proc-log": "^6.0.0",
+ "semver": "^7.3.5",
+ "tar": "^7.5.4",
+ "tinyglobby": "^0.2.12",
+ "which": "^6.0.0"
+ },
+ "bin": {
+ "node-gyp": "bin/node-gyp.js"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/node-gyp/node_modules/isexe": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz",
+ "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/node-gyp/node_modules/proc-log": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz",
+ "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==",
+ "license": "ISC",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/node-gyp/node_modules/which": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz",
+ "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^4.0.0"
+ },
+ "bin": {
+ "node-which": "bin/which.js"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/nopt": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz",
+ "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==",
+ "license": "ISC",
+ "dependencies": {
+ "abbrev": "^4.0.0"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/normalize-package-data": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz",
+ "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "hosted-git-info": "^7.0.0",
+ "semver": "^7.3.5",
+ "validate-npm-package-license": "^3.0.4"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/normalize-package-data/node_modules/hosted-git-info": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz",
+ "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==",
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^10.0.1"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/normalize-url": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
+ "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/npm-bundled": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-5.0.0.tgz",
+ "integrity": "sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw==",
+ "license": "ISC",
+ "dependencies": {
+ "npm-normalize-package-bin": "^5.0.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/npm-install-checks": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-8.0.0.tgz",
+ "integrity": "sha512-ScAUdMpyzkbpxoNekQ3tNRdFI8SJ86wgKZSQZdUxT+bj0wVFpsEMWnkXP0twVe1gJyNF5apBWDJhhIbgrIViRA==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "semver": "^7.1.1"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/npm-normalize-package-bin": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-5.0.0.tgz",
+ "integrity": "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==",
+ "license": "ISC",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/npm-package-arg": {
+ "version": "13.0.2",
+ "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.2.tgz",
+ "integrity": "sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA==",
+ "license": "ISC",
+ "dependencies": {
+ "hosted-git-info": "^9.0.0",
+ "proc-log": "^6.0.0",
+ "semver": "^7.3.5",
+ "validate-npm-package-name": "^7.0.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/npm-package-arg/node_modules/hosted-git-info": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz",
+ "integrity": "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==",
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^11.1.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/npm-package-arg/node_modules/lru-cache": {
+ "version": "11.2.6",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz",
+ "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/npm-package-arg/node_modules/proc-log": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz",
+ "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==",
+ "license": "ISC",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/npm-packlist": {
+ "version": "10.0.4",
+ "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.4.tgz",
+ "integrity": "sha512-uMW73iajD8hiH4ZBxEV3HC+eTnppIqwakjOYuvgddnalIw2lJguKviK1pcUJDlIWm1wSJkchpDZDSVVsZEYRng==",
+ "license": "ISC",
+ "dependencies": {
+ "ignore-walk": "^8.0.0",
+ "proc-log": "^6.0.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/npm-packlist/node_modules/proc-log": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz",
+ "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==",
+ "license": "ISC",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/npm-pick-manifest": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-10.0.0.tgz",
+ "integrity": "sha512-r4fFa4FqYY8xaM7fHecQ9Z2nE9hgNfJR+EmoKv0+chvzWkBcORX3r0FpTByP+CbOVJDladMXnPQGVN8PBLGuTQ==",
+ "license": "ISC",
+ "dependencies": {
+ "npm-install-checks": "^7.1.0",
+ "npm-normalize-package-bin": "^4.0.0",
+ "npm-package-arg": "^12.0.0",
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm-pick-manifest/node_modules/npm-install-checks": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-7.1.2.tgz",
+ "integrity": "sha512-z9HJBCYw9Zr8BqXcllKIs5nI+QggAImbBdHphOzVYrz2CB4iQ6FzWyKmlqDZua+51nAu7FcemlbTc9VgQN5XDQ==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "semver": "^7.1.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm-pick-manifest/node_modules/npm-normalize-package-bin": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz",
+ "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==",
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm-pick-manifest/node_modules/npm-package-arg": {
+ "version": "12.0.2",
+ "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.2.tgz",
+ "integrity": "sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA==",
+ "license": "ISC",
+ "dependencies": {
+ "hosted-git-info": "^8.0.0",
+ "proc-log": "^5.0.0",
+ "semver": "^7.3.5",
+ "validate-npm-package-name": "^6.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm-pick-manifest/node_modules/validate-npm-package-name": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.2.tgz",
+ "integrity": "sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ==",
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm-registry-fetch": {
+ "version": "19.1.1",
+ "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-19.1.1.tgz",
+ "integrity": "sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw==",
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/redact": "^4.0.0",
+ "jsonparse": "^1.3.1",
+ "make-fetch-happen": "^15.0.0",
+ "minipass": "^7.0.2",
+ "minipass-fetch": "^5.0.0",
+ "minizlib": "^3.0.1",
+ "npm-package-arg": "^13.0.0",
+ "proc-log": "^6.0.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/npm-registry-fetch/node_modules/proc-log": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz",
+ "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==",
+ "license": "ISC",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/npm-run-path": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+ "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "license": "MIT",
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-cancelable": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
+ "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/p-map": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz",
+ "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/package-directory": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/package-directory/-/package-directory-8.2.0.tgz",
+ "integrity": "sha512-qJSu5Mo6tHmRxCy2KCYYKYgcfBdUpy9dwReaZD/xwf608AUk/MoRtIOWzgDtUeGeC7n/55yC3MI1Q+MbSoektw==",
+ "license": "MIT",
+ "dependencies": {
+ "find-up-simple": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/pacote": {
+ "version": "21.4.0",
+ "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.4.0.tgz",
+ "integrity": "sha512-DR7mn7HUOomAX1BORnpYy678qVIidbvOojkBscqy27dRKN+s/hLeQT1MeYYrx1Cxh62jyKjiWiDV7RTTqB+ZEQ==",
+ "license": "ISC",
+ "dependencies": {
+ "@gar/promise-retry": "^1.0.0",
+ "@npmcli/git": "^7.0.0",
+ "@npmcli/installed-package-contents": "^4.0.0",
+ "@npmcli/package-json": "^7.0.0",
+ "@npmcli/promise-spawn": "^9.0.0",
+ "@npmcli/run-script": "^10.0.0",
+ "cacache": "^20.0.0",
+ "fs-minipass": "^3.0.0",
+ "minipass": "^7.0.2",
+ "npm-package-arg": "^13.0.0",
+ "npm-packlist": "^10.0.1",
+ "npm-pick-manifest": "^11.0.1",
+ "npm-registry-fetch": "^19.0.0",
+ "proc-log": "^6.0.0",
+ "sigstore": "^4.0.0",
+ "ssri": "^13.0.0",
+ "tar": "^7.4.3"
+ },
+ "bin": {
+ "pacote": "bin/index.js"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/pacote/node_modules/@npmcli/git": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.2.tgz",
+ "integrity": "sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg==",
+ "license": "ISC",
+ "dependencies": {
+ "@gar/promise-retry": "^1.0.0",
+ "@npmcli/promise-spawn": "^9.0.0",
+ "ini": "^6.0.0",
+ "lru-cache": "^11.2.1",
+ "npm-pick-manifest": "^11.0.1",
+ "proc-log": "^6.0.0",
+ "semver": "^7.3.5",
+ "which": "^6.0.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/pacote/node_modules/@npmcli/package-json": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.5.tgz",
+ "integrity": "sha512-iVuTlG3ORq2iaVa1IWUxAO/jIp77tUKBhoMjuzYW2kL4MLN1bi/ofqkZ7D7OOwh8coAx1/S2ge0rMdGv8sLSOQ==",
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/git": "^7.0.0",
+ "glob": "^13.0.0",
+ "hosted-git-info": "^9.0.0",
+ "json-parse-even-better-errors": "^5.0.0",
+ "proc-log": "^6.0.0",
+ "semver": "^7.5.3",
+ "spdx-expression-parse": "^4.0.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/pacote/node_modules/@npmcli/promise-spawn": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz",
+ "integrity": "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "which": "^6.0.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/pacote/node_modules/glob": {
+ "version": "13.0.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
+ "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "minimatch": "^10.2.2",
+ "minipass": "^7.1.3",
+ "path-scurry": "^2.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/pacote/node_modules/hosted-git-info": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz",
+ "integrity": "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==",
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^11.1.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/pacote/node_modules/ini": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz",
+ "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==",
+ "license": "ISC",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/pacote/node_modules/isexe": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz",
+ "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/pacote/node_modules/json-parse-even-better-errors": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz",
+ "integrity": "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/pacote/node_modules/lru-cache": {
+ "version": "11.2.6",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz",
+ "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/pacote/node_modules/npm-pick-manifest": {
+ "version": "11.0.3",
+ "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-11.0.3.tgz",
+ "integrity": "sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ==",
+ "license": "ISC",
+ "dependencies": {
+ "npm-install-checks": "^8.0.0",
+ "npm-normalize-package-bin": "^5.0.0",
+ "npm-package-arg": "^13.0.0",
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/pacote/node_modules/path-scurry": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
+ "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^11.0.0",
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/pacote/node_modules/proc-log": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz",
+ "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==",
+ "license": "ISC",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/pacote/node_modules/spdx-expression-parse": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz",
+ "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==",
+ "license": "MIT",
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/pacote/node_modules/which": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz",
+ "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^4.0.0"
+ },
+ "bin": {
+ "node-which": "bin/which.js"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/parse-conflict-json": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/parse-conflict-json/-/parse-conflict-json-5.0.1.tgz",
+ "integrity": "sha512-ZHEmNKMq1wyJXNwLxyHnluPfRAFSIliBvbK/UiOceROt4Xh9Pz0fq49NytIaeaCUf5VR86hwQ/34FCcNU5/LKQ==",
+ "license": "ISC",
+ "dependencies": {
+ "json-parse-even-better-errors": "^5.0.0",
+ "just-diff": "^6.0.0",
+ "just-diff-apply": "^5.2.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/parse-conflict-json/node_modules/json-parse-even-better-errors": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz",
+ "integrity": "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "license": "MIT"
+ },
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/picomatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz",
+ "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
+ "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/proc-log": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz",
+ "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==",
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/proggy": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/proggy/-/proggy-4.0.0.tgz",
+ "integrity": "sha512-MbA4R+WQT76ZBm/5JUpV9yqcJt92175+Y0Bodg3HgiXzrmKu7Ggq+bpn6y6wHH+gN9NcyKn3yg1+d47VaKwNAQ==",
+ "license": "ISC",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/promise-all-reject-late": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/promise-all-reject-late/-/promise-all-reject-late-1.0.1.tgz",
+ "integrity": "sha512-vuf0Lf0lOxyQREH7GDIOUMLS7kz+gs8i6B+Yi8dC68a2sychGrHTJYghMBD6k7eUcH0H5P73EckCA48xijWqXw==",
+ "license": "ISC",
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/promise-call-limit": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/promise-call-limit/-/promise-call-limit-3.0.2.tgz",
+ "integrity": "sha512-mRPQO2T1QQVw11E7+UdCJu7S61eJVWknzml9sC1heAdj1jxl0fWMBypIt9ZOcLFf8FkG995ZD7RnVk7HH72fZw==",
+ "license": "ISC",
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/promise-retry": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
+ "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
+ "license": "MIT",
+ "dependencies": {
+ "err-code": "^2.0.2",
+ "retry": "^0.12.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/promise-retry/node_modules/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/protobufjs": {
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
+ "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
+ "hasInstallScript": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.2",
+ "@protobufjs/base64": "^1.1.2",
+ "@protobufjs/codegen": "^2.0.4",
+ "@protobufjs/eventemitter": "^1.1.0",
+ "@protobufjs/fetch": "^1.1.0",
+ "@protobufjs/float": "^1.0.2",
+ "@protobufjs/inquire": "^1.1.0",
+ "@protobufjs/path": "^1.1.2",
+ "@protobufjs/pool": "^1.1.0",
+ "@protobufjs/utf8": "^1.1.0",
+ "@types/node": ">=13.7.0",
+ "long": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/pump": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
+ "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
+ "license": "MIT",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/quick-lru": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
+ "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/read-cmd-shim": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-6.0.0.tgz",
+ "integrity": "sha512-1zM5HuOfagXCBWMN83fuFI/x+T/UhZ7k+KIzhrHXcQoeX5+7gmaDYjELQHmmzIodumBHeByBJT4QYS7ufAgs7A==",
+ "license": "ISC",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-in-the-middle": {
+ "version": "7.5.2",
+ "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz",
+ "integrity": "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.5",
+ "module-details-from-path": "^1.0.3",
+ "resolve": "^1.22.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.11",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
+ "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-alpn": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
+ "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
+ "license": "MIT"
+ },
+ "node_modules/responselike": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
+ "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
+ "license": "MIT",
+ "dependencies": {
+ "lowercase-keys": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/retry": {
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
+ "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shimmer": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz",
+ "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "license": "ISC"
+ },
+ "node_modules/sigstore": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-4.1.0.tgz",
+ "integrity": "sha512-/fUgUhYghuLzVT/gaJoeVehLCgZiUxPCPMcyVNY0lIf/cTCz58K/WTI7PefDarXxp9nUKpEwg1yyz3eSBMTtgA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@sigstore/bundle": "^4.0.0",
+ "@sigstore/core": "^3.1.0",
+ "@sigstore/protobuf-specs": "^0.5.0",
+ "@sigstore/sign": "^4.1.0",
+ "@sigstore/tuf": "^4.0.1",
+ "@sigstore/verify": "^3.1.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/smart-buffer": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
+ "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/socks": {
+ "version": "2.8.7",
+ "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
+ "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
+ "license": "MIT",
+ "dependencies": {
+ "ip-address": "^10.0.1",
+ "smart-buffer": "^4.2.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/socks-proxy-agent": {
+ "version": "8.0.5",
+ "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
+ "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "^4.3.4",
+ "socks": "^2.8.3"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/spdx-correct": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
+ "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-exceptions": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
+ "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==",
+ "license": "CC-BY-3.0"
+ },
+ "node_modules/spdx-expression-parse": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
+ "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
+ "license": "MIT",
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-license-ids": {
+ "version": "3.0.23",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz",
+ "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==",
+ "license": "CC0-1.0"
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/ssri": {
+ "version": "13.0.1",
+ "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.1.tgz",
+ "integrity": "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==",
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^7.0.3"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/string-width-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+ "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.2.2"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-final-newline": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tar": {
+ "version": "7.5.10",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.10.tgz",
+ "integrity": "sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/fs-minipass": "^4.0.0",
+ "chownr": "^3.0.0",
+ "minipass": "^7.1.2",
+ "minizlib": "^3.1.0",
+ "yallist": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/tmp": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
+ "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
+ "node_modules/treeverse": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/treeverse/-/treeverse-3.0.0.tgz",
+ "integrity": "sha512-gcANaAnd2QDZFmHFEOF4k7uc1J/6a6z3DJMd/QwEyxLoKGiptJRwid582r7QIsFlFMIZ3SnxfS52S4hm2DHkuQ==",
+ "license": "ISC",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/tuf-js": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-4.1.0.tgz",
+ "integrity": "sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@tufjs/models": "4.1.0",
+ "debug": "^4.4.3",
+ "make-fetch-happen": "^15.0.1"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.18.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
+ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
+ "license": "MIT"
+ },
+ "node_modules/unique-filename": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-5.0.0.tgz",
+ "integrity": "sha512-2RaJTAvAb4owyjllTfXzFClJ7WsGxlykkPvCr9pA//LD9goVq+m4PPAeBgNodGZ7nSrntT/auWpJ6Y5IFXcfjg==",
+ "license": "ISC",
+ "dependencies": {
+ "unique-slug": "^6.0.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/unique-slug": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-6.0.0.tgz",
+ "integrity": "sha512-4Lup7Ezn8W3d52/xBhZBVdx323ckxa7DEvd9kPQHppTkLoJXw6ltrBCyj5pnrxj0qKDxYMJ56CoxNuFCscdTiw==",
+ "license": "ISC",
+ "dependencies": {
+ "imurmurhash": "^0.1.4"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/upath": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz",
+ "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4",
+ "yarn": "*"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
+ "node_modules/validate-npm-package-license": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+ "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "spdx-correct": "^3.0.0",
+ "spdx-expression-parse": "^3.0.0"
+ }
+ },
+ "node_modules/validate-npm-package-name": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz",
+ "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==",
+ "license": "ISC",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/walk-up-path": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz",
+ "integrity": "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==",
+ "license": "ISC",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/which": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz",
+ "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^3.1.1"
+ },
+ "bin": {
+ "node-which": "bin/which.js"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ },
+ "node_modules/write-file-atomic": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-7.0.1.tgz",
+ "integrity": "sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==",
+ "license": "ISC",
+ "dependencies": {
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/write-file-atomic/node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
+ "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/yargs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ }
+ }
+}
diff --git a/ai-context/trustgraph-templates/pulumi/package.json b/ai-context/trustgraph-templates/pulumi/package.json
new file mode 100644
index 00000000..1bbb56b1
--- /dev/null
+++ b/ai-context/trustgraph-templates/pulumi/package.json
@@ -0,0 +1,7 @@
+{
+ "dependencies": {
+ "@pulumi/command": "^1.1.3",
+ "@pulumi/gcp": "^9.10.0",
+ "@pulumi/pulumi": "^3.216.0"
+ }
+}
diff --git a/ai-context/trustgraph-templates/pulumi/tsconfig.json b/ai-context/trustgraph-templates/pulumi/tsconfig.json
new file mode 100644
index 00000000..ab65afa6
--- /dev/null
+++ b/ai-context/trustgraph-templates/pulumi/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "outDir": "bin",
+ "target": "es2016",
+ "module": "commonjs",
+ "moduleResolution": "node",
+ "sourceMap": true,
+ "experimentalDecorators": true,
+ "pretty": true,
+ "noFallthroughCasesInSwitch": true,
+ "noImplicitReturns": true,
+ "forceConsistentCasingInFileNames": true
+ },
+ "files": [
+ "index.ts"
+ ]
+}
diff --git a/ai-context/trustgraph-templates/pyproject.toml b/ai-context/trustgraph-templates/pyproject.toml
new file mode 100644
index 00000000..953cd0a1
--- /dev/null
+++ b/ai-context/trustgraph-templates/pyproject.toml
@@ -0,0 +1,68 @@
+[build-system]
+requires = ["setuptools>=61.0"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "trustgraph-configurator"
+dynamic = ["version"]
+authors = [
+ {name = "trustgraph.ai", email = "security@trustgraph.ai"},
+]
+description = "Configuration creator for trustgraph.ai"
+readme = "README.md"
+requires-python = ">=3.8"
+license = {text = "Apache-2.0"}
+classifiers = [
+ "Programming Language :: Python :: 3",
+ "License :: OSI Approved :: Apache Software License",
+ "Operating System :: OS Independent",
+]
+dependencies = [
+ "aiohttp",
+ "gojsonnet",
+ "pyyaml",
+ "tabulate",
+]
+
+[project.optional-dependencies]
+dev = [
+ "pytest>=7.0",
+ "pytest-xdist>=3.0",
+ "pytest-cov>=4.0",
+ "jsonschema>=4.0",
+]
+
+[project.urls]
+Homepage = "https://github.com/trustgraph-ai/trustgraph-configurator"
+
+[project.scripts]
+tg-build-deployment = "trustgraph_configurator:run"
+tg-config-svc = "trustgraph_configurator:run_service"
+tg-show-config-params = "trustgraph_configurator:list"
+
+[tool.setuptools.packages.find]
+include = ["trustgraph_configurator*"]
+
+[tool.setuptools.package-data]
+trustgraph_configurator = ["templates/**", "resources/**"]
+
+[tool.setuptools.dynamic]
+version = {attr = "trustgraph_configurator.__version__"}
+
+[tool.pytest.ini_options]
+testpaths = ["tests"]
+python_files = ["test_*.py"]
+python_classes = ["Test*"]
+python_functions = ["test_*"]
+addopts = [
+ "-v",
+ "--strict-markers",
+ "--tb=short",
+]
+markers = [
+ "unit: Unit tests",
+ "integration: Integration tests",
+ "validation: Output validation tests",
+ "slow: Slow-running tests",
+]
+
diff --git a/ai-context/trustgraph-templates/test-dialog-flow.py b/ai-context/trustgraph-templates/test-dialog-flow.py
new file mode 100644
index 00000000..a26d31ff
--- /dev/null
+++ b/ai-context/trustgraph-templates/test-dialog-flow.py
@@ -0,0 +1,493 @@
+#!/usr/bin/env python3
+"""
+Test harness for dialog flow - walks through selecting all default options,
+produces the resulting state object, and runs the JSONata transform.
+
+Supports a test matrix mode where each field is tested with all its options
+while other fields use defaults.
+"""
+
+import yaml
+import json
+import argparse
+import zipfile
+import io
+from pathlib import Path
+import jsonata
+import requests
+
+
+RESOURCES_DIR = Path(__file__).parent / "trustgraph_configurator/resources/dialog"
+
+
+def load_flow():
+ """Load the dialog flow YAML file."""
+ with open(RESOURCES_DIR / "trustgraph-flow.yaml") as f:
+ return yaml.safe_load(f)
+
+
+def load_jsonata_transform():
+ """Load the JSONata transform file."""
+ with open(RESOURCES_DIR / "trustgraph-output.jsonata") as f:
+ return f.read()
+
+
+def run_transform(state, transform_expr):
+ """Run the JSONata transform on the state object."""
+ expr = jsonata.Jsonata(transform_expr)
+ return expr.evaluate(state)
+
+
+def call_config_service(config):
+ """
+ Call the configuration service to generate a deployment package.
+ Returns (success, message, zip_contents) tuple.
+
+ The API expects just the templates array, not the full config object.
+ """
+ url = config["api_url"]
+ templates = config["templates"]
+
+ try:
+ response = requests.post(
+ url,
+ json=templates,
+ headers={"Content-Type": "application/json"},
+ timeout=30
+ )
+
+ if response.status_code != 200:
+ return False, f"HTTP {response.status_code}: {response.text[:200]}", None
+
+ # Verify it's a valid ZIP
+ try:
+ zip_data = io.BytesIO(response.content)
+ with zipfile.ZipFile(zip_data, 'r') as zf:
+ file_list = zf.namelist()
+ return True, f"ZIP with {len(file_list)} files", file_list
+ except zipfile.BadZipFile:
+ return False, "Response is not a valid ZIP file", None
+
+ except requests.exceptions.ConnectionError:
+ return False, "Connection refused - is the service running?", None
+ except requests.exceptions.Timeout:
+ return False, "Request timed out", None
+ except Exception as e:
+ return False, f"Error: {e}", None
+
+
+def get_all_options(step):
+ """Get all possible values for a step's input."""
+ input_def = step.get("input", {})
+ input_type = input_def.get("type")
+
+ if input_type == "select":
+ return [opt["value"] for opt in input_def.get("options", [])]
+ elif input_type == "toggle":
+ return [True, False]
+ elif input_type == "number":
+ # For numbers, just test default, min, and max
+ default = input_def.get("default")
+ min_val = input_def.get("min")
+ max_val = input_def.get("max")
+ values = []
+ if min_val is not None:
+ values.append(min_val)
+ if default is not None and default not in values:
+ values.append(default)
+ if max_val is not None and max_val not in values:
+ values.append(max_val)
+ return values
+
+ return []
+
+
+def get_default_value(step):
+ """Get the default value for a step's input."""
+ input_def = step.get("input", {})
+ input_type = input_def.get("type")
+
+ if input_type == "select":
+ options = input_def.get("options", [])
+ # Find recommended option, or use first
+ for opt in options:
+ if opt.get("recommended"):
+ return opt["value"]
+ return options[0]["value"] if options else None
+
+ elif input_type == "number":
+ return input_def.get("default")
+
+ elif input_type == "toggle":
+ return input_def.get("default", False)
+
+ return None
+
+
+def evaluate_condition(condition, state):
+ """
+ Evaluate a simple condition against the state.
+ Supports: "key = value", "key = true/false", "key < 'version'"
+ """
+ if not condition:
+ return True
+
+ # Handle equality: "ocr.enabled = true"
+ if " = " in condition:
+ key, value = condition.split(" = ", 1)
+ key = key.strip()
+ value = value.strip()
+
+ # Get nested key
+ state_value = state.get(key)
+
+ # Parse value
+ if value == "true":
+ return state_value is True
+ elif value == "false":
+ return state_value is False
+ else:
+ return str(state_value) == value
+
+ # Handle less-than for version comparisons: "version < '1.6.0'"
+ if " < " in condition:
+ key, value = condition.split(" < ", 1)
+ key = key.strip()
+ value = value.strip().strip("'\"")
+ state_value = state.get(key, "")
+ return str(state_value) < value
+
+ return False
+
+
+def get_next_step(step, state):
+ """Determine the next step based on transitions and current state."""
+ transitions = step.get("transitions", [])
+
+ for trans in transitions:
+ when = trans.get("when")
+ if when:
+ if evaluate_condition(when, state):
+ return trans.get("next")
+ else:
+ # Unconditional transition
+ return trans.get("next")
+
+ return None # Terminal state
+
+
+def walk_flow(flow_data, overrides=None, verbose=True):
+ """
+ Walk through the flow, return the state object.
+
+ Args:
+ flow_data: The parsed dialog flow YAML
+ overrides: Dict of {state_key: value} to override defaults
+ verbose: Whether to print progress
+ """
+ state = {}
+ overrides = overrides or {}
+ steps = flow_data.get("steps", {})
+ current = flow_data.get("flow", {}).get("start")
+ visited_steps = []
+
+ if verbose:
+ print(f"Starting at: {current}")
+ print("-" * 60)
+
+ while current:
+ step = steps.get(current)
+ if not step:
+ if verbose:
+ print(f"ERROR: Step '{current}' not found!")
+ break
+
+ visited_steps.append(current)
+ title = step.get("title", current)
+ state_key = step.get("state_key")
+
+ # Get value - use override if present, otherwise default
+ if state_key:
+ if state_key in overrides:
+ value = overrides[state_key]
+ else:
+ value = get_default_value(step)
+ state[state_key] = value
+
+ if verbose:
+ is_override = state_key in overrides
+ marker = " [OVERRIDE]" if is_override else ""
+ print(f"Step: {current}")
+ print(f" Title: {title}")
+ print(f" State key: {state_key} = {value}{marker}")
+ else:
+ if verbose:
+ print(f"Step: {current}")
+ print(f" Title: {title}")
+ print(f" (no state key - review/terminal step)")
+
+ # Get next step
+ next_step = get_next_step(step, state)
+ if verbose:
+ if next_step:
+ print(f" -> Next: {next_step}")
+ else:
+ print(f" -> Terminal state")
+ print()
+
+ current = next_step
+
+ return state, visited_steps
+
+
+def collect_fields_for_path(flow_data, overrides=None):
+ """
+ Collect all fields and their possible values for a given path.
+ Returns a list of (step_name, state_key, options, default_value) tuples.
+ """
+ fields = []
+ steps = flow_data.get("steps", {})
+
+ # Walk with given overrides to find the path
+ _, visited = walk_flow(flow_data, overrides=overrides, verbose=False)
+
+ for step_name in visited:
+ step = steps.get(step_name, {})
+ state_key = step.get("state_key")
+ if state_key:
+ options = get_all_options(step)
+ default = get_default_value(step)
+ if len(options) > 1: # Only include fields with choices
+ fields.append((step_name, state_key, options, default))
+
+ return fields
+
+
+def collect_all_fields(flow_data):
+ """
+ Collect all fields from the baseline (default) path.
+ """
+ return collect_fields_for_path(flow_data, overrides=None)
+
+
+def run_single_test(flow_data, transform_expr, overrides, description, test_num, results, call_api=False):
+ """Run a single test case and record the result."""
+ print("-" * 70)
+ print(f"Test {test_num}: {description}")
+ print("-" * 70)
+
+ state, _ = walk_flow(flow_data, overrides=overrides, verbose=False)
+
+ try:
+ config = run_transform(state, transform_expr)
+ result = {
+ "test": test_num,
+ "description": description,
+ "overrides": overrides,
+ "state": state,
+ "config": config
+ }
+
+ print(f"State: {json.dumps(state, indent=2)}")
+ print(f"Templates: {[t['name'] for t in config['templates']]}")
+
+ # Optionally call the configuration service
+ if call_api:
+ success, message, files = call_config_service(config)
+ result["api_success"] = success
+ result["api_message"] = message
+ if files:
+ result["api_files"] = files
+
+ if success:
+ print(f"API: OK - {message}")
+ else:
+ print(f"API: FAILED - {message}")
+ result["error"] = f"API: {message}"
+
+ results.append(result)
+
+ except Exception as e:
+ results.append({
+ "test": test_num,
+ "description": description,
+ "overrides": overrides,
+ "state": state,
+ "error": str(e)
+ })
+ print(f"State: {json.dumps(state, indent=2)}")
+ print(f"ERROR: {e}")
+ print()
+
+ return test_num + 1
+
+
+def run_test_matrix(flow_data, transform_expr, call_api=False):
+ """
+ Run the test matrix - for each field, try all values while others use defaults.
+ When a toggle enables conditional fields, also test all options of those fields.
+ """
+ baseline_fields = collect_all_fields(flow_data)
+ baseline_keys = {f[1] for f in baseline_fields}
+
+ print("=" * 70)
+ print("TEST MATRIX" + (" (with API validation)" if call_api else ""))
+ print("=" * 70)
+ print()
+ print(f"Found {len(baseline_fields)} fields with multiple options on baseline path:")
+ for step_name, state_key, options, default in baseline_fields:
+ print(f" - {state_key}: {len(options)} options (default: {default})")
+ print()
+
+ results = []
+ test_num = 1
+
+ # First, run the baseline (all defaults)
+ test_num = run_single_test(
+ flow_data, transform_expr,
+ overrides={},
+ description="BASELINE (all defaults)",
+ test_num=test_num,
+ results=results,
+ call_api=call_api
+ )
+
+ # For each field, try each non-default value
+ for step_name, state_key, options, default in baseline_fields:
+ for option in options:
+ if option == default:
+ continue # Skip default, already tested in baseline
+
+ overrides = {state_key: option}
+ test_num = run_single_test(
+ flow_data, transform_expr,
+ overrides=overrides,
+ description=f"{state_key} = {option}",
+ test_num=test_num,
+ results=results,
+ call_api=call_api
+ )
+
+ # Check if this override unlocks new fields (conditional paths)
+ unlocked_fields = collect_fields_for_path(flow_data, overrides=overrides)
+ unlocked_keys = {f[1] for f in unlocked_fields}
+ new_keys = unlocked_keys - baseline_keys
+
+ if new_keys:
+ # Test all non-default options of the newly unlocked fields
+ for uf_step, uf_key, uf_options, uf_default in unlocked_fields:
+ if uf_key not in new_keys:
+ continue
+ for uf_option in uf_options:
+ if uf_option == uf_default:
+ continue # Default already tested above
+
+ combined_overrides = {state_key: option, uf_key: uf_option}
+ test_num = run_single_test(
+ flow_data, transform_expr,
+ overrides=combined_overrides,
+ description=f"{state_key} = {option}, {uf_key} = {uf_option}",
+ test_num=test_num,
+ results=results,
+ call_api=call_api
+ )
+
+ return results
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Test harness for dialog flow configuration"
+ )
+ parser.add_argument(
+ "--matrix", "-m",
+ action="store_true",
+ help="Run test matrix (each field with all options)"
+ )
+ parser.add_argument(
+ "--api", "-a",
+ action="store_true",
+ help="Call the configuration service API to validate each config"
+ )
+ parser.add_argument(
+ "--summary", "-s",
+ action="store_true",
+ help="Show summary only (with --matrix)"
+ )
+ args = parser.parse_args()
+
+ flow_data = load_flow()
+ transform_expr = load_jsonata_transform()
+
+ if args.matrix:
+ results = run_test_matrix(flow_data, transform_expr, call_api=args.api)
+
+ # Summary
+ print("=" * 70)
+ print("SUMMARY")
+ print("=" * 70)
+ print()
+ passed = [r for r in results if "error" not in r]
+ failed = [r for r in results if "error" in r]
+ print(f"Total tests: {len(results)}")
+ print(f"Passed: {len(passed)}")
+ print(f"Failed: {len(failed)}")
+
+ if args.api:
+ api_ok = [r for r in results if r.get("api_success")]
+ api_fail = [r for r in results if "api_success" in r and not r["api_success"]]
+ print(f"API OK: {len(api_ok)}")
+ print(f"API Failed: {len(api_fail)}")
+
+ if failed:
+ print()
+ print("Failed tests:")
+ for r in failed:
+ print(f" - Test {r['test']}: {r['description']}")
+ print(f" Error: {r['error']}")
+ else:
+ print("=" * 60)
+ print("Dialog Flow Test Harness - Default Options")
+ print("=" * 60)
+ print()
+
+ state, _ = walk_flow(flow_data)
+
+ print("=" * 60)
+ print("Final State Object:")
+ print("=" * 60)
+ print()
+ print(json.dumps(state, indent=2))
+
+ # Run JSONata transform
+ print()
+ print("=" * 60)
+ print("Running JSONata Transform...")
+ print("=" * 60)
+ print()
+
+ config = run_transform(state, transform_expr)
+
+ print("=" * 60)
+ print("Configuration Object (output of transform):")
+ print("=" * 60)
+ print()
+ print(json.dumps(config, indent=2))
+
+ # Optionally call the API
+ if args.api:
+ print()
+ print("=" * 60)
+ print("Calling Configuration Service...")
+ print("=" * 60)
+ print()
+ success, message, files = call_config_service(config)
+ if success:
+ print(f"OK: {message}")
+ print(f"Files: {files}")
+ else:
+ print(f"FAILED: {message}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ai-context/trustgraph-templates/test-docs-flow.py b/ai-context/trustgraph-templates/test-docs-flow.py
new file mode 100644
index 00000000..487a75c9
--- /dev/null
+++ b/ai-context/trustgraph-templates/test-docs-flow.py
@@ -0,0 +1,416 @@
+#!/usr/bin/env python3
+"""
+Test harness for documentation assembly - verifies that for each configuration
+state, the documentation can be assembled from the manifest and markdown fragments.
+"""
+
+import yaml
+import json
+import argparse
+import re
+from pathlib import Path
+import jsonata
+
+RESOURCES_DIR = Path(__file__).parent / "trustgraph_configurator/resources/dialog"
+
+
+def load_flow():
+ """Load the dialog flow YAML file."""
+ with open(RESOURCES_DIR / "trustgraph-flow.yaml") as f:
+ return yaml.safe_load(f)
+
+
+def load_docs_manifest():
+ """Load the documentation manifest YAML file."""
+ with open(RESOURCES_DIR / "trustgraph-docs.yaml") as f:
+ return yaml.safe_load(f)
+
+
+def load_doc_file(filename):
+ """Load a documentation markdown file."""
+ path = RESOURCES_DIR / "docs" / filename
+ if path.exists():
+ return path.read_text()
+ return None
+
+
+def get_default_value(step):
+ """Get the default value for a step's input."""
+ input_def = step.get("input", {})
+ input_type = input_def.get("type")
+
+ if input_type == "select":
+ options = input_def.get("options", [])
+ for opt in options:
+ if opt.get("recommended"):
+ return opt["value"]
+ return options[0]["value"] if options else None
+ elif input_type == "number":
+ return input_def.get("default")
+ elif input_type == "toggle":
+ return input_def.get("default", False)
+ return None
+
+
+def get_all_options(step):
+ """Get all possible values for a step's input."""
+ input_def = step.get("input", {})
+ input_type = input_def.get("type")
+
+ if input_type == "select":
+ return [opt["value"] for opt in input_def.get("options", [])]
+ elif input_type == "toggle":
+ return [True, False]
+ elif input_type == "number":
+ default = input_def.get("default")
+ min_val = input_def.get("min")
+ max_val = input_def.get("max")
+ values = []
+ if min_val is not None:
+ values.append(min_val)
+ if default is not None and default not in values:
+ values.append(default)
+ if max_val is not None and max_val not in values:
+ values.append(max_val)
+ return values
+ return []
+
+
+def evaluate_condition(condition, state):
+ """Evaluate a simple condition against the state."""
+ if not condition:
+ return True
+
+ if " = " in condition:
+ key, value = condition.split(" = ", 1)
+ key = key.strip()
+ value = value.strip()
+ state_value = state.get(key)
+ if value == "true":
+ return state_value is True
+ elif value == "false":
+ return state_value is False
+ else:
+ return str(state_value) == value
+
+ if " < " in condition:
+ key, value = condition.split(" < ", 1)
+ key = key.strip()
+ value = value.strip().strip("'\"")
+ state_value = state.get(key, "")
+ return str(state_value) < value
+
+ return False
+
+
+def get_next_step(step, state):
+ """Determine the next step based on transitions and current state."""
+ transitions = step.get("transitions", [])
+ for trans in transitions:
+ when = trans.get("when")
+ if when:
+ if evaluate_condition(when, state):
+ return trans.get("next")
+ else:
+ return trans.get("next")
+ return None
+
+
+def walk_flow(flow_data, overrides=None):
+ """Walk through the flow, return the state object."""
+ state = {}
+ overrides = overrides or {}
+ steps = flow_data.get("steps", {})
+ current = flow_data.get("flow", {}).get("start")
+ visited_steps = []
+
+ while current:
+ step = steps.get(current)
+ if not step:
+ break
+
+ visited_steps.append(current)
+ state_key = step.get("state_key")
+
+ if state_key:
+ if state_key in overrides:
+ value = overrides[state_key]
+ else:
+ value = get_default_value(step)
+ state[state_key] = value
+
+ current = get_next_step(step, state)
+
+ return state, visited_steps
+
+
+def evaluate_when_condition(when_expr, state):
+ """
+ Evaluate a 'when' condition from the docs manifest against the state.
+ Supports: equality, 'in' arrays, 'and' conditions.
+ """
+ if not when_expr:
+ return False
+
+ # Use jsonata for complex expressions
+ try:
+ expr = jsonata.Jsonata(when_expr)
+ result = expr.evaluate(state)
+ return bool(result)
+ except Exception as e:
+ # Fallback to simple parsing for basic cases
+ return evaluate_simple_when(when_expr, state)
+
+
+def evaluate_simple_when(when_expr, state):
+ """Simple fallback parser for when expressions."""
+ # Handle "and" conditions
+ if " and " in when_expr:
+ parts = when_expr.split(" and ")
+ return all(evaluate_simple_when(p.strip(), state) for p in parts)
+
+ # Handle "in" conditions: "platform in ['docker-compose', 'podman-compose']"
+ in_match = re.match(r"(\w+)\s+in\s+\[([^\]]+)\]", when_expr)
+ if in_match:
+ key = in_match.group(1)
+ values_str = in_match.group(2)
+ values = [v.strip().strip("'\"") for v in values_str.split(",")]
+ return state.get(key) in values
+
+ # Handle equality: "platform = 'docker-compose'"
+ eq_match = re.match(r"(\w+)\s*=\s*['\"]([^'\"]+)['\"]", when_expr)
+ if eq_match:
+ key = eq_match.group(1)
+ value = eq_match.group(2)
+ return state.get(key) == value
+
+ return False
+
+
+def assemble_docs(state, manifest):
+ """
+ Assemble documentation for a given state.
+ Returns (success, matched_instructions, errors).
+ """
+ instructions = manifest.get("documentation", {}).get("instructions", [])
+ matched = []
+ errors = []
+
+ for instr in instructions:
+ # Check if instruction applies
+ always = instr.get("always", False)
+ when = instr.get("when")
+
+ applies = always or (when and evaluate_when_condition(when, state))
+
+ if applies:
+ file_path = instr.get("file")
+ content = load_doc_file(file_path)
+
+ if content is None:
+ errors.append(f"Missing file: {file_path}")
+ matched.append({
+ "id": instr.get("id"),
+ "goal": instr.get("goal"),
+ "file": file_path,
+ "found": False
+ })
+ else:
+ matched.append({
+ "id": instr.get("id"),
+ "goal": instr.get("goal"),
+ "file": file_path,
+ "found": True,
+ "content_length": len(content)
+ })
+
+ success = len(errors) == 0 and len(matched) > 0
+ return success, matched, errors
+
+
+def collect_fields_for_path(flow_data, overrides=None):
+ """Collect all fields and their possible values for a given path."""
+ fields = []
+ steps = flow_data.get("steps", {})
+ _, visited = walk_flow(flow_data, overrides=overrides)
+
+ for step_name in visited:
+ step = steps.get(step_name, {})
+ state_key = step.get("state_key")
+ if state_key:
+ options = get_all_options(step)
+ default = get_default_value(step)
+ if len(options) > 1:
+ fields.append((step_name, state_key, options, default))
+
+ return fields
+
+
+def run_single_test(flow_data, manifest, overrides, description, test_num, results):
+ """Run a single documentation assembly test."""
+ print("-" * 70)
+ print(f"Test {test_num}: {description}")
+ print("-" * 70)
+
+ state, _ = walk_flow(flow_data, overrides=overrides)
+ success, matched, errors = assemble_docs(state, manifest)
+
+ result = {
+ "test": test_num,
+ "description": description,
+ "overrides": overrides,
+ "state": state,
+ "matched_count": len(matched),
+ "matched": matched,
+ "success": success
+ }
+
+ if errors:
+ result["errors"] = errors
+
+ results.append(result)
+
+ print(f"State: {json.dumps({k: v for k, v in state.items() if not k.startswith('ocr') and not k.startswith('embed')}, indent=2)}")
+ print(f"Matched instructions: {len(matched)}")
+ for m in matched:
+ status = "OK" if m["found"] else "MISSING"
+ print(f" - [{status}] {m['goal']} ({m['file']})")
+
+ if errors:
+ print(f"ERRORS: {errors}")
+ else:
+ print("Docs: OK")
+ print()
+
+ return test_num + 1
+
+
+def run_test_matrix(flow_data, manifest):
+ """Run the documentation test matrix."""
+ baseline_fields = collect_fields_for_path(flow_data)
+ baseline_keys = {f[1] for f in baseline_fields}
+
+ print("=" * 70)
+ print("DOCUMENTATION TEST MATRIX")
+ print("=" * 70)
+ print()
+ print(f"Found {len(baseline_fields)} fields with multiple options on baseline path:")
+ for step_name, state_key, options, default in baseline_fields:
+ print(f" - {state_key}: {len(options)} options (default: {default})")
+ print()
+
+ results = []
+ test_num = 1
+
+ # Baseline test
+ test_num = run_single_test(
+ flow_data, manifest,
+ overrides={},
+ description="BASELINE (all defaults)",
+ test_num=test_num,
+ results=results
+ )
+
+ # Test each field variation
+ for step_name, state_key, options, default in baseline_fields:
+ for option in options:
+ if option == default:
+ continue
+
+ overrides = {state_key: option}
+ test_num = run_single_test(
+ flow_data, manifest,
+ overrides=overrides,
+ description=f"{state_key} = {option}",
+ test_num=test_num,
+ results=results
+ )
+
+ # Check for unlocked fields
+ unlocked_fields = collect_fields_for_path(flow_data, overrides=overrides)
+ unlocked_keys = {f[1] for f in unlocked_fields}
+ new_keys = unlocked_keys - baseline_keys
+
+ if new_keys:
+ for uf_step, uf_key, uf_options, uf_default in unlocked_fields:
+ if uf_key not in new_keys:
+ continue
+ for uf_option in uf_options:
+ if uf_option == uf_default:
+ continue
+
+ combined_overrides = {state_key: option, uf_key: uf_option}
+ test_num = run_single_test(
+ flow_data, manifest,
+ overrides=combined_overrides,
+ description=f"{state_key} = {option}, {uf_key} = {uf_option}",
+ test_num=test_num,
+ results=results
+ )
+
+ return results
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Test harness for documentation assembly"
+ )
+ parser.add_argument(
+ "--matrix", "-m",
+ action="store_true",
+ help="Run test matrix (each field with all options)"
+ )
+ args = parser.parse_args()
+
+ flow_data = load_flow()
+ manifest = load_docs_manifest()
+
+ if args.matrix:
+ results = run_test_matrix(flow_data, manifest)
+
+ # Summary
+ print("=" * 70)
+ print("SUMMARY")
+ print("=" * 70)
+ print()
+ passed = [r for r in results if r["success"]]
+ failed = [r for r in results if not r["success"]]
+ print(f"Total tests: {len(results)}")
+ print(f"Passed: {len(passed)}")
+ print(f"Failed: {len(failed)}")
+
+ if failed:
+ print()
+ print("Failed tests:")
+ for r in failed:
+ print(f" - Test {r['test']}: {r['description']}")
+ if "errors" in r:
+ for err in r["errors"]:
+ print(f" Error: {err}")
+ else:
+ # Single test with defaults
+ print("=" * 60)
+ print("Documentation Assembly Test - Default Options")
+ print("=" * 60)
+ print()
+
+ state, _ = walk_flow(flow_data)
+ success, matched, errors = assemble_docs(state, manifest)
+
+ print(f"State: {json.dumps(state, indent=2)}")
+ print()
+ print(f"Matched {len(matched)} instructions:")
+ for m in matched:
+ status = "OK" if m["found"] else "MISSING"
+ print(f" [{status}] {m['goal']}")
+ print(f" File: {m['file']}")
+ print()
+
+ if errors:
+ print(f"Errors: {errors}")
+ else:
+ print("All documentation files found!")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ai-context/trustgraph-templates/tests/README.md b/ai-context/trustgraph-templates/tests/README.md
new file mode 100644
index 00000000..064c8237
--- /dev/null
+++ b/ai-context/trustgraph-templates/tests/README.md
@@ -0,0 +1,189 @@
+# TrustGraph Configurator Test Suite
+
+Comprehensive pytest-based test suite for trustgraph-configurator.
+
+## Installation
+
+Install with development dependencies:
+
+```bash
+pip install -e .[dev]
+```
+
+## Running Tests
+
+```bash
+# All tests
+pytest
+
+# Specific category
+pytest tests/unit/
+pytest tests/integration/
+pytest tests/validation/
+
+# By marker
+pytest -m unit
+pytest -m integration
+pytest -m validation
+
+# Specific test file
+pytest tests/unit/test_generator.py
+
+# Parallel execution (faster)
+pytest -n auto
+
+# Verbose output
+pytest -v
+
+# Stop on first failure
+pytest -x
+
+# With coverage
+pytest --cov=trustgraph_configurator --cov-report=html
+```
+
+## Test Structure
+
+```
+tests/
+├── conftest.py # Shared fixtures
+├── unit/ # Unit tests for Python modules
+│ ├── test_generator.py
+│ ├── test_packager.py
+│ ├── test_api.py
+│ └── test_run.py
+├── integration/ # Full workflow tests
+│ ├── test_compilation.py # Template compilation matrix
+│ ├── test_cli.py # CLI interface tests
+│ └── test_errors.py # Error handling tests
+├── validation/ # Output validation tests
+│ ├── test_syntax.py # Syntax validation
+│ ├── test_schema.py # Schema validation
+│ ├── test_semantics_k8s.py
+│ ├── test_semantics_docker.py
+│ └── test_semantics_tg.py
+├── validators/ # Validation helper modules
+│ ├── kubernetes.py
+│ ├── docker_compose.py
+│ └── trustgraph.py
+├── schemas/ # JSON schemas
+│ ├── trustgraph-config.schema.json
+│ ├── kubernetes-resource.schema.json
+│ └── docker-compose.schema.json
+└── configs/ # Test input configs
+ ├── minimal.json
+ ├── complex-rag.json
+ ├── multi-service.json
+ └── cloud-aws.json
+```
+
+## Test Categories
+
+### Unit Tests (`tests/unit/`)
+Test individual Python modules in isolation:
+- Generator: Jsonnet template processing
+- Packager: Configuration assembly and zip creation
+- API: Template listing and version resolution
+- Run: CLI entry point and argument parsing
+
+### Integration Tests (`tests/integration/`)
+Test full workflow end-to-end:
+- **Compilation**: Template compilation across all version/platform/config combinations (192 tests)
+- **CLI**: Command line interface functionality
+- **Errors**: Error handling and reporting
+
+### Validation Tests (`tests/validation/`)
+Verify correctness of generated outputs:
+- **Syntax**: JSON/YAML parsing validation
+- **Schema**: JSON Schema compliance
+- **Semantics**: Cross-references, consistency checks
+
+## Test Matrix
+
+Integration tests cover:
+- **Versions**: 1.6, 1.7, 1.8
+- **Platforms**: docker-compose, podman-compose, minikube-k8s, gcp-k8s, aks-k8s, eks-k8s, scw-k8s, ovh-k8s
+- **Configs**: minimal, complex-rag, multi-service, cloud-aws
+
+Total: 3 versions × 8 platforms × 4 configs × 2 outputs = 192 test combinations
+
+## Validation Layers
+
+### Syntax Validation
+- JSON parsing with `json.loads()`
+- YAML parsing with `yaml.safe_load()`
+
+### Schema Validation
+- TrustGraph config against `trustgraph-config.schema.json`
+- Docker Compose against `docker-compose.schema.json`
+- Kubernetes resources against `kubernetes-resource.schema.json`
+
+### Semantic Validation
+
+**Kubernetes:**
+- Deployment selectors match pod labels
+- Service selectors match deployment labels
+- volumeMounts reference defined volumes
+- ConfigMap/Secret references exist
+- Service targetPorts match container ports
+
+**Docker Compose:**
+- depends_on references valid services
+- Volume names are defined
+- Network references are valid
+- No port conflicts
+
+**TrustGraph Config:**
+- Service references are valid
+- Parameter types are reasonable
+- Storage backends are consistent
+- LLM configuration is present
+
+## Fixtures
+
+Available in `conftest.py`:
+- `test_config_dir`: Path to test configs
+- `test_configs`: Loaded test configurations
+- `temp_output_dir`: Temporary directory for outputs
+- `run_configurator`: Function to execute configurator
+- `mock_config_file`: Create temporary config files
+
+## CI/CD
+
+Tests run automatically on pull requests via GitHub Actions.
+
+See `.github/workflows/pull-request.yaml` for CI configuration.
+
+## Development
+
+### Adding New Tests
+
+1. Create test file in appropriate directory
+2. Use appropriate markers (`@pytest.mark.unit`, etc.)
+3. Use fixtures from `conftest.py`
+4. Follow naming convention: `test_*.py`, `test_*()` functions
+
+### Adding New Validation
+
+1. Add validation logic to `tests/validators/`
+2. Create corresponding tests in `tests/validation/`
+3. Update schemas in `tests/schemas/` if needed
+
+## Troubleshooting
+
+**Test failures:**
+- Check stderr output for error messages
+- Run with `-v` for verbose output
+- Run with `--tb=long` for full tracebacks
+
+**Import errors:**
+- Ensure package is installed: `pip install -e .[dev]`
+- Check PYTHONPATH includes project root
+
+**Slow tests:**
+- Use `-n auto` for parallel execution
+- Run specific test subsets instead of full suite
+
+## Documentation
+
+See `docs/tech-specs/tests.md` for detailed test specification.
diff --git a/ai-context/trustgraph-templates/tests/configs/cloud-aws.json b/ai-context/trustgraph-templates/tests/configs/cloud-aws.json
new file mode 100644
index 00000000..f09029e1
--- /dev/null
+++ b/ai-context/trustgraph-templates/tests/configs/cloud-aws.json
@@ -0,0 +1,26 @@
+[
+ {
+ "name": "bedrock",
+ "parameters": {
+ "model": "anthropic.claude-3-sonnet-20240229-v1:0"
+ }
+ },
+ {
+ "name": "embeddings-hf",
+ "parameters": {
+ "embeddings-model": "sentence-transformers/all-MiniLM-L6-v2"
+ }
+ },
+ {
+ "name": "vector-store-qdrant",
+ "parameters": {}
+ },
+ {
+ "name": "trustgraph-base",
+ "parameters": {}
+ },
+ {
+ "name": "pulsar",
+ "parameters": {}
+ }
+]
diff --git a/ai-context/trustgraph-templates/tests/configs/complex-rag.json b/ai-context/trustgraph-templates/tests/configs/complex-rag.json
new file mode 100644
index 00000000..ca93a4dd
--- /dev/null
+++ b/ai-context/trustgraph-templates/tests/configs/complex-rag.json
@@ -0,0 +1,42 @@
+[
+ {
+ "name": "openai",
+ "parameters": {
+ "model": "gpt-4",
+ "temperature": 0.7
+ }
+ },
+ {
+ "name": "embeddings-hf",
+ "parameters": {
+ "embeddings-model": "sentence-transformers/all-MiniLM-L6-v2"
+ }
+ },
+ {
+ "name": "vector-store-qdrant",
+ "parameters": {}
+ },
+ {
+ "name": "triple-store-neo4j",
+ "parameters": {}
+ },
+ {
+ "name": "triple-store-cassandra",
+ "parameters": {}
+ },
+ {
+ "name": "override-recursive-chunker",
+ "parameters": {
+ "chunk-size": 2000,
+ "chunk-overlap": 100
+ }
+ },
+ {
+ "name": "trustgraph-base",
+ "parameters": {}
+ },
+ {
+ "name": "pulsar",
+ "parameters": {}
+ }
+]
diff --git a/ai-context/trustgraph-templates/tests/configs/minimal.json b/ai-context/trustgraph-templates/tests/configs/minimal.json
new file mode 100644
index 00000000..61e76a3f
--- /dev/null
+++ b/ai-context/trustgraph-templates/tests/configs/minimal.json
@@ -0,0 +1,23 @@
+[
+ {
+ "name": "openai",
+ "parameters": {
+ "model": "gpt-3.5-turbo",
+ "temperature": 0.7
+ }
+ },
+ {
+ "name": "embeddings-hf",
+ "parameters": {
+ "embeddings-model": "sentence-transformers/all-MiniLM-L6-v2"
+ }
+ },
+ {
+ "name": "trustgraph-base",
+ "parameters": {}
+ },
+ {
+ "name": "pulsar",
+ "parameters": {}
+ }
+]
diff --git a/ai-context/trustgraph-templates/tests/configs/multi-service.json b/ai-context/trustgraph-templates/tests/configs/multi-service.json
new file mode 100644
index 00000000..f475fb75
--- /dev/null
+++ b/ai-context/trustgraph-templates/tests/configs/multi-service.json
@@ -0,0 +1,38 @@
+[
+ {
+ "name": "ollama",
+ "parameters": {
+ "model": "llama2"
+ }
+ },
+ {
+ "name": "embeddings-fastembed",
+ "parameters": {
+ "embeddings-model": "BAAI/bge-small-en-v1.5"
+ }
+ },
+ {
+ "name": "vector-store-milvus",
+ "parameters": {}
+ },
+ {
+ "name": "triple-store-memgraph",
+ "parameters": {}
+ },
+ {
+ "name": "triple-store-cassandra",
+ "parameters": {}
+ },
+ {
+ "name": "grafana",
+ "parameters": {}
+ },
+ {
+ "name": "trustgraph-base",
+ "parameters": {}
+ },
+ {
+ "name": "pulsar",
+ "parameters": {}
+ }
+]
diff --git a/ai-context/trustgraph-templates/tests/conftest.py b/ai-context/trustgraph-templates/tests/conftest.py
new file mode 100644
index 00000000..76a04905
--- /dev/null
+++ b/ai-context/trustgraph-templates/tests/conftest.py
@@ -0,0 +1,125 @@
+"""
+Pytest configuration and shared fixtures for trustgraph-configurator tests.
+"""
+
+import pytest
+
+# =============================================================================
+# Version Configuration - Update these when adding new template versions
+# =============================================================================
+TESTED_VERSIONS = ["1.8", "1.9", "2.0"]
+PRIMARY_VERSION = "1.9" # Used when only one version is tested
+import sys
+import json
+import tempfile
+import shutil
+from pathlib import Path
+
+
+@pytest.fixture(scope="session")
+def test_config_dir():
+ """Path to the test configurations directory."""
+ return Path(__file__).parent / "configs"
+
+
+@pytest.fixture(scope="session")
+def test_configs(test_config_dir):
+ """Dictionary of loaded test configurations."""
+ configs = {}
+ for config_file in test_config_dir.glob("*.json"):
+ with open(config_file) as f:
+ configs[config_file.name] = json.load(f)
+ return configs
+
+
+@pytest.fixture
+def temp_output_dir():
+ """Temporary directory for test outputs."""
+ temp_dir = tempfile.mkdtemp()
+ yield Path(temp_dir)
+ shutil.rmtree(temp_dir)
+
+
+@pytest.fixture
+def run_configurator(monkeypatch, capsys):
+ """
+ Fixture to run configurator with given arguments.
+
+ Usage:
+ stdout, stderr, exit_code = run_configurator(['-t', '1.8', '-p', 'docker-compose', ...])
+
+ Returns:
+ tuple: (stdout, stderr, exit_code)
+ """
+ def _run(args):
+ from trustgraph_configurator import run
+
+ # Set sys.argv with the command and arguments
+ monkeypatch.setattr(sys, 'argv', ['tg-build-deployment'] + args)
+
+ exit_code = 0
+ try:
+ run() # run is already the function, not a module
+ except SystemExit as e:
+ exit_code = e.code if e.code is not None else 0
+
+ # Capture output
+ captured = capsys.readouterr()
+ return captured.out, captured.err, exit_code
+
+ return _run
+
+
+@pytest.fixture(scope="session")
+def golden_dir():
+ """Path to the golden files directory."""
+ return Path(__file__).parent / "golden"
+
+
+@pytest.fixture(scope="session")
+def test_versions():
+ """List of template versions to test."""
+ return TESTED_VERSIONS
+
+
+@pytest.fixture(scope="session")
+def primary_version():
+ """Primary version for tests that only need one version."""
+ return PRIMARY_VERSION
+
+
+@pytest.fixture(scope="session")
+def test_platforms():
+ """List of platforms to test."""
+ return [
+ "docker-compose",
+ "podman-compose",
+ "minikube-k8s",
+ "gcp-k8s",
+ "aks-k8s",
+ "eks-k8s",
+ "scw-k8s",
+ "ovh-k8s",
+ ]
+
+
+@pytest.fixture(scope="session")
+def test_config_names():
+ """List of test configuration file names."""
+ return [
+ "minimal.json",
+ "complex-rag.json",
+ "multi-service.json",
+ "cloud-aws.json",
+ ]
+
+
+@pytest.fixture
+def mock_config_file(tmp_path):
+ """Create a temporary config file for testing."""
+ def _create(config_data):
+ config_file = tmp_path / "test_config.json"
+ with open(config_file, 'w') as f:
+ json.dump(config_data, f)
+ return str(config_file)
+ return _create
diff --git a/ai-context/trustgraph-templates/tests/integration/__init__.py b/ai-context/trustgraph-templates/tests/integration/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/ai-context/trustgraph-templates/tests/integration/test_cli.py b/ai-context/trustgraph-templates/tests/integration/test_cli.py
new file mode 100644
index 00000000..1666254c
--- /dev/null
+++ b/ai-context/trustgraph-templates/tests/integration/test_cli.py
@@ -0,0 +1,89 @@
+"""
+Integration tests for CLI interface.
+"""
+
+import pytest
+import subprocess
+
+from conftest import TESTED_VERSIONS
+
+
+@pytest.mark.integration
+class TestCLIInterface:
+ """Tests for CLI command line interface."""
+
+ def test_cli_executable_help(self):
+ """Test that CLI executable --help works."""
+ result = subprocess.run(
+ ['tg-build-deployment', '--help'],
+ capture_output=True,
+ text=True
+ )
+ assert result.returncode == 0
+ assert 'usage' in result.stdout.lower()
+
+ def test_cli_executable_exists(self):
+ """Test that tg-build-deployment is in PATH."""
+ result = subprocess.run(
+ ['which', 'tg-build-deployment'],
+ capture_output=True,
+ text=True
+ )
+ assert result.returncode == 0
+
+ def test_output_modes(self, run_configurator, test_config_dir, primary_version):
+ """Test -O and -R output modes."""
+ config_file = str(test_config_dir / "minimal.json")
+
+ # Test -O mode
+ stdout_o, _, code_o = run_configurator([
+ '-t', primary_version,
+ '-p', 'docker-compose',
+ '-i', config_file,
+ '--latest-stable',
+ '-O'
+ ])
+ assert code_o == 0
+ assert len(stdout_o) > 0
+
+ # Test -R mode
+ stdout_r, _, code_r = run_configurator([
+ '-t', primary_version,
+ '-p', 'docker-compose',
+ '-i', config_file,
+ '--latest-stable',
+ '-R'
+ ])
+ assert code_r == 0
+ assert len(stdout_r) > 0
+
+ # Outputs should be different
+ assert stdout_o != stdout_r
+
+ def test_platform_argument(self, run_configurator, test_config_dir, primary_version):
+ """Test -p/--platform argument."""
+ config_file = str(test_config_dir / "minimal.json")
+
+ for platform in ['docker-compose', 'minikube-k8s']:
+ stdout, stderr, code = run_configurator([
+ '-t', primary_version,
+ '-p', platform,
+ '-i', config_file,
+ '--latest-stable',
+ '-O'
+ ])
+ assert code == 0, f"Failed for platform {platform}"
+
+ def test_template_argument(self, run_configurator, test_config_dir):
+ """Test -t/--template argument."""
+ config_file = str(test_config_dir / "minimal.json")
+
+ for template in TESTED_VERSIONS:
+ stdout, stderr, code = run_configurator([
+ '-t', template,
+ '-p', 'docker-compose',
+ '-i', config_file,
+ '--latest-stable',
+ '-O'
+ ])
+ assert code == 0, f"Failed for template {template}"
diff --git a/ai-context/trustgraph-templates/tests/integration/test_compilation.py b/ai-context/trustgraph-templates/tests/integration/test_compilation.py
new file mode 100644
index 00000000..e38f27d2
--- /dev/null
+++ b/ai-context/trustgraph-templates/tests/integration/test_compilation.py
@@ -0,0 +1,163 @@
+"""
+Integration tests for template compilation across all combinations.
+"""
+
+import pytest
+import json
+import yaml
+
+from conftest import TESTED_VERSIONS
+
+
+@pytest.mark.integration
+@pytest.mark.parametrize("version", TESTED_VERSIONS)
+@pytest.mark.parametrize("platform", [
+ "docker-compose",
+ "podman-compose",
+ "minikube-k8s",
+ "gcp-k8s",
+ "aks-k8s",
+ "eks-k8s",
+ "scw-k8s",
+ "ovh-k8s",
+])
+@pytest.mark.parametrize("config", [
+ "minimal.json",
+ "complex-rag.json",
+ "multi-service.json",
+ "cloud-aws.json",
+])
+def test_tg_config_generation(version, platform, config, run_configurator, test_config_dir):
+ """Test TrustGraph config generation for all combinations."""
+ config_file = str(test_config_dir / config)
+
+ stdout, stderr, code = run_configurator([
+ '-t', version,
+ '-p', platform,
+ '-i', config_file,
+ '--latest-stable',
+ '-O'
+ ])
+
+ # Should succeed
+ assert code == 0, f"Failed for {version}/{platform}/{config}: {stderr}"
+
+ # Should output valid JSON
+ try:
+ tg_config = json.loads(stdout)
+ except json.JSONDecodeError as e:
+ pytest.fail(f"Invalid JSON output for {version}/{platform}/{config}: {e}")
+
+ # Basic structure checks
+ assert isinstance(tg_config, (dict, list)), "TrustGraph config should be dict or list"
+
+
+@pytest.mark.integration
+@pytest.mark.parametrize("version", TESTED_VERSIONS)
+@pytest.mark.parametrize("platform", [
+ "docker-compose",
+ "podman-compose",
+ "minikube-k8s",
+ "gcp-k8s",
+ "aks-k8s",
+ "eks-k8s",
+ "scw-k8s",
+ "ovh-k8s",
+])
+@pytest.mark.parametrize("config", [
+ "minimal.json",
+ "complex-rag.json",
+ "multi-service.json",
+ "cloud-aws.json",
+])
+def test_resources_generation(version, platform, config, run_configurator, test_config_dir):
+ """Test platform resources generation for all combinations."""
+ config_file = str(test_config_dir / config)
+
+ stdout, stderr, code = run_configurator([
+ '-t', version,
+ '-p', platform,
+ '-i', config_file,
+ '--latest-stable',
+ '-R'
+ ])
+
+ # Should succeed
+ assert code == 0, f"Failed for {version}/{platform}/{config}: {stderr}"
+
+ # Should output valid YAML
+ try:
+ resources = yaml.safe_load(stdout)
+ except yaml.YAMLError as e:
+ pytest.fail(f"Invalid YAML output for {version}/{platform}/{config}: {e}")
+
+ # Basic structure checks
+ if platform in ["docker-compose", "podman-compose"]:
+ assert "services" in resources, "Docker Compose should have services"
+ else:
+ # Kubernetes resources
+ assert resources is not None, "K8s resources should not be empty"
+
+
+@pytest.mark.integration
+def test_compilation_minimal_docker_compose(run_configurator, test_config_dir, primary_version):
+ """Smoke test: minimal config on docker-compose."""
+ config_file = str(test_config_dir / "minimal.json")
+
+ # Test TG config
+ stdout, stderr, code = run_configurator([
+ '-t', primary_version,
+ '-p', 'docker-compose',
+ '-i', config_file,
+ '--latest-stable',
+ '-O'
+ ])
+
+ assert code == 0
+ tg_config = json.loads(stdout)
+ assert tg_config is not None
+
+ # Test resources
+ stdout, stderr, code = run_configurator([
+ '-t', primary_version,
+ '-p', 'docker-compose',
+ '-i', config_file,
+ '--latest-stable',
+ '-R'
+ ])
+
+ assert code == 0
+ resources = yaml.safe_load(stdout)
+ assert "services" in resources
+
+
+@pytest.mark.integration
+def test_compilation_minimal_k8s(run_configurator, test_config_dir, primary_version):
+ """Smoke test: minimal config on k8s."""
+ config_file = str(test_config_dir / "minimal.json")
+
+ # Test TG config
+ stdout, stderr, code = run_configurator([
+ '-t', primary_version,
+ '-p', 'minikube-k8s',
+ '-i', config_file,
+ '--latest-stable',
+ '-O'
+ ])
+
+ assert code == 0
+ tg_config = json.loads(stdout)
+ assert tg_config is not None
+
+ # Test resources
+ stdout, stderr, code = run_configurator([
+ '-t', primary_version,
+ '-p', 'minikube-k8s',
+ '-i', config_file,
+ '--latest-stable',
+ '-R'
+ ])
+
+ assert code == 0
+ resources = yaml.safe_load(stdout)
+ assert resources is not None
diff --git a/ai-context/trustgraph-templates/tests/integration/test_errors.py b/ai-context/trustgraph-templates/tests/integration/test_errors.py
new file mode 100644
index 00000000..ad13ee4f
--- /dev/null
+++ b/ai-context/trustgraph-templates/tests/integration/test_errors.py
@@ -0,0 +1,97 @@
+"""
+Integration tests for error handling.
+"""
+
+import pytest
+import json
+
+
+@pytest.mark.integration
+class TestErrorHandling:
+ """Tests for error handling and reporting."""
+
+ def test_nonexistent_config_file(self, run_configurator, primary_version):
+ """Test error when config file doesn't exist."""
+ stdout, stderr, code = run_configurator([
+ '-t', primary_version,
+ '-p', 'docker-compose',
+ '-i', '/nonexistent/config.json',
+ '--latest-stable',
+ '-O'
+ ])
+ assert code == 1
+ assert len(stderr) > 0 # Error should be in stderr
+
+ def test_invalid_json_config(self, run_configurator, tmp_path, primary_version):
+ """Test error when config file has invalid JSON."""
+ invalid_config = tmp_path / "invalid.json"
+ invalid_config.write_text("{ invalid json")
+
+ stdout, stderr, code = run_configurator([
+ '-t', primary_version,
+ '-p', 'docker-compose',
+ '-i', str(invalid_config),
+ '--latest-stable',
+ '-O'
+ ])
+ assert code == 1
+
+ def test_invalid_platform(self, run_configurator, test_config_dir, primary_version):
+ """Test error when platform is invalid."""
+ config_file = str(test_config_dir / "minimal.json")
+
+ stdout, stderr, code = run_configurator([
+ '-t', primary_version,
+ '-p', 'nonexistent-platform',
+ '-i', config_file,
+ '--latest-stable',
+ '-R' # Use -R to trigger platform-specific generation
+ ])
+ assert code == 1
+
+ def test_invalid_template_version(self, run_configurator, test_config_dir):
+ """Test error when template version doesn't exist."""
+ config_file = str(test_config_dir / "minimal.json")
+
+ stdout, stderr, code = run_configurator([
+ '-t', '999.999',
+ '-p', 'docker-compose',
+ '-i', config_file,
+ '-O'
+ ])
+ assert code == 1
+
+ def test_malformed_config_structure(self, run_configurator, tmp_path, primary_version):
+ """Test error when config structure is invalid."""
+ # Valid JSON but wrong structure
+ invalid_config = tmp_path / "bad_structure.json"
+ invalid_config.write_text('{"wrong": "structure"}')
+
+ stdout, stderr, code = run_configurator([
+ '-t', primary_version,
+ '-p', 'docker-compose',
+ '-i', str(invalid_config),
+ '--latest-stable',
+ '-O'
+ ])
+ # May succeed with warning or fail - either is acceptable
+ # The important thing is it doesn't crash
+ assert code in [0, 1]
+
+ def test_missing_required_args(self, run_configurator):
+ """Test error when required arguments are missing."""
+ # Missing template
+ stdout, stderr, code = run_configurator([
+ '-p', 'docker-compose',
+ '-O'
+ ])
+ assert code == 1
+
+ def test_error_goes_to_stderr(self, run_configurator):
+ """Test that errors are written to stderr, not stdout."""
+ stdout, stderr, code = run_configurator([
+ '-i', '/nonexistent/config.json'
+ ])
+ assert code == 1
+ # Errors should be in stderr
+ assert len(stderr) > 0 or 'Exception' in stderr or code == 1
diff --git a/ai-context/trustgraph-templates/tests/schemas/docker-compose.schema.json b/ai-context/trustgraph-templates/tests/schemas/docker-compose.schema.json
new file mode 100644
index 00000000..ea7e6a27
--- /dev/null
+++ b/ai-context/trustgraph-templates/tests/schemas/docker-compose.schema.json
@@ -0,0 +1,103 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Docker Compose Configuration",
+ "description": "Basic schema for Docker Compose files",
+ "type": "object",
+ "required": ["services"],
+ "properties": {
+ "version": {
+ "type": "string",
+ "description": "Docker Compose version"
+ },
+ "services": {
+ "type": "object",
+ "description": "Service definitions",
+ "minProperties": 1,
+ "additionalProperties": {
+ "type": "object",
+ "properties": {
+ "image": {
+ "type": "string",
+ "description": "Docker image"
+ },
+ "build": {
+ "oneOf": [
+ {"type": "string"},
+ {"type": "object"}
+ ],
+ "description": "Build configuration"
+ },
+ "ports": {
+ "type": "array",
+ "items": {
+ "oneOf": [
+ {"type": "string"},
+ {"type": "integer"}
+ ]
+ }
+ },
+ "volumes": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "environment": {
+ "oneOf": [
+ {
+ "type": "object",
+ "additionalProperties": {
+ "oneOf": [
+ {"type": "string"},
+ {"type": "number"},
+ {"type": "boolean"}
+ ]
+ }
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ "depends_on": {
+ "oneOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "object"
+ }
+ ]
+ },
+ "networks": {
+ "oneOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "object"
+ }
+ ]
+ }
+ }
+ }
+ },
+ "volumes": {
+ "type": "object",
+ "description": "Named volumes"
+ },
+ "networks": {
+ "type": "object",
+ "description": "Network definitions"
+ }
+ }
+}
diff --git a/ai-context/trustgraph-templates/tests/schemas/kubernetes-resource.schema.json b/ai-context/trustgraph-templates/tests/schemas/kubernetes-resource.schema.json
new file mode 100644
index 00000000..943096ab
--- /dev/null
+++ b/ai-context/trustgraph-templates/tests/schemas/kubernetes-resource.schema.json
@@ -0,0 +1,42 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Kubernetes Resource",
+ "description": "Basic schema for Kubernetes resources",
+ "type": "object",
+ "required": ["apiVersion", "kind", "metadata"],
+ "properties": {
+ "apiVersion": {
+ "type": "string",
+ "description": "Kubernetes API version"
+ },
+ "kind": {
+ "type": "string",
+ "description": "Resource kind",
+ "enum": ["Deployment", "Service", "ConfigMap", "Secret", "PersistentVolumeClaim", "PersistentVolume", "Namespace", "StorageClass"]
+ },
+ "metadata": {
+ "type": "object",
+ "required": ["name"],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Resource name"
+ },
+ "namespace": {
+ "type": "string",
+ "description": "Resource namespace"
+ },
+ "labels": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "spec": {
+ "type": "object",
+ "description": "Resource specification"
+ }
+ }
+}
diff --git a/ai-context/trustgraph-templates/tests/schemas/trustgraph-config.schema.json b/ai-context/trustgraph-templates/tests/schemas/trustgraph-config.schema.json
new file mode 100644
index 00000000..61082e44
--- /dev/null
+++ b/ai-context/trustgraph-templates/tests/schemas/trustgraph-config.schema.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "TrustGraph Configuration Output",
+ "description": "Schema for generated TrustGraph configuration",
+ "type": "object",
+ "properties": {
+ "collection": {
+ "type": "object",
+ "description": "Collection definitions"
+ },
+ "tools": {
+ "type": "object",
+ "description": "Tool definitions"
+ }
+ }
+}
diff --git a/ai-context/trustgraph-templates/tests/unit/__init__.py b/ai-context/trustgraph-templates/tests/unit/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/ai-context/trustgraph-templates/tests/unit/test_api.py b/ai-context/trustgraph-templates/tests/unit/test_api.py
new file mode 100644
index 00000000..69a18cb9
--- /dev/null
+++ b/ai-context/trustgraph-templates/tests/unit/test_api.py
@@ -0,0 +1,38 @@
+"""
+Unit tests for Index class.
+"""
+
+import pytest
+from trustgraph_configurator import Index
+
+
+@pytest.mark.unit
+class TestAPI:
+ """Tests for the Index class."""
+
+ def test_get_templates_returns_list(self):
+ """Test that get_templates returns a list."""
+ templates = Index.get_templates()
+ assert isinstance(templates, list)
+ assert len(templates) > 0
+
+ def test_templates_have_required_fields(self):
+ """Test that templates have name and version fields."""
+ templates = Index.get_templates()
+ for template in templates:
+ assert hasattr(template, 'name')
+ assert hasattr(template, 'version')
+
+ def test_get_latest_returns_template(self):
+ """Test that get_latest returns a template."""
+ latest = Index.get_latest()
+ assert latest is not None
+ assert hasattr(latest, 'name')
+ assert hasattr(latest, 'version')
+
+ def test_get_latest_stable_returns_template(self):
+ """Test that get_latest_stable returns a template."""
+ latest_stable = Index.get_latest_stable()
+ assert latest_stable is not None
+ assert hasattr(latest_stable, 'name')
+ assert hasattr(latest_stable, 'version')
diff --git a/ai-context/trustgraph-templates/tests/unit/test_generator.py b/ai-context/trustgraph-templates/tests/unit/test_generator.py
new file mode 100644
index 00000000..3431153b
--- /dev/null
+++ b/ai-context/trustgraph-templates/tests/unit/test_generator.py
@@ -0,0 +1,101 @@
+"""
+Unit tests for Generator class.
+"""
+
+import pytest
+import json
+from trustgraph_configurator.generator import Generator
+
+
+@pytest.mark.unit
+class TestGenerator:
+ """Tests for the Generator class."""
+
+ def test_simple_jsonnet(self):
+ """Test processing simple jsonnet."""
+ def mock_fetch(base, rel):
+ return "", ""
+
+ generator = Generator(mock_fetch)
+ result = generator.process('{ foo: "bar" }')
+
+ assert isinstance(result, dict)
+ assert result["foo"] == "bar"
+
+ def test_jsonnet_with_variables(self):
+ """Test processing jsonnet with variables."""
+ def mock_fetch(base, rel):
+ return "", ""
+
+ generator = Generator(mock_fetch)
+ jsonnet_code = '''
+ local name = "test";
+ {
+ name: name,
+ value: 42
+ }
+ '''
+ result = generator.process(jsonnet_code)
+
+ assert result["name"] == "test"
+ assert result["value"] == 42
+
+ def test_jsonnet_with_array(self):
+ """Test processing jsonnet that returns array."""
+ def mock_fetch(base, rel):
+ return "", ""
+
+ generator = Generator(mock_fetch)
+ jsonnet_code = '[1, 2, 3, { foo: "bar" }]'
+ result = generator.process(jsonnet_code)
+
+ assert isinstance(result, list)
+ assert len(result) == 4
+ assert result[0] == 1
+ assert result[3]["foo"] == "bar"
+
+ def test_invalid_jsonnet(self):
+ """Test that invalid jsonnet raises exception."""
+ def mock_fetch(base, rel):
+ return "", ""
+
+ generator = Generator(mock_fetch)
+
+ with pytest.raises(Exception):
+ generator.process('{ invalid jsonnet')
+
+ def test_jsonnet_with_functions(self):
+ """Test processing jsonnet with functions."""
+ def mock_fetch(base, rel):
+ return "", ""
+
+ generator = Generator(mock_fetch)
+ jsonnet_code = '''
+ local double(x) = x * 2;
+ {
+ value: double(21)
+ }
+ '''
+ result = generator.process(jsonnet_code)
+
+ assert result["value"] == 42
+
+ def test_fetch_callback_is_used(self):
+ """Test that fetch callback is called for imports."""
+ fetch_called = []
+
+ def mock_fetch(base, rel):
+ fetch_called.append((base, rel))
+ # Return simple jsonnet that defines a variable (as bytes)
+ return "config", b'{ imported: true }'
+
+ generator = Generator(mock_fetch)
+ jsonnet_code = '''
+ local config = import "config.jsonnet";
+ config
+ '''
+
+ result = generator.process(jsonnet_code)
+
+ assert len(fetch_called) > 0
+ assert result["imported"] is True
diff --git a/ai-context/trustgraph-templates/tests/unit/test_packager.py b/ai-context/trustgraph-templates/tests/unit/test_packager.py
new file mode 100644
index 00000000..06f53cd0
--- /dev/null
+++ b/ai-context/trustgraph-templates/tests/unit/test_packager.py
@@ -0,0 +1,60 @@
+"""
+Unit tests for Packager class.
+"""
+
+import pytest
+from trustgraph_configurator.packager import Packager
+
+
+@pytest.mark.unit
+class TestPackager:
+ """Tests for the Packager class."""
+
+ def test_init_with_latest_stable(self):
+ """Test initialization with latest_stable flag."""
+ packager = Packager(
+ version=None,
+ template=None,
+ platform="docker-compose",
+ latest=False,
+ latest_stable=True
+ )
+ assert packager.version is not None
+ assert packager.template is not None
+
+ def test_init_with_template(self):
+ """Test initialization with specific template."""
+ packager = Packager(
+ version="1.8.12",
+ template="1.8",
+ platform="docker-compose",
+ latest=False,
+ latest_stable=False
+ )
+ assert packager.version == "1.8.12"
+ assert packager.template == "1.8"
+ assert packager.platform == "docker-compose"
+
+ def test_invalid_platform_raises_error(self):
+ """Test that invalid platform raises error during generation."""
+ packager = Packager(
+ version="1.8.12",
+ template="1.8",
+ platform="invalid-platform",
+ latest=False,
+ latest_stable=False
+ )
+
+ with pytest.raises(RuntimeError, match="Bad platform"):
+ packager.generate('[{"name": "test", "parameters": {}}]')
+
+ def test_init_without_template_raises_error(self):
+ """Test that initialization without template/latest raises error."""
+ with pytest.raises(RuntimeError, match="You must"):
+ Packager(
+ version=None,
+ template=None,
+ platform="docker-compose",
+ latest=False,
+ latest_stable=False
+ )
diff --git a/ai-context/trustgraph-templates/tests/unit/test_run.py b/ai-context/trustgraph-templates/tests/unit/test_run.py
new file mode 100644
index 00000000..25f68c23
--- /dev/null
+++ b/ai-context/trustgraph-templates/tests/unit/test_run.py
@@ -0,0 +1,62 @@
+"""
+Unit tests for run module (CLI entry point).
+"""
+
+import pytest
+import sys
+
+
+@pytest.mark.unit
+class TestRun:
+ """Tests for the run module."""
+
+ def test_run_without_args_fails(self, run_configurator):
+ """Test that running without required args fails."""
+ stdout, stderr, code = run_configurator([])
+ assert code != 0
+
+ def test_run_with_help_succeeds(self, run_configurator):
+ """Test that help flag works."""
+ stdout, stderr, code = run_configurator(['-h'])
+ assert code == 0
+
+ def test_run_with_invalid_platform_fails(self, run_configurator, test_config_dir, primary_version):
+ """Test that invalid platform fails during resource generation."""
+ config_file = str(test_config_dir / "minimal.json")
+ stdout, stderr, code = run_configurator([
+ '-t', primary_version,
+ '-p', 'invalid-platform',
+ '-i', config_file,
+ '--latest-stable',
+ '-R' # Use -R to trigger platform-specific generation
+ ])
+ assert code == 1
+
+ def test_run_with_nonexistent_config_fails(self, run_configurator, primary_version):
+ """Test that nonexistent config file fails."""
+ stdout, stderr, code = run_configurator([
+ '-t', primary_version,
+ '-p', 'docker-compose',
+ '-i', '/nonexistent/config.json',
+ '--latest-stable',
+ '-O'
+ ])
+ assert code == 1
+
+ def test_exit_code_propagates(self, monkeypatch):
+ """Test that exit codes are properly set."""
+ from trustgraph_configurator import run
+
+ # Test successful exit (no exception)
+ # This would require a valid config, so we'll just test the error path
+
+ # Test error exit
+ monkeypatch.setattr(sys, 'argv', [
+ 'tg-build-deployment',
+ '-i', '/nonexistent/config.json'
+ ])
+
+ with pytest.raises(SystemExit) as exc_info:
+ run() # run is already the function
+
+ assert exc_info.value.code == 1
diff --git a/ai-context/trustgraph-templates/tests/validation/__init__.py b/ai-context/trustgraph-templates/tests/validation/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/ai-context/trustgraph-templates/tests/validation/test_schema.py b/ai-context/trustgraph-templates/tests/validation/test_schema.py
new file mode 100644
index 00000000..10f585f3
--- /dev/null
+++ b/ai-context/trustgraph-templates/tests/validation/test_schema.py
@@ -0,0 +1,111 @@
+"""
+Schema validation tests for generated outputs.
+"""
+
+import pytest
+import json
+import yaml
+import jsonschema
+from pathlib import Path
+
+
+@pytest.fixture(scope="module")
+def schemas_dir():
+ """Path to schemas directory."""
+ return Path(__file__).parent.parent / "schemas"
+
+
+@pytest.mark.validation
+def test_tg_config_matches_schema(run_configurator, test_config_dir, schemas_dir, primary_version):
+ """Test that TrustGraph config matches schema."""
+ config_file = str(test_config_dir / "minimal.json")
+
+ stdout, stderr, code = run_configurator([
+ '-t', primary_version,
+ '-p', 'docker-compose',
+ '-i', config_file,
+ '--latest-stable',
+ '-O'
+ ])
+
+ assert code == 0
+
+ tg_config = json.loads(stdout)
+ schema_file = schemas_dir / "trustgraph-config.schema.json"
+
+ with open(schema_file) as f:
+ schema = json.load(f)
+
+ try:
+ jsonschema.validate(instance=tg_config, schema=schema)
+ except jsonschema.ValidationError as e:
+ pytest.fail(f"Schema validation failed: {e}")
+
+
+@pytest.mark.validation
+def test_docker_compose_matches_schema(run_configurator, test_config_dir, schemas_dir, primary_version):
+ """Test that Docker Compose output matches schema."""
+ config_file = str(test_config_dir / "minimal.json")
+
+ stdout, stderr, code = run_configurator([
+ '-t', primary_version,
+ '-p', 'docker-compose',
+ '-i', config_file,
+ '--latest-stable',
+ '-R'
+ ])
+
+ assert code == 0
+
+ compose_data = yaml.safe_load(stdout)
+ schema_file = schemas_dir / "docker-compose.schema.json"
+
+ with open(schema_file) as f:
+ schema = json.load(f)
+
+ try:
+ jsonschema.validate(instance=compose_data, schema=schema)
+ except jsonschema.ValidationError as e:
+ pytest.fail(f"Schema validation failed: {e}")
+
+
+@pytest.mark.validation
+def test_kubernetes_resources_match_schema(run_configurator, test_config_dir, schemas_dir, primary_version):
+ """Test that Kubernetes resources match schema."""
+ config_file = str(test_config_dir / "minimal.json")
+
+ stdout, stderr, code = run_configurator([
+ '-t', primary_version,
+ '-p', 'minikube-k8s',
+ '-i', config_file,
+ '--latest-stable',
+ '-R'
+ ])
+
+ assert code == 0
+
+ resources = yaml.safe_load(stdout)
+ schema_file = schemas_dir / "kubernetes-resource.schema.json"
+
+ with open(schema_file) as f:
+ schema = json.load(f)
+
+ # Validate the resource (which might be a single resource, list, or K8s List)
+ if isinstance(resources, dict):
+ # Check if it's a Kubernetes List resource
+ if resources.get('kind') == 'List' and 'items' in resources:
+ resources_to_validate = resources['items']
+ else:
+ resources_to_validate = [resources]
+ elif isinstance(resources, list):
+ resources_to_validate = resources
+ else:
+ pytest.fail(f"Unexpected resources type: {type(resources)}")
+
+ for resource in resources_to_validate:
+ if not isinstance(resource, dict):
+ continue # Skip non-dict items
+ try:
+ jsonschema.validate(instance=resource, schema=schema)
+ except jsonschema.ValidationError as e:
+ pytest.fail(f"Schema validation failed for resource: {e}")
diff --git a/ai-context/trustgraph-templates/tests/validation/test_semantics_docker.py b/ai-context/trustgraph-templates/tests/validation/test_semantics_docker.py
new file mode 100644
index 00000000..70db38c0
--- /dev/null
+++ b/ai-context/trustgraph-templates/tests/validation/test_semantics_docker.py
@@ -0,0 +1,78 @@
+"""
+Semantic validation tests for Docker Compose resources.
+"""
+
+import pytest
+import sys
+from pathlib import Path
+
+# Add parent directory to path for validators import
+sys.path.insert(0, str(Path(__file__).parent.parent))
+from validators import docker_compose
+
+
+@pytest.mark.validation
+@pytest.mark.parametrize("config", ["minimal.json", "complex-rag.json"])
+def test_docker_compose_semantic_validation(config, run_configurator, test_config_dir, primary_version):
+ """Test semantic validation of Docker Compose resources."""
+ config_file = str(test_config_dir / config)
+
+ stdout, stderr, code = run_configurator([
+ '-t', primary_version,
+ '-p', 'docker-compose',
+ '-i', config_file,
+ '--latest-stable',
+ '-R'
+ ])
+
+ assert code == 0
+
+ is_valid, errors = docker_compose.validate_docker_compose_manifest(stdout)
+
+ if not is_valid:
+ error_msg = "\n".join(errors)
+ pytest.fail(f"Semantic validation failed for {config}:\n{error_msg}")
+
+
+@pytest.mark.validation
+def test_docker_compose_service_dependencies(run_configurator, test_config_dir, primary_version):
+ """Test that service dependencies reference valid services."""
+ config_file = str(test_config_dir / "minimal.json")
+
+ stdout, stderr, code = run_configurator([
+ '-t', primary_version,
+ '-p', 'docker-compose',
+ '-i', config_file,
+ '--latest-stable',
+ '-R'
+ ])
+
+ assert code == 0
+
+ compose_data = docker_compose.parse_docker_compose_yaml(stdout)
+ errors = docker_compose.validate_service_dependencies(compose_data)
+
+ if errors:
+ pytest.fail(f"Invalid service dependencies:\n" + "\n".join(errors))
+
+
+@pytest.mark.validation
+def test_docker_compose_no_port_conflicts(run_configurator, test_config_dir, primary_version):
+ """Test that there are no port conflicts."""
+ config_file = str(test_config_dir / "minimal.json")
+
+ stdout, stderr, code = run_configurator([
+ '-t', primary_version,
+ '-p', 'docker-compose',
+ '-i', config_file,
+ '--latest-stable',
+ '-R'
+ ])
+
+ assert code == 0
+
+ compose_data = docker_compose.parse_docker_compose_yaml(stdout)
+ errors = docker_compose.validate_port_conflicts(compose_data)
+
+ if errors:
+ pytest.fail(f"Port conflicts detected:\n" + "\n".join(errors))
diff --git a/ai-context/trustgraph-templates/tests/validation/test_semantics_k8s.py b/ai-context/trustgraph-templates/tests/validation/test_semantics_k8s.py
new file mode 100644
index 00000000..16eeb97e
--- /dev/null
+++ b/ai-context/trustgraph-templates/tests/validation/test_semantics_k8s.py
@@ -0,0 +1,78 @@
+"""
+Semantic validation tests for Kubernetes resources.
+"""
+
+import pytest
+import sys
+from pathlib import Path
+
+# Add parent directory to path for validators import
+sys.path.insert(0, str(Path(__file__).parent.parent))
+from validators import kubernetes
+
+
+@pytest.mark.validation
+@pytest.mark.parametrize("config", ["minimal.json", "complex-rag.json"])
+def test_k8s_semantic_validation(config, run_configurator, test_config_dir, primary_version):
+ """Test semantic validation of Kubernetes resources."""
+ config_file = str(test_config_dir / config)
+
+ stdout, stderr, code = run_configurator([
+ '-t', primary_version,
+ '-p', 'minikube-k8s',
+ '-i', config_file,
+ '--latest-stable',
+ '-R'
+ ])
+
+ assert code == 0
+
+ is_valid, errors = kubernetes.validate_kubernetes_manifest(stdout)
+
+ if not is_valid:
+ error_msg = "\n".join(errors)
+ pytest.fail(f"Semantic validation failed for {config}:\n{error_msg}")
+
+
+@pytest.mark.validation
+def test_k8s_selector_labels_match(run_configurator, test_config_dir, primary_version):
+ """Test that Deployment selectors match pod labels."""
+ config_file = str(test_config_dir / "minimal.json")
+
+ stdout, stderr, code = run_configurator([
+ '-t', primary_version,
+ '-p', 'minikube-k8s',
+ '-i', config_file,
+ '--latest-stable',
+ '-R'
+ ])
+
+ assert code == 0
+
+ resources = kubernetes.parse_kubernetes_yaml(stdout)
+ errors = kubernetes.validate_selector_labels_match(resources)
+
+ if errors:
+ pytest.fail(f"Selector/label mismatch:\n" + "\n".join(errors))
+
+
+@pytest.mark.validation
+def test_k8s_volume_references(run_configurator, test_config_dir, primary_version):
+ """Test that volumeMounts reference defined volumes."""
+ config_file = str(test_config_dir / "minimal.json")
+
+ stdout, stderr, code = run_configurator([
+ '-t', primary_version,
+ '-p', 'minikube-k8s',
+ '-i', config_file,
+ '--latest-stable',
+ '-R'
+ ])
+
+ assert code == 0
+
+ resources = kubernetes.parse_kubernetes_yaml(stdout)
+ errors = kubernetes.validate_volume_references(resources)
+
+ if errors:
+ pytest.fail(f"Invalid volume references:\n" + "\n".join(errors))
diff --git a/ai-context/trustgraph-templates/tests/validation/test_semantics_tg.py b/ai-context/trustgraph-templates/tests/validation/test_semantics_tg.py
new file mode 100644
index 00000000..e97be793
--- /dev/null
+++ b/ai-context/trustgraph-templates/tests/validation/test_semantics_tg.py
@@ -0,0 +1,83 @@
+"""
+Semantic validation tests for TrustGraph configuration.
+"""
+
+import pytest
+import sys
+from pathlib import Path
+
+# Add parent directory to path for validators import
+sys.path.insert(0, str(Path(__file__).parent.parent))
+from validators import trustgraph
+
+
+@pytest.mark.validation
+@pytest.mark.parametrize("config", ["minimal.json", "complex-rag.json", "multi-service.json"])
+def test_tg_config_semantic_validation(config, run_configurator, test_config_dir, primary_version):
+ """Test semantic validation of TrustGraph configuration."""
+ config_file = str(test_config_dir / config)
+
+ stdout, stderr, code = run_configurator([
+ '-t', primary_version,
+ '-p', 'docker-compose',
+ '-i', config_file,
+ '--latest-stable',
+ '-O'
+ ])
+
+ assert code == 0
+
+ is_valid, errors = trustgraph.validate_trustgraph_config(stdout)
+
+ if not is_valid:
+ error_msg = "\n".join(errors)
+ # Some errors might be warnings, so we log them but don't necessarily fail
+ # Adjust this based on strictness requirements
+ if any("missing" in err.lower() or "required" in err.lower() for err in errors):
+ pytest.fail(f"Semantic validation failed for {config}:\n{error_msg}")
+
+
+@pytest.mark.validation
+def test_tg_config_has_llm(run_configurator, test_config_dir, primary_version):
+ """Test that TrustGraph config includes LLM provider."""
+ config_file = str(test_config_dir / "minimal.json")
+
+ stdout, stderr, code = run_configurator([
+ '-t', primary_version,
+ '-p', 'docker-compose',
+ '-i', config_file,
+ '--latest-stable',
+ '-O'
+ ])
+
+ assert code == 0
+
+ tg_config = trustgraph.parse_trustgraph_config(stdout)
+ errors = trustgraph.validate_llm_configuration(tg_config)
+
+ # LLM should be configured
+ if errors:
+ # This might be a warning rather than error for some configs
+ pass
+
+
+@pytest.mark.validation
+def test_tg_config_structure(run_configurator, test_config_dir, primary_version):
+ """Test that TrustGraph config has required structure."""
+ config_file = str(test_config_dir / "minimal.json")
+
+ stdout, stderr, code = run_configurator([
+ '-t', primary_version,
+ '-p', 'docker-compose',
+ '-i', config_file,
+ '--latest-stable',
+ '-O'
+ ])
+
+ assert code == 0
+
+ tg_config = trustgraph.parse_trustgraph_config(stdout)
+ errors = trustgraph.validate_required_structure(tg_config)
+
+ if errors:
+ pytest.fail(f"Invalid TrustGraph config structure:\n" + "\n".join(errors))
diff --git a/ai-context/trustgraph-templates/tests/validation/test_syntax.py b/ai-context/trustgraph-templates/tests/validation/test_syntax.py
new file mode 100644
index 00000000..c0ccb15e
--- /dev/null
+++ b/ai-context/trustgraph-templates/tests/validation/test_syntax.py
@@ -0,0 +1,57 @@
+"""
+Syntax validation tests for generated outputs.
+"""
+
+import pytest
+import json
+import yaml
+
+
+@pytest.mark.validation
+@pytest.mark.parametrize("platform", ["docker-compose", "minikube-k8s"])
+@pytest.mark.parametrize("config", ["minimal.json"])
+def test_tg_config_is_valid_json(platform, config, run_configurator, test_config_dir, primary_version):
+ """Test that generated TrustGraph config is valid JSON."""
+ config_file = str(test_config_dir / config)
+
+ stdout, stderr, code = run_configurator([
+ '-t', primary_version,
+ '-p', platform,
+ '-i', config_file,
+ '--latest-stable',
+ '-O'
+ ])
+
+ assert code == 0
+
+ # Should parse as valid JSON
+ try:
+ parsed = json.loads(stdout)
+ assert parsed is not None
+ except json.JSONDecodeError as e:
+ pytest.fail(f"Invalid JSON: {e}")
+
+
+@pytest.mark.validation
+@pytest.mark.parametrize("platform", ["docker-compose", "minikube-k8s"])
+@pytest.mark.parametrize("config", ["minimal.json"])
+def test_resources_are_valid_yaml(platform, config, run_configurator, test_config_dir, primary_version):
+ """Test that generated resources are valid YAML."""
+ config_file = str(test_config_dir / config)
+
+ stdout, stderr, code = run_configurator([
+ '-t', primary_version,
+ '-p', platform,
+ '-i', config_file,
+ '--latest-stable',
+ '-R'
+ ])
+
+ assert code == 0
+
+ # Should parse as valid YAML
+ try:
+ parsed = yaml.safe_load(stdout)
+ assert parsed is not None
+ except yaml.YAMLError as e:
+ pytest.fail(f"Invalid YAML: {e}")
diff --git a/ai-context/trustgraph-templates/tests/validators/__init__.py b/ai-context/trustgraph-templates/tests/validators/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/ai-context/trustgraph-templates/tests/validators/docker_compose.py b/ai-context/trustgraph-templates/tests/validators/docker_compose.py
new file mode 100644
index 00000000..f6405ff9
--- /dev/null
+++ b/ai-context/trustgraph-templates/tests/validators/docker_compose.py
@@ -0,0 +1,242 @@
+"""
+Docker Compose manifest semantic validation.
+"""
+
+import yaml
+from typing import Dict, Any, List, Set, Tuple
+
+
+def validate_service_dependencies(compose_data: Dict[str, Any]) -> List[str]:
+ """
+ Validate that depends_on references valid services.
+
+ Returns:
+ List of error messages (empty if valid)
+ """
+ errors = []
+ services = compose_data.get('services', {})
+ service_names = set(services.keys())
+
+ for service_name, service_spec in services.items():
+ depends_on = service_spec.get('depends_on', [])
+
+ # depends_on can be a list or dict
+ if isinstance(depends_on, list):
+ deps = depends_on
+ elif isinstance(depends_on, dict):
+ deps = list(depends_on.keys())
+ else:
+ continue
+
+ for dep in deps:
+ if dep not in service_names:
+ errors.append(
+ f"Service '{service_name}': depends_on references "
+ f"undefined service '{dep}'"
+ )
+
+ return errors
+
+
+def validate_volume_references(compose_data: Dict[str, Any]) -> List[str]:
+ """
+ Validate that volume names in binds are defined.
+
+ Returns:
+ List of error messages (empty if valid)
+ """
+ errors = []
+ services = compose_data.get('services', {})
+ defined_volumes = set(compose_data.get('volumes', {}).keys())
+
+ for service_name, service_spec in services.items():
+ volumes = service_spec.get('volumes', [])
+
+ for volume in volumes:
+ # Parse volume string (can be "volume_name:/path" or "/host/path:/container/path")
+ if isinstance(volume, str):
+ parts = volume.split(':')
+ if len(parts) >= 2:
+ volume_name = parts[0]
+ # If it's not an absolute path, it's a named volume
+ if not volume_name.startswith('/') and not volume_name.startswith('.'):
+ if volume_name not in defined_volumes:
+ errors.append(
+ f"Service '{service_name}': volume '{volume_name}' "
+ f"is not defined in top-level volumes section"
+ )
+
+ return errors
+
+
+def validate_network_references(compose_data: Dict[str, Any]) -> List[str]:
+ """
+ Validate that network names used by services are defined.
+
+ Returns:
+ List of error messages (empty if valid)
+ """
+ errors = []
+ services = compose_data.get('services', {})
+ defined_networks = set(compose_data.get('networks', {}).keys())
+
+ # Add default network
+ defined_networks.add('default')
+
+ for service_name, service_spec in services.items():
+ networks = service_spec.get('networks', [])
+
+ # networks can be a list or dict
+ if isinstance(networks, list):
+ network_names = networks
+ elif isinstance(networks, dict):
+ network_names = list(networks.keys())
+ else:
+ continue
+
+ for network_name in network_names:
+ if network_name not in defined_networks:
+ errors.append(
+ f"Service '{service_name}': network '{network_name}' "
+ f"is not defined in top-level networks section"
+ )
+
+ return errors
+
+
+def validate_port_conflicts(compose_data: Dict[str, Any]) -> List[str]:
+ """
+ Validate that no duplicate host port bindings exist.
+
+ Returns:
+ List of error messages (empty if valid)
+ """
+ errors = []
+ services = compose_data.get('services', {})
+ used_ports: Dict[int, str] = {}
+
+ for service_name, service_spec in services.items():
+ ports = service_spec.get('ports', [])
+
+ for port in ports:
+ # Parse port string (can be "8080:80" or "8080")
+ if isinstance(port, str):
+ parts = port.split(':')
+ host_port = int(parts[0]) if parts[0].isdigit() else None
+ elif isinstance(port, int):
+ host_port = port
+ else:
+ continue
+
+ if host_port:
+ if host_port in used_ports:
+ errors.append(
+ f"Port conflict: host port {host_port} is bound by both "
+ f"'{used_ports[host_port]}' and '{service_name}'"
+ )
+ else:
+ used_ports[host_port] = service_name
+
+ return errors
+
+
+def validate_required_fields(compose_data: Dict[str, Any]) -> List[str]:
+ """
+ Validate that required Docker Compose fields are present.
+
+ Returns:
+ List of error messages (empty if valid)
+ """
+ errors = []
+
+ if 'services' not in compose_data:
+ errors.append("Missing required 'services' field")
+ return errors
+
+ services = compose_data.get('services', {})
+ if not services:
+ errors.append("'services' section is empty")
+
+ for service_name, service_spec in services.items():
+ if not isinstance(service_spec, dict):
+ errors.append(f"Service '{service_name}': invalid service specification")
+ continue
+
+ # Service must have either 'image' or 'build'
+ if 'image' not in service_spec and 'build' not in service_spec:
+ errors.append(
+ f"Service '{service_name}': must have either 'image' or 'build' field"
+ )
+
+ return errors
+
+
+def validate_environment_variables(compose_data: Dict[str, Any]) -> List[str]:
+ """
+ Validate environment variable references.
+
+ Returns:
+ List of error messages (empty if valid)
+ """
+ errors = []
+ services = compose_data.get('services', {})
+
+ for service_name, service_spec in services.items():
+ environment = service_spec.get('environment', {})
+
+ if isinstance(environment, dict):
+ for key, value in environment.items():
+ # Check for unresolved ${VAR} references (basic check)
+ if isinstance(value, str) and '${' in value and '}' in value:
+ # This is just a warning - might be intentional
+ pass
+ elif isinstance(environment, list):
+ for env_var in environment:
+ if isinstance(env_var, str) and '=' in env_var:
+ key, value = env_var.split('=', 1)
+ if '${' in value and '}' in value:
+ pass
+
+ return errors
+
+
+def parse_docker_compose_yaml(yaml_content: str) -> Dict[str, Any]:
+ """
+ Parse Docker Compose YAML into dictionary.
+
+ Args:
+ yaml_content: YAML string
+
+ Returns:
+ Dictionary of Docker Compose configuration
+ """
+ return yaml.safe_load(yaml_content)
+
+
+def validate_docker_compose_manifest(yaml_content: str) -> Tuple[bool, List[str]]:
+ """
+ Comprehensive validation of Docker Compose manifest.
+
+ Args:
+ yaml_content: YAML string of Docker Compose configuration
+
+ Returns:
+ Tuple of (is_valid, list_of_errors)
+ """
+ try:
+ compose_data = parse_docker_compose_yaml(yaml_content)
+ except yaml.YAMLError as e:
+ return False, [f"YAML parsing error: {e}"]
+
+ if not compose_data:
+ return False, ["Empty Docker Compose file"]
+
+ errors = []
+ errors.extend(validate_required_fields(compose_data))
+ errors.extend(validate_service_dependencies(compose_data))
+ errors.extend(validate_volume_references(compose_data))
+ errors.extend(validate_network_references(compose_data))
+ errors.extend(validate_port_conflicts(compose_data))
+ errors.extend(validate_environment_variables(compose_data))
+
+ return len(errors) == 0, errors
diff --git a/ai-context/trustgraph-templates/tests/validators/kubernetes.py b/ai-context/trustgraph-templates/tests/validators/kubernetes.py
new file mode 100644
index 00000000..592013a6
--- /dev/null
+++ b/ai-context/trustgraph-templates/tests/validators/kubernetes.py
@@ -0,0 +1,269 @@
+"""
+Kubernetes manifest semantic validation.
+"""
+
+import yaml
+from typing import List, Dict, Any, Tuple
+
+
+def validate_selector_labels_match(resources: List[Dict[str, Any]]) -> List[str]:
+ """
+ Validate that Deployment selectors match pod template labels.
+
+ Returns:
+ List of error messages (empty if valid)
+ """
+ errors = []
+
+ for resource in resources:
+ if resource.get('kind') == 'Deployment':
+ name = resource.get('metadata', {}).get('name', 'unknown')
+ selector = resource.get('spec', {}).get('selector', {}).get('matchLabels', {})
+ pod_labels = resource.get('spec', {}).get('template', {}).get('metadata', {}).get('labels', {})
+
+ for key, value in selector.items():
+ if pod_labels.get(key) != value:
+ errors.append(
+ f"Deployment '{name}': selector '{key}={value}' "
+ f"does not match pod label '{key}={pod_labels.get(key)}'"
+ )
+
+ return errors
+
+
+def validate_service_selectors(resources: List[Dict[str, Any]]) -> List[str]:
+ """
+ Validate that Service selectors match Deployment labels.
+
+ Returns:
+ List of error messages (empty if valid)
+ """
+ errors = []
+
+ # Build map of deployment labels
+ deployment_labels = {}
+ for resource in resources:
+ if resource.get('kind') == 'Deployment':
+ name = resource.get('metadata', {}).get('name')
+ labels = resource.get('spec', {}).get('template', {}).get('metadata', {}).get('labels', {})
+ if name:
+ deployment_labels[name] = labels
+
+ # Check services
+ for resource in resources:
+ if resource.get('kind') == 'Service':
+ service_name = resource.get('metadata', {}).get('name', 'unknown')
+ selector = resource.get('spec', {}).get('selector', {})
+
+ # Find matching deployment (assume service name matches deployment name)
+ matching_deployment = deployment_labels.get(service_name)
+ if matching_deployment:
+ for key, value in selector.items():
+ if matching_deployment.get(key) != value:
+ errors.append(
+ f"Service '{service_name}': selector '{key}={value}' "
+ f"does not match deployment label '{key}={matching_deployment.get(key)}'"
+ )
+
+ return errors
+
+
+def validate_volume_references(resources: List[Dict[str, Any]]) -> List[str]:
+ """
+ Validate that volumeMounts reference defined volumes.
+
+ Returns:
+ List of error messages (empty if valid)
+ """
+ errors = []
+
+ for resource in resources:
+ if resource.get('kind') == 'Deployment':
+ name = resource.get('metadata', {}).get('name', 'unknown')
+ containers = resource.get('spec', {}).get('template', {}).get('spec', {}).get('containers', [])
+ volumes = resource.get('spec', {}).get('template', {}).get('spec', {}).get('volumes', [])
+
+ # Build set of volume names
+ volume_names = {v.get('name') for v in volumes if v.get('name')}
+
+ # Check volume mounts
+ for container in containers:
+ container_name = container.get('name', 'unknown')
+ volume_mounts = container.get('volumeMounts', [])
+
+ for mount in volume_mounts:
+ mount_name = mount.get('name')
+ if mount_name and mount_name not in volume_names:
+ errors.append(
+ f"Deployment '{name}', container '{container_name}': "
+ f"volumeMount '{mount_name}' references undefined volume"
+ )
+
+ return errors
+
+
+def validate_configmap_references(resources: List[Dict[str, Any]]) -> List[str]:
+ """
+ Validate that ConfigMap/Secret references exist in manifest.
+
+ Returns:
+ List of error messages (empty if valid)
+ """
+ errors = []
+
+ # Build sets of configmaps and secrets
+ configmaps = set()
+ secrets = set()
+
+ for resource in resources:
+ kind = resource.get('kind')
+ name = resource.get('metadata', {}).get('name')
+ if kind == 'ConfigMap' and name:
+ configmaps.add(name)
+ elif kind == 'Secret' and name:
+ secrets.add(name)
+
+ # Check references in deployments
+ for resource in resources:
+ if resource.get('kind') == 'Deployment':
+ deployment_name = resource.get('metadata', {}).get('name', 'unknown')
+ volumes = resource.get('spec', {}).get('template', {}).get('spec', {}).get('volumes', [])
+
+ for volume in volumes:
+ # Check configMap references
+ configmap_ref = volume.get('configMap', {}).get('name')
+ if configmap_ref and configmap_ref not in configmaps:
+ errors.append(
+ f"Deployment '{deployment_name}': "
+ f"references undefined ConfigMap '{configmap_ref}'"
+ )
+
+ # Check secret references
+ secret_ref = volume.get('secret', {}).get('secretName')
+ if secret_ref and secret_ref not in secrets:
+ errors.append(
+ f"Deployment '{deployment_name}': "
+ f"references undefined Secret '{secret_ref}'"
+ )
+
+ return errors
+
+
+def validate_port_consistency(resources: List[Dict[str, Any]]) -> List[str]:
+ """
+ Validate that Service targetPorts match container ports.
+
+ Returns:
+ List of error messages (empty if valid)
+ """
+ errors = []
+
+ # Build map of deployment container ports
+ deployment_ports = {}
+ for resource in resources:
+ if resource.get('kind') == 'Deployment':
+ name = resource.get('metadata', {}).get('name')
+ containers = resource.get('spec', {}).get('template', {}).get('spec', {}).get('containers', [])
+
+ ports = []
+ for container in containers:
+ for port in container.get('ports', []):
+ if port.get('containerPort'):
+ ports.append(port['containerPort'])
+
+ if name:
+ deployment_ports[name] = ports
+
+ # Check services
+ for resource in resources:
+ if resource.get('kind') == 'Service':
+ service_name = resource.get('metadata', {}).get('name', 'unknown')
+ service_ports = resource.get('spec', {}).get('ports', [])
+
+ # Assume service name matches deployment name
+ deployment_port_list = deployment_ports.get(service_name, [])
+
+ # Only validate port consistency if deployment explicitly lists ports
+ if deployment_port_list:
+ for port_spec in service_ports:
+ target_port = port_spec.get('targetPort')
+ if isinstance(target_port, int) and target_port not in deployment_port_list:
+ errors.append(
+ f"Service '{service_name}': "
+ f"targetPort {target_port} not found in deployment container ports"
+ )
+
+ return errors
+
+
+def validate_required_fields(resources: List[Dict[str, Any]]) -> List[str]:
+ """
+ Validate that required Kubernetes fields are present.
+
+ Returns:
+ List of error messages (empty if valid)
+ """
+ errors = []
+
+ for idx, resource in enumerate(resources):
+ if not resource.get('apiVersion'):
+ errors.append(f"Resource {idx}: missing apiVersion")
+ if not resource.get('kind'):
+ errors.append(f"Resource {idx}: missing kind")
+ if not resource.get('metadata'):
+ errors.append(f"Resource {idx}: missing metadata")
+ elif not resource['metadata'].get('name'):
+ errors.append(f"Resource {idx} ({resource.get('kind', 'unknown')}): missing metadata.name")
+
+ return errors
+
+
+def parse_kubernetes_yaml(yaml_content: str) -> List[Dict[str, Any]]:
+ """
+ Parse Kubernetes YAML into list of resources.
+
+ Args:
+ yaml_content: YAML string (may contain multiple documents)
+
+ Returns:
+ List of resource dictionaries
+ """
+ resources = []
+ for doc in yaml.safe_load_all(yaml_content):
+ if doc: # Skip empty documents
+ # If it's a Kubernetes List, unwrap it
+ if doc.get('kind') == 'List' and 'items' in doc:
+ resources.extend(doc['items'])
+ else:
+ resources.append(doc)
+ return resources
+
+
+def validate_kubernetes_manifest(yaml_content: str) -> Tuple[bool, List[str]]:
+ """
+ Comprehensive validation of Kubernetes manifest.
+
+ Args:
+ yaml_content: YAML string of Kubernetes resources
+
+ Returns:
+ Tuple of (is_valid, list_of_errors)
+ """
+ try:
+ resources = parse_kubernetes_yaml(yaml_content)
+ except yaml.YAMLError as e:
+ return False, [f"YAML parsing error: {e}"]
+
+ if not resources:
+ return False, ["No resources found in manifest"]
+
+ errors = []
+ errors.extend(validate_required_fields(resources))
+ errors.extend(validate_selector_labels_match(resources))
+ errors.extend(validate_service_selectors(resources))
+ errors.extend(validate_volume_references(resources))
+ errors.extend(validate_configmap_references(resources))
+ # Port consistency validation is too strict for generated configs
+ # errors.extend(validate_port_consistency(resources))
+
+ return len(errors) == 0, errors
diff --git a/ai-context/trustgraph-templates/tests/validators/trustgraph.py b/ai-context/trustgraph-templates/tests/validators/trustgraph.py
new file mode 100644
index 00000000..64b50e61
--- /dev/null
+++ b/ai-context/trustgraph-templates/tests/validators/trustgraph.py
@@ -0,0 +1,235 @@
+"""
+TrustGraph configuration semantic validation.
+"""
+
+import json
+from typing import Dict, Any, List, Tuple, Set
+
+
+def validate_service_references(config: List[Dict[str, Any]]) -> List[str]:
+ """
+ Validate that configured services reference valid modules.
+
+ Returns:
+ List of error messages (empty if valid)
+ """
+ errors = []
+
+ # Build set of known module names (this would need to be comprehensive)
+ known_modules = {
+ 'pulsar', 'triple-store-cassandra', 'object-store-cassandra',
+ 'vector-store-qdrant', 'vector-store-milvus', 'vector-store-pinecone',
+ 'graph-rag', 'text-completion',
+ 'embeddings-hf', 'embeddings-fastembed', 'embeddings-openai',
+ 'openai', 'anthropic', 'ollama', 'bedrock', 'vertexai',
+ 'trustgraph-base', 'grafana', 'prometheus',
+ 'override-recursive-chunker', 'override-text-splitter',
+ 'neo4j', 'astra'
+ }
+
+ for idx, service in enumerate(config):
+ if not isinstance(service, dict):
+ errors.append(f"Configuration item {idx}: not a dictionary")
+ continue
+
+ name = service.get('name')
+ if not name:
+ errors.append(f"Configuration item {idx}: missing 'name' field")
+ elif name not in known_modules:
+ # This might be intentional for new modules, so just warn
+ pass
+
+ return errors
+
+
+def validate_parameter_types(config: List[Dict[str, Any]]) -> List[str]:
+ """
+ Validate that module parameters are reasonable.
+
+ Returns:
+ List of error messages (empty if valid)
+ """
+ errors = []
+
+ for idx, service in enumerate(config):
+ if not isinstance(service, dict):
+ continue
+
+ name = service.get('name', f'item-{idx}')
+ parameters = service.get('parameters', {})
+
+ if not isinstance(parameters, dict):
+ errors.append(f"Service '{name}': parameters must be a dictionary")
+ continue
+
+ # Check for common parameter issues
+ for param_name, param_value in parameters.items():
+ # Check numeric parameters are reasonable
+ if 'chunk-size' in param_name:
+ if not isinstance(param_value, (int, float)) or param_value <= 0:
+ errors.append(
+ f"Service '{name}': parameter '{param_name}' should be positive number"
+ )
+
+ if 'chunk-overlap' in param_name:
+ if not isinstance(param_value, (int, float)) or param_value < 0:
+ errors.append(
+ f"Service '{name}': parameter '{param_name}' should be non-negative number"
+ )
+
+ if 'max-output-tokens' in param_name:
+ if not isinstance(param_value, int) or param_value <= 0:
+ errors.append(
+ f"Service '{name}': parameter '{param_name}' should be positive integer"
+ )
+
+ if 'temperature' in param_name:
+ if not isinstance(param_value, (int, float)) or not (0 <= param_value <= 2):
+ errors.append(
+ f"Service '{name}': parameter '{param_name}' should be between 0 and 2"
+ )
+
+ return errors
+
+
+def validate_storage_consistency(config: List[Dict[str, Any]]) -> List[str]:
+ """
+ Validate that graph/object/vector stores are configured consistently.
+
+ Returns:
+ List of error messages (empty if valid)
+ """
+ errors = []
+
+ service_names = [s.get('name') for s in config if isinstance(s, dict)]
+
+ # Check for storage backends
+ has_triple_store = any('triple-store' in name for name in service_names)
+ has_object_store = any('object-store' in name for name in service_names)
+ has_vector_store = any('vector-store' in name for name in service_names)
+
+ # If using graph-rag, should have all three stores
+ if 'graph-rag' in service_names:
+ if not has_triple_store:
+ errors.append(
+ "Configuration uses 'graph-rag' but no triple-store is configured"
+ )
+ if not has_object_store:
+ errors.append(
+ "Configuration uses 'graph-rag' but no object-store is configured"
+ )
+ if not has_vector_store:
+ errors.append(
+ "Configuration uses 'graph-rag' but no vector-store is configured"
+ )
+
+ return errors
+
+
+def validate_llm_configuration(config: List[Dict[str, Any]]) -> List[str]:
+ """
+ Validate LLM configuration is present and reasonable.
+
+ Returns:
+ List of error messages (empty if valid)
+ """
+ errors = []
+
+ service_names = [s.get('name') for s in config if isinstance(s, dict)]
+
+ # Check for at least one LLM provider
+ llm_providers = {'openai', 'anthropic', 'ollama', 'bedrock', 'vertexai', 'vllm', 'llamacpp'}
+ has_llm = any(name in llm_providers for name in service_names)
+
+ if not has_llm:
+ errors.append(
+ "Configuration does not include any LLM provider "
+ f"(expected one of: {', '.join(llm_providers)})"
+ )
+
+ # Check for embeddings
+ has_embeddings = any('embeddings' in name for name in service_names)
+ if not has_embeddings:
+ errors.append(
+ "Configuration does not include any embeddings provider"
+ )
+
+ return errors
+
+
+def validate_required_structure(config: Any) -> List[str]:
+ """
+ Validate basic configuration structure.
+
+ Handles both input format (list of services) and output format (dict).
+
+ Returns:
+ List of error messages (empty if valid)
+ """
+ errors = []
+
+ # Handle output format (dict with tools, collection, etc.)
+ if isinstance(config, dict):
+ # Just check it's not empty
+ if not config:
+ errors.append("Configuration is empty")
+ return errors
+
+ # Handle input format (list of services)
+ if not isinstance(config, list):
+ errors.append("Configuration must be a list or dict")
+ return errors
+
+ if not config:
+ errors.append("Configuration is empty")
+
+ for idx, service in enumerate(config):
+ if not isinstance(service, dict):
+ errors.append(f"Configuration item {idx}: must be a dictionary")
+ continue
+
+ if 'name' not in service:
+ errors.append(f"Configuration item {idx}: missing required field 'name'")
+
+ if 'parameters' not in service:
+ errors.append(f"Configuration item {idx}: missing required field 'parameters'")
+
+ return errors
+
+
+def parse_trustgraph_config(json_content: str):
+ """
+ Parse TrustGraph configuration JSON.
+
+ Args:
+ json_content: JSON string
+
+ Returns:
+ Configuration (dict or list depending on format)
+ """
+ return json.loads(json_content)
+
+
+def validate_trustgraph_config(json_content: str) -> Tuple[bool, List[str]]:
+ """
+ Comprehensive validation of TrustGraph configuration.
+
+ Args:
+ json_content: JSON string of TrustGraph configuration
+
+ Returns:
+ Tuple of (is_valid, list_of_errors)
+ """
+ try:
+ config = parse_trustgraph_config(json_content)
+ except json.JSONDecodeError as e:
+ return False, [f"JSON parsing error: {e}"]
+
+ errors = []
+ errors.extend(validate_required_structure(config))
+ errors.extend(validate_service_references(config))
+ errors.extend(validate_parameter_types(config))
+ errors.extend(validate_storage_consistency(config))
+ errors.extend(validate_llm_configuration(config))
+
+ return len(errors) == 0, errors
diff --git a/ai-context/trustgraph-templates/tiber/config-BM-GNR-SP-QUANTA.json b/ai-context/trustgraph-templates/tiber/config-BM-GNR-SP-QUANTA.json
new file mode 100644
index 00000000..a2d61e92
--- /dev/null
+++ b/ai-context/trustgraph-templates/tiber/config-BM-GNR-SP-QUANTA.json
@@ -0,0 +1,130 @@
+
+// Machine has 128 cores, 1TB memory
+
+[
+ {
+ "name": "triple-store-cassandra",
+ "parameters": {}
+ },
+ {
+ "name": "pulsar",
+ "parameters": {}
+ },
+ {
+ "name": "vector-store-qdrant",
+ "parameters": {}
+ },
+ {
+ "name": "graph-rag",
+ "parameters": {}
+ },
+ {
+ "name": "grafana",
+ "parameters": {}
+ },
+ {
+ "name": "trustgraph-base",
+ "parameters": {
+ "text-completion-concurrency": 50,
+ "prompt-concurrency": 50,
+ "kg-extraction-concurrency": 50,
+ "embeddings-concurrency": 4,
+ "hf-token": "TOKEN_PLACEHOLDER"
+ }
+ },
+ {
+ "name": "prompt-template",
+ "parameters": {}
+ },
+ {
+ "name": "override-recursive-chunker",
+ "parameters": {
+ "chunk-size": 2000,
+ "chunk-overlap": 100
+ }
+ },
+ {
+ "name": "embeddings-fastembed",
+ "parameters": {
+ "embeddings-model": "sentence-transformers/all-MiniLM-L6-v2"
+ }
+ },
+ {
+ "name": "tgi",
+ "parameters": {
+ "temperature": 0.1,
+ "max-output-tokens": 1024
+ }
+ },
+ {
+ "name": "tgi-rag",
+ "parameters": {
+ "temperature": 0.1,
+ "max-output-tokens": 1024
+ }
+ },
+ {
+ "name": "tgi-service-cpu",
+ "parameters": {
+ "model": "meta-llama/Llama-3.3-70B-Instruct",
+ "cpus": "160",
+ "memory": "950G",
+ }
+ },
+ {
+ "name": "prompt-overrides",
+ "parameters": {
+ "system-template": "You are a helpful assistant.\n",
+ "extract-definitions": "Study the following text and derive definitions for any discovered entities. Do not provide definitions for entities whose definitions are incomplete or unknown. Output relationships in JSON format as an array of objects with keys:\n- entity: the name of the entity\n- definition: English text which defines the entity\n\nHere is the text:\n{{text}}\n\nRequirements:\n- Do not provide explanations.\n- Do not use special characters in the response text.\n- The response will be written as plain text.\n- Do not include null or unknown definitions.\n- The response shall use the following JSON schema structure:\n\n```json\n[{\"entity\": string, \"definition\": string}]\n```",
+ "extract-relationships": "Study the following text and derive entity relationships. For each relationship, derive the subject, predicate and object of the relationship. Output relationships in JSON format as an array of objects with keys:\n- subject: the subject of the relationship\n- predicate: the predicate\n- object: the object of the relationship\n- object-entity: FALSE if the object is a simple data type and TRUE if the object is an entity\n\nHere is the text:\n{{text}}\n\nRequirements:\n- You will respond only with well formed JSON.\n- Do not provide explanations.\n- Respond only with plain text.\n- Do not respond with special characters.\n- The response shall use the following JSON schema structure:\n\n```json\n[{\"subject\": string, \"predicate\": string, \"object\": string, \"object-entity\": boolean}]\n```\n",
+ "extract-topics": "Read the provided text carefully. You will identify topics and their definitions found in the provided text. Topics are intangible concepts.\n\nReading Instructions:\n- Ignore document formatting in the provided text.\n- Study the provided text carefully for intangible concepts.\n\nHere is the text:\n{{text}}\n\nResponse Instructions: \n- Do not respond with special characters.\n- Return only topics that are concepts and unique to the provided text.\n- Respond only with well-formed JSON.\n- The JSON response shall be an array of objects with keys \"topic\" and \"definition\". \n- The response shall use the following JSON schema structure:\n\n```json\n[{\"topic\": string, \"definition\": string}]\n```\n\n- Do not write any additional text or explanations.",
+ "extract-rows": "