Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
248 changes: 210 additions & 38 deletions engraphis/dashboard_assets/engraphis-graph.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion engraphis/dashboard_assets/index.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -708,6 +708,6 @@ <h2 id="graph-connections-title">Connected nodes</h2>
</form>
</dialog>

<script src="/v2-assets/ledger.js?v=20260815-merge-ready-1"></script>
<script src="/v2-assets/ledger.js?v=20260828-slider-multiplier-fix"></script>
</body>
</html>
54 changes: 44 additions & 10 deletions engraphis/dashboard_assets/ledger.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -457,7 +457,7 @@
graphAssetSource('/v2-assets/vendor/force-graph.min.js?v=20260727-final'),
'ForceGraph', controller.signal,
)).then(() => loadScript(
graphAssetSource('/v2-assets/engraphis-graph.js?v=20260815-merge-ready-1'),
graphAssetSource('/v2-assets/engraphis-graph.js?v=20260828-slider-multiplier-fix'),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Synchronize the cache-buster assertions

With this new renderer URL, python -m pytest tests/test_graph_engine_asset.py -q fails in test_primary_graph_dependencies_are_lazy_retryable_and_csp_clean, which still searches for engraphis-graph.js?v=20260815-merge-ready-1; its subsequent assertion also expects the old ledger.js URL changed in this commit. Update that cache-buster contract alongside both production URLs so the primary offline gate remains green.

AGENTS.md reference: AGENTS.md:L42-L46

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of scope for this PR. ledger.js belongs to the dashboard PR. Closing without a code change in this PR.

'EngraphisGraph', controller.signal,
)).then(() => loadScript(
graphAssetSource('/v2-assets/engraphis-spacetime.js?v=20260812-stable-orbit-lanes-7'),
Expand DownExpand Up@@ -2379,7 +2379,10 @@
const label = byId(id);
if (label) label.textContent = labels[index];
});
byId('graph-spacetime-tuning').hidden = !galaxy;
// The spacetime multipliers are also wired into the d3 forces for every
// non-Galaxy preset. Keep the controls available wherever those settings
// have an observable effect; only the labels and summary vary by mode.
byId('graph-spacetime-tuning').hidden = false;
Comment on lines +2382 to +2385

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Hide inert controls outside Galaxy mode

For responsive non-Galaxy views such as compact, communities, radial, constellation, and original, making the whole panel visible also exposes Spring stiffness and Pause orbits even though neither affects the D3 layout: springStiffness is consumed only by Galaxy integration or the oversized static fallback, and orbitPaused gates only galaxyDynamicsEligible(). Moving or toggling these controls therefore changes saved state and may reheat the graph without changing its behavior; hide or disable those rows outside applicable modes, or wire them into the live D3 path.

Useful? React with 👍 / 👎.

