Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/current-cosmos-omnigraph.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@modernrelay/orbit-engine-cosmos': minor
'@modernrelay/orbit-omnigraph': minor
'@modernrelay/orbit-react': patch
'@modernrelay/orbit-data': patch
'@modernrelay/orbit-core': patch
---

Upgrade Cosmos to 3.5.1 and OmniGraph to 0.14.0. Both adapters now require Node.js 22 or newer.

The Cosmos adapter resolves the compatible Luma 9.3 peers and uses flat position snapshots. The OmniGraph adapter follows the 0.14 HTTP contract, reads physical IDs separately from user properties, normalizes nullable and UTC temporal values, and preserves search cancellation. Generated OmniGraph attribute types no longer inject a physical `id`; schema-declared `id` properties retain their declared types, and list types account for nullable elements. Regenerate generated types and update stored-query column mappings for the new result format when upgrading.

Upgrade OmniGraph servers alongside the SDK; the 0.13 HTTP contract is no longer accepted. Export drift checks now read the captured head from bounded commit-history pages, preserving revision checks without fetching the entire history. Integration fixtures and CI use the official 0.14.0 server.

Refresh dependencies across all packages and apps, including React 19.3, Vite 8, Storybook 10.6, Vitest 5, jsdom 30, Playwright 1.64, and TypeScript 7. React 18 remains supported. The root retains TypeScript 6 for build tools that require its compiler API, and Luma stays on the latest 9.3 patch required by Cosmos. Development uses pnpm 12 and Node.js 22.18+, 24.11+, or 26+.
77 changes: 65 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v7
with:
node-version: 24
cache: pnpm
Expand All @@ -37,9 +37,9 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v7
with:
node-version: 24
cache: pnpm
Expand All @@ -49,7 +49,7 @@ jobs:
run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT"
working-directory: apps/demo
- name: Cache Playwright browsers
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ steps.pw.outputs.version }}
Expand All @@ -75,7 +75,7 @@ jobs:
# rendering only helps idle gaps, not active streaming.
# Raising this back toward 30000 is a separate canary experiment.
STREAM_ROWS: '12000'
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v7
if: always()
with:
name: demo-playwright
Expand All @@ -85,16 +85,69 @@ jobs:
if-no-files-found: ignore
retention-days: 7

omnigraph:
# Exercise the current protocol against an actual pinned server. The
# recordings stay useful for fast unit tests, but cannot replace this gate.
runs-on: ubuntu-latest
timeout-minutes: 25
env:
OMNIGRAPH_LIVE_REQUIRED: '1'
steps:
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v7
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Install OmniGraph 0.14.0
shell: bash
run: |
mkdir -p "$RUNNER_TEMP/omnigraph-0.14.0"
cd "$RUNNER_TEMP/omnigraph-0.14.0"
release_url=https://github.com/ModernRelay/omnigraph/releases/download/v0.14.0
curl --fail --location --retry 3 "$release_url/omnigraph-linux-x86_64.tar.gz" -o omnigraph-linux-x86_64.tar.gz
curl --fail --location --retry 3 "$release_url/omnigraph-linux-x86_64.sha256" -o omnigraph-linux-x86_64.sha256
sha256sum --check omnigraph-linux-x86_64.sha256
tar -xzf omnigraph-linux-x86_64.tar.gz
"$RUNNER_TEMP/omnigraph-0.14.0/omnigraph" version
{
echo "OMNIGRAPH_BIN=$RUNNER_TEMP/omnigraph-0.14.0/omnigraph"
echo "OMNIGRAPH_SERVER_BIN=$RUNNER_TEMP/omnigraph-0.14.0/omnigraph-server"
} >> "$GITHUB_ENV"
- name: Install Playwright Chromium
timeout-minutes: 15
run: |
for attempt in 1 2 3; do
if timeout 240 pnpm --filter orbit-demo exec playwright install --with-deps chromium; then
exit 0
fi
echo "playwright install attempt ${attempt} failed or stalled; retrying" >&2
sleep 5
done
exit 1
- name: Live export and stored-query integration
run: pnpm --filter orbit-demo exec playwright test e2e/omnigraph.spec.ts
- uses: actions/upload-artifact@v7
if: always()
with:
name: omnigraph-playwright
path: |
apps/demo/playwright-report/
apps/demo/test-results/
if-no-files-found: ignore
retention-days: 7

package:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: [22, 24]
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node }}
cache: pnpm
Expand All @@ -104,7 +157,7 @@ jobs:
- run: pnpm run smoke
- run: pnpm run probe:node
- run: pnpm run svg:node-fixture
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v7
if: always()
with:
name: smoke-node-${{ matrix.node }}
Expand Down
22 changes: 13 additions & 9 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,23 @@ jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v7
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Configure npm publishing
uses: actions/setup-node@v7
with:
registry-url: https://registry.npmjs.org
- name: Version PR or publish
uses: changesets/action@v1
uses: changesets/action@v2
with:
version: pnpm run version
publish: pnpm run release
createGithubReleases: true
version-script: pnpm run version
publish-script: pnpm run release
create-github-releases: true
github-token: ${{ secrets.GITHUB_TOKEN }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
11 changes: 11 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,23 @@

## Setup

Use Node.js 22.18+, 24.11+, or 26+ for the current development tools, and
pnpm 12.15.0 (pinned in `packageManager`). The published Cosmos and OmniGraph
adapters require Node.js 22 or newer.

```bash
pnpm install
pnpm check # boundaries + lint + typecheck + all tests + gate evaluator
pnpm demo:dev # demo app at http://localhost:5199
```

Workspace packages and apps use TypeScript 7 for `pnpm typecheck`, and the
OmniGraph code-generation tests invoke that compiler directly. The root keeps
TypeScript 6.0.3 for tsup declaration generation and typescript-eslint: these
tools still require the JavaScript compiler API removed from TypeScript 7.
Run typechecks through the workspace scripts, rather than the root `tsc` binary.
Remove the root compatibility compiler when both tools support TypeScript 7.

## Ground rules

- **One focused change per PR**, with tests. When a PR fixes a bug, add the
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ Handles 100K+ node graphs. UI components included: search, tables, histograms, a
npm install @modernrelay/orbit-react @modernrelay/orbit-core @modernrelay/orbit-engine-cosmos
```

**Status:** `0.15.0` on npm — all five packages release in lockstep. See
The Cosmos engine and OmniGraph adapter require Node.js 22 or newer.

All five packages release in lockstep. See
[Releases](https://github.com/ModernRelay/orbit/releases) for changelogs.

## Packages
Expand Down
3 changes: 3 additions & 0 deletions apps/demo/e2e/data-search.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ test('search: results list, Enter focuses, a filtered result surfaces its reason
test('minimap: canvas rasterizes and a corner click pans the real viewport', async ({
page,
}) => {
// Pausing physics does not stop the initial camera fit. Make that fit
// instant so the before/after zoom comparison measures only the minimap pan.
await page.emulateMedia({ reducedMotion: 'reduce' });
await gotoReady(page);
await pauseSimulation(page);

Expand Down
139 changes: 67 additions & 72 deletions apps/demo/e2e/omnigraph.spec.ts
Original file line number Diff line number Diff line change
@@ -1,103 +1,100 @@
/**
* v0.6 omnigraph live-integration smoke.
*
* Boots the REAL `omnigraph-server` over the committed fixture cluster
* (fixtures/omnigraph/cluster) and drives the demo's Omnigraph panel end to
* end through the vite `/og` same-origin proxy: Load from Omnigraph → live
* meter completes → header counts match the recorded manifest's rowCounts →
* hovering a node surfaces a type-derived label → screenshot artifact.
*
* Skips cleanly (never fails) when the `omnigraph-server` binary or the
* fixture cluster graph store is absent — CI installs neither and consumes
* the recorded HTTP fixtures instead; this spec is the local full-stack
* proof. Regenerate the cluster store with `node scripts/omnigraph-fixture.mjs`.
* Live OmniGraph 0.14 integration. The fixture script creates a fresh local
* cluster, seeds it through HTTP, and owns its server until teardown.
* Optional locally; OMNIGRAPH_LIVE_REQUIRED=1 makes missing binaries fail CI.
*/

import { spawn, spawnSync } from 'node:child_process';
import type { ChildProcess } from 'node:child_process';
import { existsSync, readFileSync } from 'node:fs';
import { once } from 'node:events';
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

import { Omnigraph } from '@modernrelay/omnigraph';
import { createGraphInstance } from '@modernrelay/orbit-core';
import { FakeEngine } from '@modernrelay/orbit-core/testing';
import { createOmnigraphSearchService, createOmnigraphSource, encodeSourceId } from '@modernrelay/orbit-omnigraph';
import { expect, test } from '@playwright/test';
import type { Page } from '@playwright/test';

const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
const CLUSTER_DIR = path.join(ROOT, 'fixtures', 'omnigraph', 'cluster');
const GRAPH_STORE = path.join(CLUSTER_DIR, 'graphs', 'demo.omni');
const MANIFEST_PATH = path.join(ROOT, 'fixtures', 'omnigraph', 'recorded', 'manifest.json');

const BIND = '127.0.0.1:8199';
const HEALTH_URL = `http://${BIND}/healthz`;

const CLI = process.env.OMNIGRAPH_BIN || 'omnigraph';
const SERVER = process.env.OMNIGRAPH_SERVER_BIN || 'omnigraph-server';
const BASE = 'http://127.0.0.1:8199';
const REQUIRED = process.env.OMNIGRAPH_LIVE_REQUIRED === '1';
const cliVersion = spawnSync(CLI, ['version'], { encoding: 'utf8' });
const AVAILABLE = cliVersion.status === 0 && cliVersion.stdout.split('\n').includes('omnigraph 0.14.0') &&
spawnSync(SERVER, ['--help'], { stdio: 'ignore' }).status === 0;
const SKIP_REASON = 'Set OMNIGRAPH_BIN and OMNIGRAPH_SERVER_BIN to the pinned 0.14.0 binaries; OMNIGRAPH_LIVE_REQUIRED=1 requires the live test.';
const READY_DOT = '[data-testid="status-dot"][title="ready"]';
const NODE_COUNT = '[data-testid="node-count"]';
const EDGE_COUNT = '[data-testid="edge-count"]';

/** Status-bar hover readout format for omnigraph nodes (App.tsx `labelOf`). */
const TYPE_LABEL = /^(Actor|Decision|Trace|Signal|Artifact) · .+/;

const fmt = (n: number): string => n.toLocaleString('en-US');

interface RecordedManifest {
graphId: string;
branch: string;
rowCounts: Record<string, number>;
}

function hasServerBinary(): boolean {
try {
return spawnSync('omnigraph-server', ['--help'], { stdio: 'ignore' }).error === undefined;
} catch {
return false;
}
}

const AVAILABLE = hasServerBinary() && existsSync(GRAPH_STORE) && existsSync(MANIFEST_PATH);
const SKIP_REASON =
'omnigraph-server binary or fixture cluster store missing (CI) — ' +
'recorded fixtures cover the adapter; run `node scripts/omnigraph-fixture.mjs` locally';

const manifest: RecordedManifest | null = AVAILABLE
? (JSON.parse(readFileSync(MANIFEST_PATH, 'utf8')) as RecordedManifest)
: null;

const manifest = JSON.parse(readFileSync(path.join(ROOT, 'fixtures/omnigraph/recorded/manifest.json'), 'utf8')) as RecordedManifest;
function tableTotal(m: RecordedManifest, prefix: 'node:' | 'edge:'): number {
return Object.entries(m.rowCounts)
.filter(([key]) => key.startsWith(prefix))
.reduce((sum, [, count]) => sum + count, 0);
return Object.entries(m.rowCounts).filter(([key]) => key.startsWith(prefix)).reduce((sum, [, count]) => sum + count, 0);
}

async function healthy(): Promise<boolean> {
try {
return (await fetch(HEALTH_URL)).ok;
} catch {
return false;
}
}

/** Spawned only when nothing already answers on 8199 (reuse a dev server). */
let server: ChildProcess | null = null;
let fixture: ChildProcess | null = null;
let fixtureClosed: Promise<unknown> | null = null;
let fixtureLog = '';

test.skip(!AVAILABLE && !REQUIRED, SKIP_REASON);
test.beforeAll(async () => {
if (!AVAILABLE) return;
if (await healthy()) return;
server = spawn(
'omnigraph-server',
['--cluster', CLUSTER_DIR, '--unauthenticated', '--bind', BIND],
{ stdio: 'ignore' },
);
const deadline = Date.now() + 20_000;
while (Date.now() < deadline) {
if (await healthy()) return;
await new Promise((resolve) => setTimeout(resolve, 200));
test.setTimeout(60_000);
if (!AVAILABLE) throw new Error(SKIP_REASON);
fixture = spawn(process.execPath, [path.join(ROOT, 'scripts/omnigraph-fixture.mjs'), '--serve'], {
cwd: ROOT, env: { ...process.env, OMNIGRAPH_FIXTURE_PORT: '8199' }, stdio: ['ignore', 'pipe', 'pipe'],
});
fixtureClosed = once(fixture, 'close');
for (const output of [fixture.stdout, fixture.stderr]) output?.on('data', (chunk) => { fixtureLog = (fixtureLog + chunk).slice(-16000); });
const deadline = Date.now() + 45_000;
while (!fixtureLog.includes('FIXTURE_READY')) {
if (fixture.exitCode !== null || fixture.signalCode !== null || Date.now() > deadline) throw new Error(`Fixture failed to start: ${fixtureLog}`);
await new Promise((resolve) => setTimeout(resolve, 100));
}
throw new Error(`omnigraph-server did not become healthy on ${HEALTH_URL} within 20s`);
const response = await fetch(`${BASE}/readyz`, { headers: { 'Omnigraph-Http-Api': '0.14' } });
expect(response.ok).toBe(true);
expect(response.headers.get('Omnigraph-Http-Api')).toBe('0.14');
});

test.afterAll(() => {
server?.kill();
server = null;
test.afterAll(async () => {
if (fixture !== null && fixture.exitCode === null && fixture.signalCode === null) {
fixture.kill('SIGTERM');
await fixtureClosed;
}
fixture = null;
});

test('live export and stored search share system identity while retaining user id data', async () => {
const client = new Omnigraph({ baseUrl: BASE });
const search = createOmnigraphSearchService({
client, graphId: 'demo', queryName: 'search_signals', typeOf: { s: 'Signal' },
labelColumn: 's.title', params: (q) => ({ q }),
});
const instance = createGraphInstance({ engine: () => new FakeEngine(), services: { search } });
try {
const loaded = await createOmnigraphSource({ client, graphId: 'demo' }).load(instance);
expect(loaded.serverVersion).toBe('0.14.0');
expect(loaded.counts).toMatchObject({ nodes: 300, edges: 500 });
expect(loaded.warnings).toEqual([]);
const id = encodeSourceId('Signal', 'signal-001');
expect(instance.getNode(id)?.attrs?.id).toBe(1);
expect(instance.getNode(encodeSourceId('Signal', 'signal-003'))?.attrs?.id).toBeNull();
expect(instance.getNode(encodeSourceId('Actor', 'actor-001'))?.attrs).not.toHaveProperty('id');
expect(instance.getNode(encodeSourceId('Decision', 'decision-001'))?.attrs?.updated_at).toMatch(/T.*\.\d{3}Z$/);
const hits = await instance.search('Signal 1:', { limit: 5 });
expect(hits).toHaveLength(1);
expect(hits[0]).toMatchObject({ id, label: 'Signal 1: search admission' });
expect(instance.getNode(hits[0]!.id)).toBeDefined();
} finally { instance.destroy(); }
});

/** Sweep a grid around the viewport center until the status bar shows an
Expand All @@ -123,8 +120,6 @@ async function huntHoverLabel(page: Page): Promise<string | null> {
test('load from omnigraph → meter completes → counts match manifest → typed hover label', async ({
page,
}, testInfo) => {
test.skip(!AVAILABLE, SKIP_REASON);
if (manifest === null) return; // unreachable past the skip; narrows the type
const nodeTotal = tableTotal(manifest, 'node:');
const edgeTotal = tableTotal(manifest, 'edge:');

Expand Down
Loading
Loading