-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.html
More file actions
354 lines (340 loc) · 26.9 KB
/
Copy pathindex.html
File metadata and controls
354 lines (340 loc) · 26.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ruvector — Vectors, crystallized into a navigable lattice.</title>
<meta name="description" content="Vectors, crystallized into a navigable lattice.">
<meta name="author" content="Independent explainer for ruvnet's ruvector — built by Stuart Kerr at ISOvision.ai">
<link rel="canonical" href="https://ruvector-explainer.netlify.app/">
<meta name="theme-color" content="#7c6cff">
<meta property="og:type" content="website">
<meta property="og:title" content="ruvector — Vectors, crystallized into a navigable lattice.">
<meta property="og:description" content="Vectors, crystallized into a navigable lattice.">
<meta property="og:url" content="https://ruvector-explainer.netlify.app/">
<meta property="og:image" content="https://ruvector-explainer.netlify.app/assets/social-card.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="ruvector — Vectors, crystallized into a navigable lattice.">
<meta name="twitter:description" content="Vectors, crystallized into a navigable lattice.">
<meta name="twitter:image" content="https://ruvector-explainer.netlify.app/assets/social-card.png">
<link rel="icon" type="image/png" sizes="16x16" href="assets/favicon-16.png">
<link rel="icon" type="image/png" sizes="32x32" href="assets/favicon-32.png">
<link rel="icon" type="image/png" sizes="48x48" href="assets/favicon-48.png">
<link rel="icon" type="image/png" sizes="192x192" href="assets/favicon-192.png">
<link rel="icon" type="image/png" sizes="512x512" href="assets/favicon-512.png">
<link rel="icon" href="assets/favicon.ico" sizes="any">
<link rel="apple-touch-icon" href="assets/apple-touch-icon.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;500;600;700&family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap">
<link rel="stylesheet" href="styles.css">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "ruvector",
"description": "Vectors, crystallized into a navigable lattice.",
"url": "https://ruvector-explainer.netlify.app/",
"applicationCategory": "DeveloperApplication",
"operatingSystem": "Cross-platform",
"image": "https://ruvector-explainer.netlify.app/assets/social-card.png",
"author": {
"@type": "Person",
"name": "ruvnet"
},
"codeRepository": "https://github.com/ruvnet/ruvector",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
}
}
</script>
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<div class="prov-banner">
<div class="wrap prov-banner-inner">
<p class="prov-attr">An independent explainer for <strong>ruvnet</strong>'s <a href="https://github.com/ruvnet/ruvector" target="_blank" rel="noopener">ruvector</a> — built to help you actually implement it.</p>
<p class="prov-live"><span class="prov-dot" aria-hidden="true">●</span> source <code>github.com/ruvnet/ruvector</code></p>
</div>
</div>
<header class="site-head">
<div class="wrap">
<a class="brand" href="#top" aria-label="ruvector explainer home">
<svg class="logo" viewBox="0 0 32 32" aria-hidden="true">
<rect x="3" y="3" width="26" height="26" rx="8" fill="none" stroke="var(--accent)" stroke-width="2"></rect>
<rect x="9" y="9" width="14" height="14" rx="4" fill="var(--accent-2)"></rect>
<circle cx="16" cy="16" r="3" fill="var(--on-accent)"></circle>
</svg>
<span>ruvector</span>
</a>
<nav class="nav-links" aria-label="Sections">
<a href="#what-it-is">What it is</a>
<a href="#how-it-works">How it works</a>
<a href="#use-cases">Use cases</a>
<a href="#get-started">Get started</a>
<a href="#the-pack">AI pack</a>
<a href="https://github.com/ruvnet/ruvector" target="_blank" rel="noopener">GitHub ↗</a>
</nav>
</div>
</header>
<main id="main">
<a id="top"></a>
<section class="hero">
<div class="wrap">
<div class="hero-grid" style="grid-template-columns:1fr;max-width:60rem">
<div>
<span class="eyebrow">One project, three jobs: find, sync, coordinate</span>
<h1>One core does the searching, the syncing, and the coordinating</h1>
<p class="lede">ruvector is a toolkit — written in Rust (a systems programming language built for speed and safety) and shipped through npm (JavaScript's standard package installer) — for storing huge numbers of short numeric fingerprints, the compressed stand-ins computers use for the meaning of a photo, a sentence, or a product, and finding the closest ones in a crowd of millions, in milliseconds.</p>
<p class="sub">278 components sharing one dependency structure, reachable from a command-line tool or called directly from code.</p>
<p class="attrib-lede">An <strong>independent explainer</strong> for <strong>rUv <span class="attrib-handle">(ruvnet)</span></strong>'s <a href="https://github.com/ruvnet/ruvector" target="_blank" rel="noopener">ruvector</a> — built to take you from "never seen it" to "ready to implement".</p>
<div class="cta-row">
<a class="cta" href="#get-started">Get started</a>
<a class="cta ghost" href="https://github.com/ruvnet/ruvector" target="_blank" rel="noopener">View on GitHub</a>
</div>
<div class="meta-row"><span><b>Ecosystems</b> Rust + npm</span><span><b>Components</b> 278</span><span><b>Public symbols</b> 47,138</span></div>
</div>
</div>
</div>
</section>
<div class="sections">
<div class="wrap">
<details class="section" id="problem" open>
<summary>
<span class="num">01</span>
<span class="head-text">
<h2>Every fast, reliable, AI-assisted app starts as three separate problems</h2>
<span class="q">Why should I care?</span>
</span>
<span class="chev" aria-hidden="true">›</span>
</summary>
<div class="body">
<p class="lead-in">Say you're building something that has to remember a lot — every product a shopper looked at, every support ticket ever filed, every paragraph in a document library — and instantly bring back the handful of items relevant to what someone just asked. A plain list won't do that. Comparing a new question against a million old records one at a time gets slower every day the pile grows, until a search that should take a blink takes seconds, then minutes.</p>
<p>Now say the app also can't go down. If it runs on one machine and that machine dies, every user waiting on it waits with it. Keeping a second, third, or tenth copy of the same data — all correct, all in agreement, even when the network between them is unreliable — is a different kind of hard problem than search. It's the kind where two machines can each believe they hold the truth, disagree, and quietly corrupt the data if nobody catches it.</p>
<p>And say part of the job is better done by more than one worker at once — one piece reviewing code for problems, another piece deciding how the system should fit together, a third piece checking the result before it ships. Coordinating several AI helpers so they don't duplicate work, forget what the others found, or overwrite each other's output is a third problem again, with its own ways to fail.</p>
<p>None of these three problems shares an off-the-shelf answer. Most teams solve them with three or four different open-source projects, glued together by hand, built by people who never talked to each other and never tested their seams under real failure.</p>
<figure class="figure">
<span class="tier friendly">The problem</span>
<button type="button" class="diagram-zoom" data-src="assets/problem.png" data-alt="ruvector: the problem" aria-label="Enlarge: ruvector: the problem"><img src="assets/problem.png" alt="ruvector: the problem" loading="lazy"></button>
</figure>
</div>
</details>
<details class="section" id="what-it-is" open>
<summary>
<span class="num">02</span>
<span class="head-text">
<h2>A search engine, a reliability layer, and a small AI workforce — packaged as one project</h2>
<span class="q">What is this, plainly?</span>
</span>
<span class="chev" aria-hidden="true">›</span>
</summary>
<div class="body">
<p class="lead-in">ruvector is what's usually called a vector search engine: a system built to compare those numeric fingerprints and return the closest matches, fast, out of collections that can run into the millions. That's its core.</p>
<p>Around that core, the same project ships a distributed database layer — the part that spreads a copy of your data across more than one machine, so one machine going offline doesn't take your search down with it — built from the same underlying pieces as the search engine itself, not bolted on afterward.</p>
<p>It also ships a set of AI agents: small, specialized AI programs with a defined job, ready to be assigned work and report back — one reviews code for quality problems, another designs how a system should fit together — instead of leaving one assistant to do everything in sequence.</p>
<p>All of it lives in one repository: 278 components, individually buildable pieces in Rust or as npm packages, sharing one dependency structure — so the search engine, the reliability layer, and the agents are built to expect each other, not discovered as compatible by accident.</p>
<table class="tbl"><caption class="visually-hidden">The pieces, roughly, and what backs each one</caption>
<thead><tr><th>Layer</th><th>What it does</th></tr></thead>
<tbody>
<tr><td>Search core</td><td>Stores fingerprints, finds the closest matches fast — ruvector-core, ruvector-cli, ruvector-node, ruvector-wasm</td></tr>
<tr><td>Reliability layer</td><td>Spreads and keeps copies of data in agreement across machines — ruvector-cluster, ruvector-raft, ruvector-replication, ruvector-graph</td></tr>
<tr><td>Reasoning layer</td><td>Re-ranks and reasons over results — ruvector-gnn, ruvector-gnn-rerank, ruvector-attention, ruvllm</td></tr>
<tr><td>AI workforce</td><td>Ready-made specialist agent roles, e.g. a code-quality reviewer and a system architect</td></tr>
</tbody>
</table>
</div>
</details>
<details class="section" id="the-insight" open>
<summary>
<span class="num">03</span>
<span class="head-text">
<h2>The insight: one core, reused three ways</h2>
<span class="q">What's the one idea that makes it click?</span>
</span>
<span class="chev" aria-hidden="true">›</span>
</summary>
<div class="body">
<p class="lead-in">Look at how the pieces connect, and a pattern falls out: nearly everything else in the repo — the layer that spreads data across machines, even the layer that double-checks an answer — is built directly on the same core module that runs the basic search.</p>
<p>That's not a coincidence of naming. The same underlying structure that lets the search core jump toward close matches without checking everything is reused to let the reliability layer decide who agrees with whom, and reused again by the layer that re-checks results before they're returned. One fix or speed-up in that shared core lifts every layer built on it, instead of three separate projects each needing their own tuning.</p>
<p>It also explains why the AI agents aren't bolted onto the database as an afterthought — they're built to read and write against the same store the search engine uses, so what an agent finds becomes searchable data immediately, not a note left in a separate system.</p>
<figure class="figure diagram concept">
<span class="tier tech">The aha</span>
<button type="button" class="diagram-zoom" data-src="assets/insight.svg" data-alt="Nearly every one of ruvector's 278 components depends back on the same core crate, so capabilities can be added or swapped without touching the foundation." aria-label="Enlarge: Nearly every one of ruvector's 278 components depends back on the same core crate, so capabilities can be added or swapped without touching the foundation."><img src="assets/insight.svg" alt="Nearly every one of ruvector's 278 components depends back on the same core crate, so capabilities can be added or swapped without touching the foundation." loading="lazy"></button>
</figure>
<p class="oh">The search engine and the reliability engine aren't cousins — they're the same engine, doing two different jobs.</p>
</div>
</details>
<details class="section" id="how-it-works" open>
<summary>
<span class="num">04</span>
<span class="head-text">
<h2>Under the hood</h2>
<span class="q">How does it actually work?</span>
</span>
<span class="chev" aria-hidden="true">›</span>
</summary>
<div class="body">
<p class="lead-in">A handful of specific mechanisms carry most of the weight. Each is defined here, once, in plain words, at first use.</p>
<p>The search core is a graph-shaped index — imagine a map where every fingerprint is a landmark linked to its nearest neighbors, so a query starts at one landmark and hops toward closer and closer matches instead of checking the whole map. That shape has a name, HNSW (hierarchical navigable small world), and it's what lets a match come back out of millions of records without scanning all of them; the component is named directly in the repo, ruvector-coherence-hnsw.</p>
<p>Keeping copies of data in agreement uses two strategies depending on how much trust you can assume. Raft is a consensus protocol — a set of rules that lets a group of machines agree on one order of updates by electing a leader and following it (ruvector-raft). CRDTs — conflict-free replicated data types — take the opposite approach: structures built so two machines can each update their own copy independently and always merge back to the same result automatically, without asking permission first; the repo's own synchronizer implements the standard shapes, including a counter that only ever grows, a set that tracks additions and removals separately, and a register that resolves conflicting writes by timestamp [16][17][18][19][20]. For the harder case — some machines not just slow but actively wrong — there's Byzantine fault-tolerant consensus, built to keep a decision correct even when part of the group is lying [14].</p>
<p>The AI agent layer ships as role definitions, not one general-purpose assistant: a code-quality reviewer with a defined job description and trigger conditions [2], a system architect explicitly marked as needing human sign-off on major decisions [9], and others. Above the raw search results, a graph neural network — a model that reasons by passing information along the connections between related items — re-ranks matches (ruvector-gnn-rerank), and an attention layer (ruvector-attention, feeding an inference runtime called ruvllm) weighs which parts of a result matter most before anything is returned.</p>
<div class="dual">
<figure class="figure diagram">
<span class="tier tech">Architecture</span>
<button type="button" class="diagram-zoom" data-src="assets/architecture.svg" data-alt="ruvector-core sits at the center of the dependency graph — collections, filter, graph, GNN, cluster, raft, and replication crates all build on it, exposed through Node, WASM, and CLI bindings." aria-label="Enlarge: ruvector-core sits at the center of the dependency graph — collections, filter, graph, GNN, cluster, raft, and replication crates all build on it, exposed through Node, WASM, and CLI bindings."><img src="assets/architecture.svg" alt="ruvector-core sits at the center of the dependency graph — collections, filter, graph, GNN, cluster, raft, and replication crates all build on it, exposed through Node, WASM, and CLI bindings." loading="lazy"></button>
<figcaption>Architecture — modules, components and how they depend on each other.</figcaption>
</figure>
<figure class="figure diagram">
<span class="tier tech">Data flow</span>
<button type="button" class="diagram-zoom" data-src="assets/flow.svg" data-alt="A query becomes a vector, then descends the navigable HNSW lattice hop by hop, returning the closest matches in milliseconds — all from one .rvf file." aria-label="Enlarge: A query becomes a vector, then descends the navigable HNSW lattice hop by hop, returning the closest matches in milliseconds — all from one .rvf file."><img src="assets/flow.svg" alt="A query becomes a vector, then descends the navigable HNSW lattice hop by hop, returning the closest matches in milliseconds — all from one .rvf file." loading="lazy"></button>
<figcaption>Data flow — how a request moves through the system at runtime.</figcaption>
</figure>
</div>
</div>
</details>
<details class="section" id="use-cases" open>
<summary>
<span class="num">05</span>
<span class="head-text">
<h2>Where this fits</h2>
<span class="q">Where would I actually use this?</span>
</span>
<span class="chev" aria-hidden="true">›</span>
</summary>
<div class="body">
<p class="lead-in">Three shapes of problem, one project underneath each.</p>
<figure class="figure">
<span class="tier friendly">In the real world</span>
<button type="button" class="diagram-zoom" data-src="assets/useCase.png" data-alt="ruvector in use" aria-label="Enlarge: ruvector in use"><img src="assets/useCase.png" alt="ruvector in use" loading="lazy"></button>
</figure>
<div class="gallery"><div class="grid">
<details class="case" open>
<summary>
<span class="uc-num">1</span>
<span class="uc-title">Give your app real search</span>
<span class="uc-tag">Search core</span>
<span class="chev" aria-hidden="true">›</span>
</summary>
<div class="uc-body">
<p>If you're building a product search box, a support-ticket lookup, or a 'find similar' feature, the CLI alone gets you a working prototype: create a store, insert your fingerprints, query it, and see ranked matches before you've written a line of application code.</p>
<p>From there, @ruvector/wasm calls the same index from inside a browser, and the Node.js bindings pulled in by the ruvector package call it from a server — the same store, two ways to reach it.</p>
</div>
</details>
<details class="case" open>
<summary>
<span class="uc-num">2</span>
<span class="uc-title">Run an index that can't be a single point of failure</span>
<span class="uc-tag">Reliability layer</span>
<span class="chev" aria-hidden="true">›</span>
</summary>
<div class="uc-body">
<p>Once search is load-bearing — a production feature real users depend on — a single machine holding the only copy of the index is a risk. The cluster, replication, and Raft components exist to spread that index across machines that stay in agreement, so one machine going down doesn't take the feature with it.</p>
</div>
</details>
<details class="case" open>
<summary>
<span class="uc-num">3</span>
<span class="uc-title">Put several AI helpers on one shared memory</span>
<span class="uc-tag">AI workforce</span>
<span class="chev" aria-hidden="true">›</span>
</summary>
<div class="uc-body">
<p>The bundled agent roles — a code-quality reviewer, a system architect, others — are built to read and write against the same store the search engine uses. Paired with the project's MCP (Model Context Protocol — the standard way tools like Claude connect to outside systems) support, that means several AI assistants can share one fast, queryable memory instead of each starting from a blank context.</p>
</div>
</details>
</div></div>
</div>
</details>
<details class="section" id="get-started" open>
<summary>
<span class="num">06</span>
<span class="head-text">
<h2>Get started</h2>
<span class="q">How do I start, right now?</span>
</span>
<span class="chev" aria-hidden="true">›</span>
</summary>
<div class="body">
<p class="lead-in">There's a real CLI here, not just a library — every command below comes straight from the project's own listed commands.</p>
<div class="install-block"><pre class="code-block"><code>npm install -g ruvector-cli</code></pre></div>
<ol class="steps">
<li><strong>Check your prerequisites</strong> You need Node.js and npm (Node's package installer) on your machine — run npm -v in a terminal; any version printing back means you're ready. No database, no Docker, nothing else to stand up first.</li>
<li><strong>Install the CLI</strong> Run npm install -g ruvector-cli. This gives you the ruvector command everywhere on your machine — the project also lists plain npm install ruvector for using it purely as a library, without the CLI.</li>
<li><strong>Create a store</strong> Run npx ruvector create --dimensions 384 --path ./vectors.db. You'll see a new, empty index file appear at ./vectors.db, sized to hold 384-number fingerprints — a common size for small AI models.</li>
<li><strong>Load data in</strong> Run npx ruvector insert --input embeddings.json, pointing at a JSON file of your own numeric fingerprints. The CLI reports back how many records it added to the store.</li>
<li><strong>Search it</strong> Run npx ruvector search --query "[0.1, 0.2, 0.3, ...]" --top-k 10. A ranked list of the 10 closest matches prints straight to your terminal — this is the moment the whole idea becomes visible.</li>
<li><strong>Confirm it holds up, then wire it in</strong> Run npx ruvector benchmark --queries 1000 for real throughput numbers on your machine, or npx ruvector info to inspect what you just built. You now have a working, queryable local index — next, call it from code with @ruvector/wasm in a browser or the Node.js bindings pulled in by the ruvector package, or clone the repo and run cargo build --release --workspace to work in the Rust source directly.</li>
</ol>
</div>
</details>
<details class="section" id="the-pack" open>
<summary>
<span class="num">07</span>
<span class="head-text">
<h2>Take it with you</h2>
<span class="q">Does my AI get it too?</span>
</span>
<span class="chev" aria-hidden="true">›</span>
</summary>
<div class="body">
<p class="lead-in">Everything above was grounded in a full pass over ruvector's source: 37,600 passages across 278 components, indexed so it can be searched and cited rather than guessed at.</p>
<div class="tree" style="white-space:pre"><span class="cmt"># ruvector-knowledge-pack.zip</span>
<span class="d">for-ai/</span> <span class="cmt"># wire this into your agent</span>
<span class="f">ruvector-kb.rvf</span> <span class="cmt"># 384-dim vector brain (semantic search)</span>
<span class="f">ruvector-kb.passages.jsonl</span> <span class="cmt"># full passage text (search returns TEXT)</span>
<span class="f">ruvector-symbols.json</span> <span class="cmt"># exact public API</span>
<span class="f">ruvector-dep-graph.json</span> <span class="cmt"># what depends on what</span>
<span class="f">ruvector-entrypoints.json</span> <span class="cmt"># build / test / run commands</span>
<span class="f">ask-kb.mjs</span> · <span class="f">kb-mcp-server.mjs</span> <span class="cmt"># CLI + MCP search server</span>
<span class="d heart">for-humans/</span> <span class="cmt"># read first</span>
<span class="f heart">ruvector-primer.md</span> <span class="cmt"># the human orientation</span></div>
<div class="dl-cta"><a class="cta" href="ruvector-knowledge-pack.zip" download>Download the knowledge pack</a><span class="dl-meta">RVF vector KB + MCP server — drop it into your own agent.</span></div>
<a class="dropzone" href="ruvector-knowledge-pack.zip" download><span class="dz-icon" aria-hidden="true">↓</span><strong>Give your AI the same understanding</strong><span class="dz-hint">ruvector-knowledge-pack.zip</span></a>
</div>
</details>
</div>
</div>
</main>
<footer class="explainer-footer" role="contentinfo">
<div class="wrap explainer-footer-inner">
<p class="ef-credit">
Explainer page created by <strong><a href="https://stuart-kerr-card.netlify.app" rel="author noopener" target="_blank">Stuart Kerr</a></strong> at
<a href="https://isovision.ai" rel="noopener" target="_blank">ISOvision</a>
<span class="ef-fine">Built with the open-source <a href="https://github.com/stuinfla/Repo-Explainer" rel="noopener" target="_blank">explainmyrepo</a> engine.
Found this valuable? <a href="https://github.com/stuinfla/Repo-Explainer" rel="noopener" target="_blank">A star on GitHub</a> genuinely helps —
and if something's off, <a href="https://github.com/stuinfla/Repo-Explainer/issues" rel="noopener" target="_blank">open an issue</a>: happy to help fix it.</span>
</p>
<a class="ef-cta" href="https://explainmyrepo.isovision.ai" target="_blank" rel="noopener">
<span class="ef-cta-lead">Would you like your own repo explainer page?</span>
<strong>Generate one here</strong>
<span class="ef-arrow" aria-hidden="true">→</span>
</a>
</div>
</footer>
<dialog class="diagram-lightbox" id="diagram-lightbox" aria-label="Enlarged diagram">
<button type="button" class="lightbox-close" data-close aria-label="Close">×</button>
<img id="diagram-lightbox-img" src="" alt="">
</dialog>
<script>
(function () {
var dlg = document.getElementById('diagram-lightbox');
if (!dlg || typeof dlg.showModal !== 'function') return;
var img = document.getElementById('diagram-lightbox-img');
document.querySelectorAll('.diagram-zoom').forEach(function (btn) {
btn.addEventListener('click', function () {
img.src = btn.getAttribute('data-src');
img.alt = btn.getAttribute('data-alt') || '';
dlg.showModal();
});
});
dlg.addEventListener('click', function (e) {
if (e.target === dlg || e.target.hasAttribute('data-close')) dlg.close();
});
})();
</script>
</body>
</html>