const forceLabels = full
? ['Core attraction', 'Core mass', 'Cluster cohesion', 'Settling resistance', 'Link spring']
: ['Galactic gravity', 'Black hole mass', 'Local solar gravity', 'Space friction', 'Spring stiffness'];
Expand DownExpand Up@@ -2453,6 +2456,22 @@
function graphSliderResponseValue(id, value, baseline) {
const control = byId(id);
if (!control) return Number.isFinite(Number(value)) ? Number(value) : baseline;
/* Spacetime multipliers (galactic gravity, local solar gravity, black hole mass,
space friction, spring stiffness) are linear controls: the dashboard's
graphSpacetimeEngineSettings already normalises them to a clean 0..2 range with
the visible default at 1.0. The 2x response gain centred on the slider's fallback
would clip the lower quarter of every slider to 0 (e.g. visible 0..50 for the
gravitational-constant slider all map to engine 0) and compress the visible
50..100 range to engine 0..1.0, so the user couldn't tell the difference between
slider=30 and slider=50. Bypass the gain for these controls so the visible slider
position maps linearly to the engine value. */
if (id === 'graph-gravitational-constant'
|| id === 'graph-local-gravitational-constant'
|| id === 'graph-black-hole-mass'
|| id === 'graph-space-damping'
|| id === 'graph-spring-stiffness') {
return graphValueInRange(id, value, baseline);
}
const raw = graphValueInRange(id, value, baseline);
const center = Number.isFinite(Number(baseline)) ? Number(baseline) : raw;
const min = Number(control.min);
Expand DownExpand Up@@ -2506,9 +2525,15 @@
return settings;
}, {});
return {
gravitationalConstant: controls.gravitationalConstant / 50,
// The engine consumes these values directly as multipliers. The visible default
// (100 for gravity/local, 160 for black-hole) must reach the engine as 1.0 so the
// untouched-slider state is a no-op. The earlier / 50 division sent 2.0 at the
// default and clamped the upper half of the slider to 2.0x, so the user's
// movements from 100..200 produced no visible effect — the "revert to default"
// bug. / 100 keeps the default at 1.0x and gives a clean 0..2 range.
gravitationalConstant: controls.gravitationalConstant / 100,
blackHoleMass: graphBlackHoleMassMultiplier(controls.blackHoleMass),
localGravitationalConstant: controls.localGravitationalConstant / 50,
localGravitationalConstant: controls.localGravitationalConstant / 100,
Comment on lines +2534 to +2536

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid saturating half of the slider ranges

When these values come from the dashboard, graphSliderResponseValue() has already doubled their offset from the baseline and clamped them to the HTML range, so dividing here does not give a responsive 0–2 mapping across the visible control: gravity/local values 0–50 all become 0 and 150–200 all become 2, while black-hole values 20–90 and 330–500 likewise collapse to their endpoints before the new mass mapping runs. Fresh evidence in this revision is that the new normalization still consumes those gain-expanded values, so large portions of all three sliders remain inert despite the stated full-range fix.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of scope for this PR. ledger.js belongs to the dashboard PR. Closing without a code change in this PR.

damping: controls.damping,
springStiffness: controls.springStiffness / 32,
orbitPaused: state.graphOrbitPaused,
Expand DownExpand Up@@ -2585,12 +2610,21 @@
const GRAPH_BLACK_HOLE_MASS_BASELINE = 160;
function graphBlackHoleMassMultiplier(controlValue) {
const value = number(controlValue);
/* Keep the established lower half and neutral default. Above 160, every +10 slider units
adds exactly +0.10 to the compact central-mass multiplier: 160→1.0, 170→1.1, 180→1.2.
Local stellar wells remain owned exclusively by Local solar gravity. */
return value <= GRAPH_BLACK_HOLE_MASS_BASELINE
? Math.max(0, value / GRAPH_BLACK_HOLE_MASS_BASELINE)
: 1 + (value - GRAPH_BLACK_HOLE_MASS_BASELINE) / 100;
/* Map the visible 20..500 range to 0.0..2.0 with the default (160) at 1.0.
Piecewise linear: below the default the multiplier rises from 0 to 1,
above the default it rises from 1 to 2. The earlier formula (value/160
for the lower half, 1 + (value-160)/100 for the upper half) sent 0.125
at the slider's HTML minimum and 4.4 at its maximum, so the engine
force jumped from a near-zero floor to a 4x ceiling while the default
sat at 1.0 — a 35x range that made the slider feel "alive" only at the
extremes. The new mapping gives a clean 0..2 range with a smooth,
predictable response around the default. */
if (!Number.isFinite(value)) return 1;
const lo = 20, hi = 500, base = GRAPH_BLACK_HOLE_MASS_BASELINE;
if (value <= base) {
return Math.max(0, (value - lo) / (base - lo));
}
return 1 + (value - base) / (hi - base);
Comment on lines +2625 to +2627

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Synchronize the Playwright slider expectations

In the browser-accessibility Playwright job, tests/e2e/graph-engine.spec.js:1902-1909 still asserts the previous response-gain contract: mass inputs 170/180 must yield 1.2/1.4, and the final settings must be {gravitationalConstant: 4, blackHoleMass: 2.6, localGravitationalConstant: 3, damping: 3, springStiffness: 3}. With this piecewise mapping and the new gain bypass, those values are approximately 1.029/1.059 and {1.5, 1.235, 1.25, 2, 2}, so npx playwright test deterministically fails; update the browser contract alongside the production normalization.

AGENTS.md reference: AGENTS.md:L92-L96

Useful? React with 👍 / 👎.

}


Expand Down
42 changes: 33 additions & 9 deletions tests/e2e/graph-engine.spec.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,7 +13,7 @@ const { test, expect } = require('@playwright/test');
*/

const workspace = 'graph-e2e';
const stellarOrbitAssetVersion = '20260815-merge-ready-1';
const stellarOrbitAssetVersion = '20260828-slider-multiplier-fix';

// A small connected store: two clusters joined by one bridge, so communities, the legend and
// the bridge detector all have something real to work on.
Expand DownExpand Up@@ -1803,7 +1803,11 @@ for (const reducedMotion of [false, true]) {
.toBe(true);
expect(Math.abs(localTravel), JSON.stringify(evidence)).toBeGreaterThan(0.75);
expect(Math.abs(screenTravel), JSON.stringify(evidence)).toBeGreaterThan(0.75);
expect(screenChord, JSON.stringify(evidence)).toBeGreaterThan(15);
/* The normalized spacetime controls intentionally use the calibrated direct field rather
than the retired 4x local multiplier. The angular travel assertions above remain the
primary motion contract; keep this pixel-space sanity check above a clearly visible
13px chord without encoding the old overpowered response. */
expect(screenChord, JSON.stringify(evidence)).toBeGreaterThan(13);
expect(coRotatingSegments, JSON.stringify(evidence)).toBeGreaterThanOrEqual(9);
expect(phaseReversals, JSON.stringify(evidence)).toBe(0);
expect(Math.min(...localStepMagnitudes), JSON.stringify(evidence)).toBeGreaterThan(0.025);
Expand DownExpand Up@@ -1843,7 +1847,7 @@ for (const reducedMotion of [false, true]) {
expect(diagnostics.linkSetting).toBe(8);
expect(diagnostics.relationOrbitScale).toBeCloseTo(0.25, 12);
expect(diagnostics.gravitySetting).toBe(96);
expect(diagnostics.blackHoleGravity).toBeCloseTo(3230.6848639753507, 12);
expect(diagnostics.blackHoleGravity).toBeCloseTo(1615.3424319876754, 12);
expect(diagnostics.localGravity).toBeCloseTo(240, 12);
expect(diagnostics.systemOrbitSeedSpeedLimit).toBeCloseTo(23.4, 12);

Expand DownExpand Up@@ -1901,12 +1905,12 @@ test('served Ledger wires normalized spacetime controls, overlay, and orbit paus
});
expect(massSteps).toEqual([
{ control: 160, multiplier: 1 },
{ control: 170, multiplier: 1.2 },
{ control: 180, multiplier: 1.4 },
{ control: 170, multiplier: 1.0294117647058822 },
{ control: 180, multiplier: 1.0588235294117647 },
]);
await expect.poll(() => page.evaluate(() => window.__engraphisGraph.state().settings))
.toMatchObject({ gravitationalConstant: 4, blackHoleMass: 2.6,
localGravitationalConstant: 3, damping: 3, springStiffness: 3, orbitPaused: false });
.toMatchObject({ gravitationalConstant: 1.5, blackHoleMass: 1.2352941176470589,
localGravitationalConstant: 1.25, damping: 2, springStiffness: 2, orbitPaused: false });
const rangeResponse = await page.evaluate(() => {
const set = (id, value) => {
const control = document.getElementById(id);
Expand DownExpand Up@@ -1987,6 +1991,19 @@ test('served Ledger wires normalized spacetime controls, overlay, and orbit paus
expect(session.pageErrors).toEqual([]);
});

test('served Ledger exposes spacetime controls for non-Galaxy presets', async ({ page }) => {
const session = await openDashboard(page);
await page.goto('/');
await page.locator('.nav-item[data-view="relations"]').click();
await expect(page.locator('#graph-canvas canvas').first()).toBeAttached({ timeout: 20_000 });
await page.locator('[data-graph-preset-choice="compact"]').click();
await expect(page.locator('[data-graph-preset-choice="compact"]'))
.toHaveAttribute('aria-pressed', 'true');
await expect(page.locator('#graph-spacetime-tuning')).toBeVisible();
await expect(page.locator('#graph-spacetime-summary')).toHaveText('Spacetime · black-hole orbit controls');
expect(session.pageErrors).toEqual([]);
});

test('served Galaxy paints complete independent solar envelopes with a visible clearance',
async ({ page }, testInfo) => {
test.setTimeout(55_000);
Expand All@@ -2004,6 +2021,13 @@ test('served Galaxy paints complete independent solar envelopes with a visible c
const audit = window.__carrierPaintAudit;
return audit && audit.ids.every(id => (audit.counts[id] || 0) > 0);
}, null, { timeout: 20_000 });
/* The dashboard's first fit is asynchronous. Establish the baseline only after every
carrier has been painted inside that fitted viewport, otherwise a slow CI frame can
sample one edge carrier during the camera transition and report a false escape. */
await page.waitForFunction(() => {
const audit = window.__carrierPaintAudit;
return audit && audit.ids.every(id => audit.last[id] && audit.last[id].insideCanvas);
}, null, { timeout: 20_000 });
const paintBefore = await carrierPaintAuditSnapshot(page);
const before = await renderedSystemEnvelopeSnapshot(page);
const steps = await page.evaluate(() => window.__engraphisGraph.physicsDiagnostics().steps + 96);
Expand DownExpand Up@@ -2751,13 +2775,13 @@ test('served primary dashboard keeps local stellar orbits independent at Galaxy-
expect(systemCenterTravel, JSON.stringify(evidence)).toBeGreaterThan(0.25);
expect(after.anchor).toMatchObject({ id: 'black-hole', x: 0, y: 0, vx: 0, vy: 0 });
expect(after.settings.gravity).toBe(0);
expect(after.diagnostics.blackHoleGravity).toBeCloseTo(344.27076923076925, 8);
expect(after.diagnostics.blackHoleGravity).toBeCloseTo(172.13538461538462, 8);
expect(after.diagnostics.globalGravityFloorSetting).toBe(24);
expect(after.diagnostics.globalGravityFloorActive).toBe(true);
expect(after.diagnostics.systemGravity).toMatchObject({
gravitySetting: 0,
stellarGravityFloorSetting: 48,
stellarGravity: 5070,
stellarGravity: 2535,
eligibleStellarAnchors: 1,
fallbackAnchors: 0,
globalAnchors: 0,
Expand Down
4 changes: 2 additions & 2 deletions tests/e2e/ledger.spec.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -580,14 +580,14 @@ test('Ledger cache-busts a graph renderer that fetched but did not register', as
await expect(page.locator('#graph-empty')).toContainText('Graph unavailable');
expect(rendererRequests).toHaveLength(1);
const first = new URL(rendererRequests[0]);
expect(first.searchParams.get('v')).toBe('20260815-merge-ready-1');
expect(first.searchParams.get('v')).toBe('20260828-slider-multiplier-fix');
expect(first.searchParams.has('retry')).toBe(false);

await page.getByRole('button', { name: 'Reload data' }).click();
await expect(page.locator('#graph-count')).toContainText('3 entities · 1 relations');
expect(rendererRequests).toHaveLength(2);
const second = new URL(rendererRequests[1]);
expect(second.searchParams.get('v')).toBe('20260815-merge-ready-1');
expect(second.searchParams.get('v')).toBe('20260828-slider-multiplier-fix');
expect(second.searchParams.get('retry')).toBe('1');
});

Expand Down
Loading