EQTYLab Explorer
A visualization tool for exploring data lineage and computational integrity. Part of the EQTYlab ecosystem, it displays data provenance chains showing how data flows through computational systems, which actors are involved, and what verification statements are attached.
It ships both as the hosted app at explorer.eqtylab.io and as an embeddable library that renders a manifest inside any page — React or not.
Changelog
See CHANGELOG.md for release changes and CONTRIBUTING.md for PR and release instructions.
Features
- Interactive Graph Visualization: Drag-to-explore canvas with zoom/pan controls using ReactFlow
- Detailed Inspector: Sidebar showing registration, governance, storage, and signature details for selected nodes
- Embeddable: Drop into any page with one script tag as
<eqty-explorer>, isolated in a shadow root so neither page can restyle the other - Dual Operating Modes:
- Manifest Viewer: Upload or select sample manifest JSON files to explore
- Integrity Service: Query a backend integrity service for manifests
Embedding
The explorer is designed to be dropped into a page it doesn't control. Use the
<eqty-explorer> custom element unless you have a specific reason not to, it's clean and simple.
The default: a custom element
<script type="module" src="https://cdn.jsdelivr.net/npm/@eqtylab/explorer/dist/standalone/explorer.js"></script>
<eqty-explorer manifest-url="/my-manifest.json" theme="auto" style="height: 600px"></eqty-explorer>
Alternatively, you can import the custom element from npm instead of the CDN:
import { defineExplorerElement } from "@eqtylab/explorer";
defineExplorerElement();
React
A React app — use the component, so the explorer composes with your tree, context and error boundaries:
npm install @eqtylab/explorer react react-dom
import { Explorer } from "@eqtylab/explorer";
import "@eqtylab/explorer/style.css"; // required — only the CDN build self-injects
<Explorer manifest={manifestJson} theme="auto" />;
Your build needs an es2022 target. Signature verification loads WebAssembly behind a
top-level await, which Vite's default target rejects outright — esbuild fails with
"Top-level await is not available in the configured target environment". In Vite that is
build: { target: "es2022" }; other bundlers want their own equivalent.
Supplying a manifest
Pass a parsed object or a JSON string as manifest, or a URL as manifestUrl;
manifest wins when both are set. These props are the only way to say what the explorer
shows: it has no upload or clear controls of its own, so to let users open their own
files, read them in your page and pass the result as manifest. Setting neither shows
an empty graph.
Configuration
| Option | React | <eqty-explorer> | Default | Notes |
|---|---|---|---|---|
manifest | manifest={obj} | attribute, property, or a child <script type="application/json"> | — | Parsed object or JSON string |
manifestUrl | ✓ | manifest-url | — | Fetched on mount, and again if it changes |
view | ✓ | view | lineage | lineage, table or text; initial only |
theme | ✓ | theme | auto | light, dark, or follow the host's prefers-color-scheme |
cssVars | ✓ | property only | — | Token overrides, e.g. { "--brand-lineage-explorer": "#0af" } |
nodeOverrides | ✓ | property only | — | Per-asset-type node appearance |
customIcons | ✓ | property only | — | Icon overrides keyed by asset type |
groupNodeThreshold | ✓ | group-node-threshold, or property | 5 | How many sibling nodes collapse into a group |
selectedNodeId | ✓ | selected-node-id, or property | — | Select a node by id; re-applied whenever it changes |
selectedStatementId | ✓ | selected-statement-id, or property | — | Show a statement in the text view, switching to it |
selectedBlobCid | ✓ | selected-blob-cid, or property | — | Show a blob in the text view; a statement wins if both are set |
query | ✓ | query, or property | — | Filter every view; re-applied whenever it changes |
onManifestChange | ✓ | manifestchange event | — | Detail is Manifest | null |
onNodeSelect | ✓ | nodeselect event | — | Detail is the node id, or null when the selection clears |
onStatementSelect | ✓ | statementselect event | — | Detail is the statement key, or null when none is shown |
onBlobSelect | ✓ | blobselect event | — | Detail is the blob CID, or null when none is shown |
onQueryChange | ✓ | querychange event | — | Detail is the applied query string, "" when cleared |
onError | ✓ | error event | logged either way | Unparseable JSON, failed fetches |
portalContainer | ✓ | set for you | shared container | Only needed if you render overlays somewhere unusual |
config, vCompData | ✓ | — | — | React only — supply a prebuilt config or already-parsed vComp state |
Attributes are live — changing one re-renders.
Options whose values are objects have no attribute form, since an attribute can only carry a string. Set those as properties, before or after the element enters the document:
const el = document.querySelector("eqty-explorer");
el.manifest = parsedObject;
el.customIcons = { dataset: "/icons/dataset.svg" };
el.cssVars = { "--brand-lineage-explorer": "#0af" };
el.addEventListener("nodeselect", (e) => console.log(e.detail)); // string | null
el.addEventListener("statementselect", (e) => console.log(e.detail)); // string | null
el.addEventListener("blobselect", (e) => console.log(e.detail)); // string | null
el.addEventListener("querychange", (e) => console.log(e.detail)); // string
el.addEventListener("manifestchange", (e) => console.log(e.detail)); // Manifest | null
el.addEventListener("error", (e) => console.error(e.detail)); // Error
The container needs a height. The explorer fills whatever it's given; a container with no height renders nothing, and warns about it in the console.
Styling
Hosts and future agent harnesses can check a query before applying it with the
exported checkQuery(index, query) API, using an index from
buildQueryIndex(nodes, edges, { statements, blobs, anchors }), which reads each
node's metadata, credentials and actors from the manifest's statements.
It returns { valid, ready, errors, warnings }, where diagnostics have code and message.
This check does not match nodes, traverse lineage, update the query, or alter the
display. Apply the query through the query prop or attribute only after
valid is true. ready additionally excludes bare text, which the UI waits to
apply until Enter; an empty query is valid and ready. Values absent from the
current manifest produce warnings rather than invalidating a query; these warnings
also delay automatic application while a value is still being typed. Closed enums
such as has: reject unknown values. The check validates against the same field
definitions and compilation logic used by evaluation, without evaluating matches.
Canonical query fields follow manifest names: registeredBy, assetType,
metadata.<path>, and associationType. The earlier by, type, meta,
assoc, and association spellings remain aliases. Actor choices insert DIDs
and display names; actor names can also be queried directly. Adjacent terms mean
AND, and precedence is NOT, then AND, then OR. Parentheses override precedence.
Numeric comparisons and numeric ranges are supported on metadata; timestamp
is text, with inclusive after / before date filters for chronological queries.
mentions: (or ref:) takes an id, its start, or a name, and matches what the
statements about a node name besides it: an input, a governing document, a
storage location, an operator. Bare words search names, types, actor names,
metadata and text; identifiers match a bare word from their start.
In the text view the same query filters statements, reading each field one of two
ways. Most read what the statement is about (a data or computation node, a DID or
an entity, followed through credentials and metadata) as the lineage view reads
that node: name, assetType, is, has, metadata.<path>, associationType,
the lineage fields and bare words. So assetType:Model lists the model's
registration, metadata, the credentials signing them, its storage and governance:
the statements behind the nodes the lineage view would show. The rest read the
statement itself: statement: (its own type: data, metadata, credential,
did, actordid, entity, …; an unknown type only warns), registeredBy,
operatedBy, executedOn, timestamp, after, before and mentions. These
can differ from the lineage view's answer: registeredBy:alice lists what Alice
registered, including metadata she added to a node someone else registered, and
statement:metadata lists metadata registrations rather than the nodes they
describe. The Blobs tab lists the blobs the query matches, each answering as the
statement it belongs to (a data or metadata blob as its registration) or, when it
belongs to none, by its CID, name and path, with a listed collection's files. A blob
is not listed for being referenced by a matching statement: is:computation lists a
computation's metadata blob, not the data it takes in. checkQuery with an index
from buildQueryIndex validates the lineage and table meaning.
Fonts aren't bundled. --font-sans resolves to TWK Lausanne/Inter and
--font-mono to IBM Plex Mono. Serve those yourself or the explorer falls back to
system fonts.
To restyle on purpose, use cssVars rather than a stylesheet. Token overrides work
through either entry point, because custom properties cross a shadow boundary where
selectors can't.
Demos
pnpm build
pnpm demo # then open http://localhost:4173/demo/embed.html
demo/embed.html is a deliberately hostile host page — it redefines the same CSS custom
property names the explorer uses, styles main/ul/img directly, and defines its own
.flex class. Four explorers on it cover all four manifest sources, the object-valued
properties and two themes, every one in a shadow root. demo/react/ covers the
light-DOM path, which is the React component's.
demo/react/ covers the npm path and the peer-dependency split.
Prerequisites
This project uses Nix for development environment management. Install Nix, then the devshell will provide all required dependencies.
Alternatively, ensure you have installed:
- Node.js
- pnpm
Local Development
# Enter the Nix development shell (if using Nix)
nix develop
# Install dependencies
pnpm install
# Start development server
pnpm dev
Available Commands
| Command | Description |
|---|---|
pnpm dev | Start the site's dev server, on the explorer's source |
pnpm build | Build all three targets and verify CSS scoping |
pnpm build:lib | Build the npm package |
pnpm build:standalone | Build the CDN bundle |
pnpm build:site | Build the hosted site (needs pnpm build:lib first) |
pnpm check:css | Verify the built stylesheets can't escape their scope |
pnpm demo | Serve the repo so demo/embed.html can be opened |
pnpm fmt | Format code with Prettier |
pnpm format:check | Check code formatting |
pnpm lint | Run ESLint |
pnpm lint:fix | Run ESLint with auto-fix |
pnpm typecheck | Run TypeScript type checking |
Repository Layout
The repository root is the @eqtylab/explorer package. The hosted site lives in site/,
a private pnpm workspace member that depends on the package through workspace:* and
imports only its public entry points (eslint enforces this), like any other consumer.
The site owns its own chrome, the manifest upload UI and the sample manifests in
site/public/manifests, which the package's tests also read as fixtures.
Build Outputs
The project builds three things.
- npm package (
dist/lib/) — published as@eqtylab/explorer. ESM only, with React as a peer dependency. The stylesheet ships alongside it as@eqtylab/explorer/style.css. - CDN bundle (
dist/standalone/) — React and every dependency in one file, loaded by a single<script type="module">. It injects its own styles. The signature-verification WebAssembly stays a separate file and loads on first use. - Hosted site (
site/dist/) — the site at explorer.eqtylab.io. Its production build resolves@eqtylab/explorertodist/liband its scoped stylesheet, so the deployed site runs exactly what npm consumers get. It deploys only on release, like the package.
pnpm dev instead compiles the explorer from src/ for hot reload (see
vite/explorer-source.ts), which leaves two things it does not cover.
- Scoped CSS.
scope-cssruns at bundle time, so in dev the explorer's stylesheet is unscoped. Scoping mistakes appear only in a real build, so CI runspnpm check:css, andpnpm build && pnpm previewserves the site against the built package. - The custom element. The site renders
<Explorer>, so the dev server never creates an<eqty-explorer>. The shadow root, the adopted stylesheet and the per-instance portal container are covered bydemo/embed.html, which runs against a built bundle.
Release
Releases are automated via GitHub Actions and triggered by bumping the version in package.json on main.
How to Release
- Prepare the stable release section in CHANGELOG.md, following the release instructions.
- Bump
versioninpackage.jsonin the same PR and run./scripts/check-changelog.sh vX.Y.Zwith the intended version. - Open a PR and merge it to
main
When the change lands on main, the Release workflow will automatically:
- Check that stable versions have a matching changelog heading
- Tag the commit as
v<version>and create a GitHub Release with generated notes - Call the Publish workflow to build and publish to npm
A version containing a hyphen (e.g. 0.11.0-rc1) is marked as a GitHub pre-release. Note that npm still publishes it under the latest dist-tag.
Version Management
package.json is the single source of truth for the version. If the tag v<version> already exists, the release workflow does nothing.
The Publish workflow checks the effective version against the changelog before building, including dry runs. Candidate and unofficial tags skip this check.
Publishing can also be triggered manually:
- Publish via Run workflow builds and packs without publishing (
--dry-runby default; uncheck to publish the version currently inpackage.json) - Creating a GitHub Release by hand still publishes the version from its tag
App configuration
See the configuration documentation for customization options.
Adding Example Manifests
- Add the manifest JSON file to
./site/public/manifests - Update the
manifestssection in./site/src/config.json- The name must match the filename without the extension