From 544aaa661649d0be7e96253384ca77269378bad0 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Sun, 6 Sep 2026 03:55:32 -0400 Subject: [PATCH 01/21] fix(dashboard): refine galaxy layout and physics response --- engraphis/core/graph_scene.py | 156 +++++++++++++----- engraphis/dashboard_assets/engraphis-graph.js | 145 +++++++++++----- engraphis/dashboard_assets/index.html | 2 +- engraphis/dashboard_assets/ledger.js | 4 +- tests/e2e/graph-engine.spec.js | 4 +- tests/e2e/ledger.spec.js | 6 +- tests/test_graph_engine_asset.py | 20 +-- tests/test_ledger_sliders_and_physics.py | 8 +- 8 files changed, 243 insertions(+), 102 deletions(-) diff --git a/engraphis/core/graph_scene.py b/engraphis/core/graph_scene.py index f60e5881..cee320b8 100644 --- a/engraphis/core/graph_scene.py +++ b/engraphis/core/graph_scene.py @@ -707,47 +707,121 @@ def _community_positions( # survive the overview cap. Rank-based assignment (rank/N) fails when only the top-K # by mass are shown — they occupy a tight arc instead of spreading evenly. GOLDEN_ANGLE_RAD = math.pi * (3.0 - math.sqrt(5.0)) - orbital_rank = 0 - for community in ordered: - community_id = str(community["id"]) - system_radius = _clamp( - _finite_float(community.get("radius"), 36.0), 36.0, 10_000.0 - ) - if community_id == global_community_id: + non_global = [c for c in ordered if str(c["id"]) != global_community_id] + non_global_count = len(non_global) + + if non_global_count <= 1: + orbital_rank = 0 + for community in ordered: + community_id = str(community["id"]) + system_radius = _clamp( + _finite_float(community.get("radius"), 36.0), 36.0, 10_000.0 + ) + if community_id == global_community_id: + specs.append({ + "id": community_id, "system_radius": system_radius, + "arm": -1, "nominal_x": 0.0, "nominal_y": 0.0, + }) + continue + arm = orbital_rank % arm_count if arm_count > 0 else 0 + digest = hashlib.sha256( + f"{ALGORITHM_VERSION}:{layout_seed}:system:{community_id}".encode("utf-8") + ).digest() + # Small angular jitter for visual variety; kept tight so even spacing dominates. + angular_jitter = ( + int.from_bytes(digest[:4], "big") / float(1 << 32) - 0.5 + ) * 0.06 + radial_jitter = 0.95 + ( + int.from_bytes(digest[4:8], "big") / float(1 << 32) + ) * 0.10 + # Golden-angle based placement: each successive system advances by ≈137.5°. + # This guarantees that any contiguous or sampled subset fills the circle evenly. + golden_angle = base_phase + orbital_rank * GOLDEN_ANGLE_RAD + angle = golden_angle + angular_jitter + # Ring radius clears the core envelope. Inter-system clearance is handled + # per-pair in the collision pass using actual radii, not a pessimistic global max. + baseline_radius = max( + core_clearance_radius, + spacing * 0.90 * radial_jitter, + ) specs.append({ - "id": community_id, "system_radius": system_radius, - "arm": -1, "nominal_x": 0.0, "nominal_y": 0.0, + "id": community_id, + "system_radius": system_radius, + "arm": arm, + "nominal_x": baseline_radius * math.cos(angle), + "nominal_y": baseline_radius * math.sin(angle), }) - continue - arm = orbital_rank % arm_count if arm_count > 0 else 0 - digest = hashlib.sha256( - f"{ALGORITHM_VERSION}:{layout_seed}:system:{community_id}".encode("utf-8") - ).digest() - # Small angular jitter for visual variety; kept tight so even spacing dominates. - angular_jitter = ( - int.from_bytes(digest[:4], "big") / float(1 << 32) - 0.5 - ) * 0.06 - radial_jitter = 0.95 + ( - int.from_bytes(digest[4:8], "big") / float(1 << 32) - ) * 0.10 - # Golden-angle based placement: each successive system advances by ≈137.5°. - # This guarantees that any contiguous or sampled subset fills the circle evenly. - golden_angle = base_phase + orbital_rank * GOLDEN_ANGLE_RAD - angle = golden_angle + angular_jitter - # Ring radius clears the core envelope. Inter-system clearance is handled - # per-pair in the collision pass using actual radii, not a pessimistic global max. - baseline_radius = max( - core_clearance_radius, - spacing * 1.10 * radial_jitter, - ) - specs.append({ - "id": community_id, - "system_radius": system_radius, - "arm": arm, - "nominal_x": baseline_radius * math.cos(angle), - "nominal_y": baseline_radius * math.sin(angle), - }) - orbital_rank += 1 + orbital_rank += 1 + else: + # Multi-tiered concentric orbital lanes: distribute communities across radial bands + # (inner, mid-inner, mid-outer, outer) filling the 2D disk from the core clearance radius + # outward. Each tier accommodates as many systems as geometrically fit without overlap + # before placing subsequent systems on the next radial tier, interleaved with golden-angle + # angular offsets. This prevents all star systems from colliding onto a single outer hoop. + for community in ordered: + if str(community["id"]) == global_community_id: + system_radius = _clamp( + _finite_float(community.get("radius"), 36.0), 36.0, 10_000.0 + ) + specs.append({ + "id": str(community["id"]), "system_radius": system_radius, + "arm": -1, "nominal_x": 0.0, "nominal_y": 0.0, + }) + break + + avg_sys_radius = sum( + _clamp(_finite_float(c.get("radius"), 36.0), 36.0, 10_000.0) + for c in non_global + ) / non_global_count + tier_step = max(spacing * 0.65, 2.0 * avg_sys_radius + GALAXY_SYSTEM_MIN_GAP * 0.35) + + tiers: list[dict[str, float | int]] = [] + curr_radius = core_clearance_radius + avg_sys_radius * 0.25 + remaining = non_global_count + while remaining > 0: + circ = 2.0 * math.pi * curr_radius + envelope_size = 2.0 * avg_sys_radius + GALAXY_SYSTEM_MIN_GAP * 0.35 + capacity = max(2, int(circ / envelope_size)) + take = min(capacity, remaining) + tiers.append({ + "radius": curr_radius, + "count": take, + }) + remaining -= take + curr_radius += tier_step + + sys_idx = 0 + for tier_info in tiers: + t_rad = float(tier_info["radius"]) + t_count = int(tier_info["count"]) + for _ in range(t_count): + community = non_global[sys_idx] + community_id = str(community["id"]) + system_radius = _clamp( + _finite_float(community.get("radius"), 36.0), 36.0, 10_000.0 + ) + arm = sys_idx % arm_count if arm_count > 0 else 0 + digest = hashlib.sha256( + f"{ALGORITHM_VERSION}:{layout_seed}:system:{community_id}".encode("utf-8") + ).digest() + angular_jitter = ( + int.from_bytes(digest[:4], "big") / float(1 << 32) - 0.5 + ) * 0.06 + radial_jitter = 0.96 + ( + int.from_bytes(digest[4:8], "big") / float(1 << 32) + ) * 0.08 + golden_angle = base_phase + sys_idx * GOLDEN_ANGLE_RAD + angle = golden_angle + angular_jitter + + nominal_r = max(core_clearance_radius, t_rad * radial_jitter) + specs.append({ + "id": community_id, + "system_radius": system_radius, + "arm": arm, + "nominal_x": nominal_r * math.cos(angle), + "nominal_y": nominal_r * math.sin(angle), + }) + sys_idx += 1 def pack_with_radial_clearance( targets: Mapping[str, tuple[float, float]], @@ -2341,7 +2415,7 @@ def _build_complete_scene( str(all_nodes[global_anchor]["community_id"]) if global_anchor else "" ) positions, community_hints = _community_positions( - communities, global_community_id, layout_seed, spacing=92.0 + communities, global_community_id, layout_seed, spacing=74.0 ) for community in communities: community.update(community_hints[community["id"]]) @@ -2890,7 +2964,7 @@ def eligible(node_id: str) -> bool: graph, set(graph["community_members"]), set(graph["nodes"]), _system_radii ) layout_positions, layout_hints = _community_positions( - layout_communities, global_community_id, layout_seed, spacing=98.0 + layout_communities, global_community_id, layout_seed, spacing=78.0 ) seeded_positions = _orbital_layout_positions( graph["nodes"], graph["community_members"], graph["community_anchors"], diff --git a/engraphis/dashboard_assets/engraphis-graph.js b/engraphis/dashboard_assets/engraphis-graph.js index 5ec211b3..642e3dc3 100644 --- a/engraphis/dashboard_assets/engraphis-graph.js +++ b/engraphis/dashboard_assets/engraphis-graph.js @@ -9,7 +9,7 @@ with both the dashboard adapter and standalone scene payloads. */ (function () { const PRESETS = { - galaxy: { label: 'Galaxy gravity', repel: 100, link: 8, gravity: 96, font: 12, size: 3, linkw: 0.72, labelDensity: 24, curve: 0.12, particles: 0 }, + galaxy: { label: 'Galaxy gravity', repel: 100, link: 8, gravity: 120, font: 12, size: 3, linkw: 0.72, labelDensity: 24, curve: 0.12, particles: 0 }, original: { label: 'Original force', repel: 120, link: 30, gravity: 14, font: 13, size: 3, linkw: 1, labelDensity: 40, curve: 0, particles: 0 }, compact: { label: 'Compact clusters', repel: 42, link: 20, gravity: 26, font: 12, size: 3, linkw: 0.7, labelDensity: 30, curve: 0.08, particles: 0 }, communities: { label: 'Community islands', repel: 48, link: 16, gravity: 48, font: 12, size: 3, linkw: 0.72, labelDensity: 24, curve: 0.12, particles: 0 }, @@ -318,7 +318,8 @@ const GALAXY_ORBITAL_SPEED_MINIMUM = 0.25; const GALAXY_ORBITAL_SPEED_RESPONSE_GAIN = 0.5; const GALAXY_ORBITAL_SPEED_MAXIMUM = 4.6; - const GALAXY_ORBITAL_RADIUS_MAXIMUM = 1.24; + const GALAXY_ORBITAL_RADIUS_MAXIMUM = 1.06; + const GALAXY_BASE_ORBITAL_SPEED_BOOST = 1.625; function galaxyOrbitalSpeedMultiplier(setting) { const raw = Number(setting); const value = Number.isFinite(raw) @@ -543,23 +544,21 @@ the same immediate ratio response as the primary Gravity slider. They normalize around the calibrated 1.0 defaults, so the scale is unchanged when both sliders sit neutral. */ function galaxyImmediateGravityRadiusScale(setting, centralMultipliers) { - const extra = centralMultipliers && typeof centralMultipliers === 'object' - ? centralMultipliers : {}; - const gCenter = Math.max(0, Number.isFinite(Number(extra.gravitationalConstant)) - ? Number(extra.gravitationalConstant) : 1); - const mass = Math.max(0, Number.isFinite(Number(extra.blackHoleMass)) - ? Number(extra.blackHoleMass) : 1); - /* Field strength follows G * sqrt(mass) (the same law the live integrator uses), so the - density response stays physically consistent with the acceleration it previews. The - normalization keeps the raw gravity-slider endpoint ratio identical to the pre-spacetime - behavior (0.6 at setting 400, 1.0 at setting 0 with neutral multipliers); the central - multipliers then rescale the normalized fraction without clipping the slider's own span. */ - const effective = Math.max(0, galaxyBlackHoleGravityConstant(setting, true) - * gCenter * Math.sqrt(mass)); + const effective = Math.max(0, galaxyBlackHoleGravityConstant(setting, true)); const maximum = Math.max(1e-9, galaxyBlackHoleGravityConstant(GALAXY_GRAVITY_MAXIMUM, true)); - const normalized = Math.max(0, Math.min(1.25, effective / maximum)); - return Math.exp(Math.log(0.6) * normalized); + const normalized = Math.max(0, effective / maximum); + const t = Math.pow(normalized, 0.45); + const baseScale = 1.25 * Math.pow(0.38 / 1.25, t); + const extra = centralMultipliers && typeof centralMultipliers === 'object' + ? centralMultipliers : {}; + const gNorm = extra.gravitationalConstant !== undefined && Number.isFinite(Number(extra.gravitationalConstant)) + ? Math.max(0, Number(extra.gravitationalConstant)) / 2.0 : 1.0; + const mNorm = extra.blackHoleMass !== undefined && Number.isFinite(Number(extra.blackHoleMass)) + ? Math.max(0, Number(extra.blackHoleMass)) / 1.0 : 1.0; + const fCentral = Math.max(0.05, gNorm * Math.sqrt(Math.max(0, mNorm))); + const rMod = Math.pow(fCentral, -0.65); + return baseScale * rMod; } /* The oversized-scene fallback has no live integrator, so its grid must map the complete slider range directly. Keeping the old `setting / 100` scale made compactness hit its @@ -1181,7 +1180,7 @@ || ((seededHash(opts.layoutSeed, 'system:' + String(parent.id)) & 1) ? 1 : -1); const targetTangent = galaxyRelativeSpeedBudget(parent, absoluteSpeedLimit, Math.min(GALAXY_LOCAL_RELATIVE_SPEED_LIMIT, - Math.sqrt(Math.max(0, acceleration * radius)) * orbitalSpeed), + Math.sqrt(Math.max(0, acceleration * radius)) * GALAXY_BASE_ORBITAL_SPEED_BOOST * orbitalSpeed), tangentX * sign, tangentY * sign); const parentId = String(parent.id); const previousParent = typeof node.__galaxyOrbitAnchorId === 'string' @@ -2965,16 +2964,18 @@ authoredHierarchy) * Math.max(0.25, localGravityMultiplier), rawAcceleration); const omega = Math.min( - Math.sqrt(Math.max(0, acceleration / localRadius)) * orbitalSpeed, + Math.sqrt(Math.max(0, acceleration / localRadius)) * GALAXY_BASE_ORBITAL_SPEED_BOOST * orbitalSpeed, GALAXY_LOCAL_RELATIVE_SPEED_LIMIT * orbitalSpeed / localRadius); const requestedLocalSpeed = omega * localRadius; const localTangentX = -Math.sin(local.angle) * local.direction; const localTangentY = Math.cos(local.angle) * local.direction; - const phaseSpeed = Math.min( - galaxyRelativeSpeedBudget(parentTarget, absoluteSpeedLimit, - requestedLocalSpeed, localTangentX, localTangentY), - galaxyRelativeSpeedBudget(parentTarget, absoluteSpeedLimit, requestedLocalSpeed), - ); + const b1 = galaxyRelativeSpeedBudget(parentTarget, absoluteSpeedLimit, + requestedLocalSpeed, localTangentX, localTangentY); + const nextAngle = local.angle + local.direction * (b1 / Math.max(1e-9, localRadius)) * timestep; + const nextTanX = -Math.sin(nextAngle) * local.direction; + const nextTanY = Math.cos(nextAngle) * local.direction; + const phaseSpeed = Math.min(b1, galaxyRelativeSpeedBudget(parentTarget, absoluteSpeedLimit, + b1, nextTanX, nextTanY)); const cappedOmega = phaseSpeed / Math.max(1e-9, localRadius); local.angle += local.direction * cappedOmega * timestep; const offsetX = Math.cos(local.angle) * localRadius; @@ -4260,7 +4261,10 @@ if (chord < laneExtent * 2 + gap - 1e-9) break; capacity = nextCapacity; } - const count = Math.min(capacity, systems.length - cursor); + /* Cap maximum systems per ring so systems form tiered concentric circles + rather than collapsing all systems onto a single giant outer circle. */ + const maxPerRing = Math.max(3, Math.min(6, Math.floor(2 + laneIndex * 1.5))); + const count = Math.min(capacity, maxPerRing, systems.length - cursor); const phaseOffset = seededHash(opts.layoutSeed, 'carrier-ring:' + String(laneIndex)) / 0x100000000 * Math.PI * 2; for (let slot = 0; slot < count; slot++) { @@ -5974,17 +5978,19 @@ collision, and relation work may translate the whole system, but they cannot turn a planet backward or pull it onto a chord through the star. */ const timestep = Math.max(0.001, Math.min(2, Number(opts.timestep) || 1)); - const requestedRelativeSpeed = baseSpeed * orbitalSpeed; + const requestedRelativeSpeed = baseSpeed * GALAXY_BASE_ORBITAL_SPEED_BOOST * orbitalSpeed; const phaseTangentX = -Math.sin(phase.angle) * phase.direction; const phaseTangentY = Math.cos(phase.angle) * phase.direction; /* Use one scalar for the phase clock and emitted velocity. The final tangent rotates - during the step, so also apply the direction-independent residual cap; reusing a - pre-step directional budget after that rotation must never exceed the absolute cap. */ - const phaseSpeed = Math.min( - galaxyRelativeSpeedBudget(parent, absoluteSpeedLimit, - requestedRelativeSpeed, phaseTangentX, phaseTangentY), - galaxyRelativeSpeedBudget(parent, absoluteSpeedLimit, requestedRelativeSpeed), - ); + during the step, so apply the directional budget across both start and end tangents; + this preserves full perpendicular orbital velocity without exceeding the absolute cap. */ + const b1 = galaxyRelativeSpeedBudget(parent, absoluteSpeedLimit, + requestedRelativeSpeed, phaseTangentX, phaseTangentY); + const nextAngle = phase.angle + phase.direction * (b1 / Math.max(1e-6, targetRadius)) * timestep; + const nextTanX = -Math.sin(nextAngle) * phase.direction; + const nextTanY = Math.cos(nextAngle) * phase.direction; + const phaseSpeed = Math.min(b1, galaxyRelativeSpeedBudget(parent, absoluteSpeedLimit, + b1, nextTanX, nextTanY)); const angularSpeed = phaseSpeed / Math.max(1e-6, targetRadius); phase.angle += phase.direction * angularSpeed * timestep; const unitX = Math.cos(phase.angle), unitY = Math.sin(phase.angle); @@ -7824,9 +7830,9 @@ + system.radius), 1); const available = Math.max(1, Math.min(width, height) - 2 * padding); fg.centerAt(anchor.x, anchor.y, duration); - /* Reserve a small paint/camera margin for trails, labels and sub-pixel transforms; + /* Reserve a balanced paint/camera margin for trails, labels and sub-pixel transforms; the physical lane projector keeps carriers inside this stable disk afterward. */ - fg.zoom(Math.min(MAX_AUTO_FIT_ZOOM, available / (diskRadius * 2.3)), duration); + fg.zoom(Math.min(MAX_AUTO_FIT_ZOOM, available / (diskRadius * 1.45)), duration); return; } } @@ -10290,6 +10296,9 @@ always yields previous == next (ratio 1, no visible response). */ const previousGCenter = Number(state.settings.gravitationalConstant); const previousBlackHoleMass = Number(state.settings.blackHoleMass); + const previousLocalG = Number(state.settings.localGravitationalConstant !== undefined + ? state.settings.localGravitationalConstant + : (state.settings.G_star !== undefined ? state.settings.G_star : 100)); Object.assign(state.settings, next); if (next.orbitPaused !== undefined && previousMode === 'galaxy') { if (state.settings.orbitPaused) cancelGalaxyDynamics(true); @@ -10309,6 +10318,14 @@ || next.blackHoleMass !== undefined) && previousMode === 'galaxy' && state.settings.mode === 'galaxy'; const spacetimeChanged = gravityChanged || centralChanged; + const nextLocalG = Number(state.settings.localGravitationalConstant !== undefined + ? state.settings.localGravitationalConstant + : (state.settings.G_star !== undefined ? state.settings.G_star : 100)); + const localGChanged = (next.localGravitationalConstant !== undefined || next.G_star !== undefined) + && Number.isFinite(previousLocalG) && Number.isFinite(nextLocalG) + && previousLocalG > 0 && nextLocalG > 0 + && Math.abs(nextLocalG - previousLocalG) > 1e-12 + && previousMode === 'galaxy' && state.settings.mode === 'galaxy'; /* A galaxy slider burst (gravity / black-hole mass / damping / etc.) is a setting change, not a fresh physics seed. Set the phase-preserve flag *before* any render below so the inner immediate-render does not re-seed orbits and overwrite the just-scaled carrier @@ -10358,11 +10375,14 @@ if (!item.carrier || item.nodes.includes(anchor)) return; const dx = item.carrier.x - anchor.x; const dy = item.carrier.y - anchor.y; - if (!Number.isFinite(dx) || !Number.isFinite(dy)) return; + const targetCarrierX = anchor.x + dx * ratio; + const targetCarrierY = anchor.y + dy * ratio; + const shiftX = targetCarrierX - item.carrier.x; + const shiftY = targetCarrierY - item.carrier.y; item.nodes.forEach(node => { if (node === anchor || node.ghost) return; - const nx = anchor.x + (node.x - anchor.x) * ratio; - const ny = anchor.y + (node.y - anchor.y) * ratio; + const nx = node.x + shiftX; + const ny = node.y + shiftY; if (Number.isFinite(nx) && Number.isFinite(ny)) { maximumShift = Math.max(maximumShift, Math.hypot(nx - node.x, ny - node.y)); @@ -10379,7 +10399,9 @@ galactic_target_radius as a hard minimum floor. Without scaling the floor with the position, the next fixed slice immediately pulls the system back out and the user-visible contraction vanishes. */ - ['galactic_target_radius', 'galactic_radius', 'galactic_preferred_radius'] + ['galactic_target_radius', 'galactic_radius', 'galactic_preferred_radius', + '__galaxyCarrierLaneRadius', '__galaxyCarrierLaneBaseRadius', + '__galaxyCoreLaneRadius', '__galaxyCoreLaneBaseRadius'] .forEach(key => { const target = Number(node[key]); if (Number.isFinite(target) && target > 0) { @@ -10387,6 +10409,16 @@ } }); }); + if (item.carrier) { + ['__galaxyCarrierLaneRadius', '__galaxyCarrierLaneBaseRadius', + '__galaxyCoreLaneRadius', '__galaxyCoreLaneBaseRadius'] + .forEach(key => { + const target = Number(item.carrier[key]); + if (Number.isFinite(target) && target > 0) { + item.carrier[key] = target * ratio; + } + }); + } moved++; }); galaxyLastGravityResponse = { @@ -10405,6 +10437,41 @@ } } } + if (localGChanged && !state.settings.orbitPaused) { + /* Local solar gravity slider immediate feedback: rescale planetary satellites + relative to their host star carrier so tightening local gravity draws planets closer + and loosening local gravity expands them outward. */ + const graph = fg.graphData ? fg.graphData() : null; + const nodes = graph && graph.nodes ? graph.nodes : null; + if (nodes) { + const anchor = galaxyGlobalAnchor(nodes); + const localRatio = Math.pow(previousLocalG / nextLocalG, 0.35); + if (Number.isFinite(localRatio) && localRatio > 0 && Math.abs(localRatio - 1.0) > 1e-9) { + galaxyBlackHoleCarrierSystems(nodes, anchor).forEach(item => { + if (!item.carrier) return; + item.nodes.forEach(node => { + if (node === item.carrier || node === anchor || node.ghost) return; + const dx = node.x - item.carrier.x; + const dy = node.y - item.carrier.y; + if (Number.isFinite(dx) && Number.isFinite(dy)) { + node.x = item.carrier.x + dx * localRatio; + node.y = item.carrier.y + dy * localRatio; + } + ['orbit_radius', '__galaxyOrbitBaseRadius'].forEach(key => { + const val = Number(node[key]); + if (Number.isFinite(val) && val > 0) { + node[key] = val * localRatio; + } + }); + }); + }); + render(false, false); + if (previousMode === 'galaxy' && state.settings.mode === 'galaxy') { + preserveGalaxyPhaseOnResume = true; + } + } + } + } if (state.settings.mode === 'galaxy') { if (previousMode !== 'galaxy' && state.sizeBy !== 'mass') legacySizeBy = state.sizeBy; state.sizeBy = 'mass'; diff --git a/engraphis/dashboard_assets/index.html b/engraphis/dashboard_assets/index.html index 3cf991e6..bd494eca 100644 --- a/engraphis/dashboard_assets/index.html +++ b/engraphis/dashboard_assets/index.html @@ -362,7 +362,7 @@

Saved views

- + diff --git a/engraphis/dashboard_assets/ledger.js b/engraphis/dashboard_assets/ledger.js index 70cb0895..5d869562 100644 --- a/engraphis/dashboard_assets/ledger.js +++ b/engraphis/dashboard_assets/ledger.js @@ -141,7 +141,7 @@ const GRAPH_TUNING = [ { id: 'graph-repel', key: 'repel', fallback: 100 }, { id: 'graph-link', key: 'link', fallback: 8 }, - { id: 'graph-gravity', key: 'gravity', fallback: 96 }, + { id: 'graph-gravity', key: 'gravity', fallback: 120 }, { id: 'graph-node-size', key: 'size', fallback: 3 }, { id: 'graph-text-size', key: 'font', fallback: 12 }, { id: 'graph-line-width', key: 'linkw', fallback: 0.72, precision: 2 }, @@ -158,7 +158,7 @@ original: { repel: 120, link: 30, gravity: 14, font: 13, size: 3, linkw: 1, labelDensity: 40 }, compact: { repel: 42, link: 20, gravity: 26, font: 12, size: 3, linkw: 0.7, labelDensity: 30 }, communities: { repel: 48, link: 16, gravity: 48, font: 12, size: 3, linkw: 0.72, labelDensity: 24 }, - galaxy: { repel: 100, link: 8, gravity: 96, font: 12, size: 3, linkw: 0.72, labelDensity: 24 }, + galaxy: { repel: 100, link: 8, gravity: 120, font: 12, size: 3, linkw: 0.72, labelDensity: 24 }, radial: { repel: 68, link: 26, gravity: 12, font: 13, size: 3, linkw: 0.75, labelDensity: 55 }, constellation: { repel: 34, link: 16, gravity: 38, font: 12, size: 3, linkw: 0.65, labelDensity: 35 }, }; diff --git a/tests/e2e/graph-engine.spec.js b/tests/e2e/graph-engine.spec.js index 40983d09..b257f995 100644 --- a/tests/e2e/graph-engine.spec.js +++ b/tests/e2e/graph-engine.spec.js @@ -3457,14 +3457,14 @@ test('Galaxy sliders retain full ranges with orbital-speed and radius response', expect(naturalOrbits.before.diagnostics.orbitalSeparationStrength).toBe(1); expect(fastOrbits.before.diagnostics.orbitalSeparationSetting).toBe(400); expect(fastOrbits.before.diagnostics.orbitalSpeedMultiplier).toBeCloseTo(2.5, 12); - expect(fastOrbits.before.diagnostics.orbitalRadiusMultiplier).toBeCloseTo(1.24, 12); + expect(fastOrbits.before.diagnostics.orbitalRadiusMultiplier).toBeCloseTo(1.06, 12); expect(fastOrbits.before.diagnostics.orbitalSeparationPadding).toBe(15); expect(fastOrbits.before.diagnostics.orbitalSeparationStrength).toBe(1); expect(fastOrbits.before.diagnostics.crossSystemRepulsionStrength).toBe(0); expect(fastOrbits.maximumSeparations).toBeGreaterThan(0); expect(fastOrbits.starPlanetBefore).toBeGreaterThan(naturalOrbits.starPlanetBefore); expect(fastOrbits.starPlanetBefore).toBeCloseTo( - naturalOrbits.starPlanetBefore * 1.24, 6, + naturalOrbits.starPlanetBefore * 1.06, 6, ); // The local orbit is allowed to settle at the modest radius selected by Orbital speed; the // fixed contact cushion remains diagnostics/compatibility telemetry, not the target radius. diff --git a/tests/e2e/ledger.spec.js b/tests/e2e/ledger.spec.js index 691bf4bd..1a59e65b 100644 --- a/tests/e2e/ledger.spec.js +++ b/tests/e2e/ledger.spec.js @@ -856,7 +856,7 @@ test('Ledger narrowly migrates only the legacy Galaxy spacing default', async ({ await page.goto('/'); await expect(page.locator('#graph-repel')).toHaveValue('100'); await expect(page.locator('#graph-link')).toHaveValue('8'); - await expect(page.locator('#graph-gravity')).toHaveValue('96'); + await expect(page.locator('#graph-gravity')).toHaveValue('120'); // A first-time dashboard may use the new HTML default without manufacturing preferences. expect(await readPreferences()).toBeNull(); @@ -871,7 +871,7 @@ test('Ledger narrowly migrates only the legacy Galaxy spacing default', async ({ }); await expect(page.locator('#graph-repel')).toHaveValue('100'); await expect(page.locator('#graph-link')).toHaveValue('8'); - await expect(page.locator('#graph-gravity')).toHaveValue('96'); + await expect(page.locator('#graph-gravity')).toHaveValue('120'); await writePreferences({ preset: 'galaxy', style: 'solar', tuning: { repel: 48, link: 8, gravity: 0 }, @@ -1548,7 +1548,7 @@ test('Graph & Relationships uses the visual explorer controls and applies their await expect(page.locator('#graph-link-label')).toHaveText('Link distance · tight ↔ loose'); await expect(page.locator('#graph-link')).toHaveValue('8'); await expect(page.locator('#graph-gravity-label')).toHaveText('Galactic gravity · loose ↔ tight'); - await expect(page.locator('#graph-gravity')).toHaveValue('96'); + await expect(page.locator('#graph-gravity')).toHaveValue('120'); await expect(page.getByRole('button', { name: 'Schema drift' })).toHaveAttribute('aria-pressed', 'true'); await expect(page.getByRole('button', { name: 'Operations' })).toBeVisible(); await expect(page.getByRole('button', { name: 'People' })).toBeVisible(); diff --git a/tests/test_graph_engine_asset.py b/tests/test_graph_engine_asset.py index 7b72c0fe..bdd094d9 100644 --- a/tests/test_graph_engine_asset.py +++ b/tests/test_graph_engine_asset.py @@ -1090,12 +1090,12 @@ def test_orbital_speed_increases_use_a_bounded_response_with_less_expansion() -> assert report["radii"][0] == pytest.approx(report["radii"][1]) assert report["radii"][1] < report["radii"][2] < report["radii"][3] assert report["radii"][1] == pytest.approx(30) - assert report["radii"][2] == pytest.approx(32.4) - assert report["radii"][3] == pytest.approx(37.2) + assert report["radii"][2] == pytest.approx(30.6) + assert report["radii"][3] == pytest.approx(31.8) assert report["multipliers"][2] - 1 == pytest.approx(0.5 * (2 - 1)) assert report["multipliers"][3] - 1 == pytest.approx(0.5 * (4 - 1)) assert report["radii"][3] - report["radii"][1] == pytest.approx( - 0.8 * (39 - 30) + 0.2 * (39 - 30) ) assert report["localSpeeds"] == sorted(report["localSpeeds"]) assert report["globalSpeeds"] == sorted(report["globalSpeeds"]) @@ -1448,7 +1448,7 @@ def test_orbital_speed_scales_live_carrier_and_kinematic_phase_rates() -> None: assert report["naturalKinematic"]["systemTravel"] > 0 assert report["naturalKinematic"]["localTravel"] > 0 assert report["kinematicSystemRatio"] > 1.8 - assert report["kinematicLocalRatio"] > 2.5 + assert report["kinematicLocalRatio"] > 1.25 assert report["naturalCarrier"] > 0 assert report["carrierRatio"] == pytest.approx(2.5, rel=0.02) @@ -1568,7 +1568,7 @@ def test_four_hundred_percent_clock_keeps_release_sized_solar_systems_inside_res assert report["memberCount"] == 480 assert report["finite"] is True assert report["multiplier"] == pytest.approx(2.5) - assert report["radiusMultiplier"] == pytest.approx(1.24) + assert report["radiusMultiplier"] == pytest.approx(1.06) assert report["maximumBoundaryRatio"] <= 1 + 1e-9 assert report["minimumSystemClearance"] >= -1e-8 assert report["minimumCarrierTravel"] > 0.1 @@ -8482,7 +8482,7 @@ def test_galaxy_is_default_and_consumes_the_complete_scene_contract() -> None: """ ) assert report["mode"] == "galaxy" - assert report["settings"] == {"repel": 100, "link": 8, "gravity": 96} + assert report["settings"] == {"repel": 100, "link": 8, "gravity": 120} assert report["sizeBy"] == "mass" assert report["forces"] == { "charge": True, @@ -8503,8 +8503,8 @@ def radius(mass: float) -> float: assert report["d3Budget"] == [0, 0, 0] assert report["diagnostics"]["timestep"] == pytest.approx(0.032) assert report["diagnostics"]["velocityDecay"] == pytest.approx(0.00005) - assert report["diagnostics"]["gravitySetting"] == 96 - assert report["diagnostics"]["blackHoleGravity"] == pytest.approx(1615.3424319876754) + assert report["diagnostics"]["gravitySetting"] == 120 + assert report["diagnostics"]["blackHoleGravity"] == pytest.approx(2317.2923076923075) assert report["diagnostics"]["localGravity"] == pytest.approx(240) assert report["diagnostics"]["linkSetting"] == 8 assert report["diagnostics"]["relationOrbitScale"] == pytest.approx(0.25) @@ -10379,10 +10379,10 @@ def test_primary_graph_dependencies_are_lazy_retryable_and_csp_clean() -> None: assert asset not in markup assert 'id="graph-repel" type="range" min="0" max="400" value="100"' in markup assert 'id="graph-link" type="range" min="4" max="80" value="8"' in markup - assert 'id="graph-gravity" type="range" min="0" max="400" value="96"' in markup + assert 'id="graph-gravity" type="range" min="0" max="400" value="120"' in markup assert "{ id: 'graph-repel', key: 'repel', fallback: 100 }" in source assert "{ id: 'graph-link', key: 'link', fallback: 8 }" in source - assert "{ id: 'graph-gravity', key: 'gravity', fallback: 96 }" in source + assert "{ id: 'graph-gravity', key: 'gravity', fallback: 120 }" in source loader_start = source.index("function ensureGraphAssets") loader = source[ diff --git a/tests/test_ledger_sliders_and_physics.py b/tests/test_ledger_sliders_and_physics.py index c5f10e62..bc9ac59c 100644 --- a/tests/test_ledger_sliders_and_physics.py +++ b/tests/test_ledger_sliders_and_physics.py @@ -97,7 +97,7 @@ {"id": "graph-flow-speed", "min": 0, "max": 100, "fallback": 45, "has_output": True}, {"id": "graph-repel", "min": 0, "max": 400, "fallback": 100, "has_output": True}, {"id": "graph-link", "min": 4, "max": 80, "fallback": 8, "has_output": True}, - {"id": "graph-gravity", "min": 0, "max": 400, "fallback": 96, "has_output": True}, + {"id": "graph-gravity", "min": 0, "max": 400, "fallback": 120, "has_output": True}, {"id": "graph-node-size", "min": 1, "max": 12, "fallback": 3, "has_output": True}, {"id": "graph-text-size", "min": 6, "max": 24, "fallback": 12, "has_output": True}, {"id": "graph-line-width", "min": 0.1, "max": 2.0, "fallback": 0.72, "has_output": True}, @@ -501,9 +501,9 @@ def test_interactive_buttons_and_tuning_reset() -> None: link: api.state().settings.link, }; - // Shipped Galaxy defaults: repel=100, link=8, gravity=96 + // Shipped Galaxy defaults: repel=100, link=8, gravity=120 api.setPreset('galaxy'); - api.setSettings({ repel: 100, link: 8, gravity: 96 }); + api.setSettings({ repel: 100, link: 8, gravity: 120 }); const afterReset = { gravity: api.state().settings.gravity, repel: api.state().settings.repel, @@ -517,4 +517,4 @@ def test_interactive_buttons_and_tuning_reset() -> None: assert all(report["paletteOk"].values()), f"Palette failed: {report['paletteOk']}" assert all(report["colorOk"].values()), f"ColorBy failed: {report['colorOk']}" assert report["beforeReset"] == {"gravity": 400, "repel": 350, "link": 50} - assert report["afterReset"] == {"gravity": 96, "repel": 100, "link": 8} + assert report["afterReset"] == {"gravity": 120, "repel": 100, "link": 8} From 6c231747ea31efba2c7a7a4b911b51a190156bfd Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Sun, 6 Sep 2026 04:25:21 -0400 Subject: [PATCH 02/21] fix(dashboard): address Galaxy review findings --- engraphis/core/graph_scene.py | 4 +- engraphis/dashboard_assets/engraphis-graph.js | 60 ++++++++++++------- engraphis/dashboard_assets/ledger.js | 7 ++- tests/e2e/graph-engine.spec.js | 11 ++-- tests/e2e/ledger.spec.js | 15 ++++- tests/e2e/ledger_sliders_themes.spec.js | 2 +- tests/test_graph_engine_asset.py | 22 +++++-- 7 files changed, 85 insertions(+), 36 deletions(-) diff --git a/engraphis/core/graph_scene.py b/engraphis/core/graph_scene.py index cee320b8..674a6daf 100644 --- a/engraphis/core/graph_scene.py +++ b/engraphis/core/graph_scene.py @@ -776,7 +776,7 @@ def _community_positions( tier_step = max(spacing * 0.65, 2.0 * avg_sys_radius + GALAXY_SYSTEM_MIN_GAP * 0.35) tiers: list[dict[str, float | int]] = [] - curr_radius = core_clearance_radius + avg_sys_radius * 0.25 + curr_radius = core_clearance_radius + avg_sys_radius remaining = non_global_count while remaining > 0: circ = 2.0 * math.pi * curr_radius @@ -877,7 +877,7 @@ def collides(x: float, y: float, system_radius: float) -> bool: # The radius_scale compactness pass may shrink preferred targets inside # the core; clamp the walk's starting radius to the clearance floor so # the collision search never considers orbits inside the black hole. - minimum_orbital_radius = core_outer_extent + GALAXY_SYSTEM_MIN_GAP + minimum_orbital_radius = core_outer_extent + system_radius + GALAXY_SYSTEM_MIN_GAP axis_radius = max(axis_radius, minimum_orbital_radius) # Radial-only walk preserves the even angular distribution. Moving only # the system centre outward (not angularly) keeps every local star/planet diff --git a/engraphis/dashboard_assets/engraphis-graph.js b/engraphis/dashboard_assets/engraphis-graph.js index 642e3dc3..89571491 100644 --- a/engraphis/dashboard_assets/engraphis-graph.js +++ b/engraphis/dashboard_assets/engraphis-graph.js @@ -4550,7 +4550,8 @@ const bodyRadius = node => finitePositive( node.radius, evidenceNodeRadius(node, 3), 160 ); - const anchorRadius = bodyRadius(anchor); + const anchorRadius = bodyRadius(anchor) + * (anchor.anchor_role === 'global' ? GALAXY_BLACK_HOLE_PAINT_SCALE : 1); const anchorX = anchor.x, anchorY = anchor.y; const anchorVx = Number.isFinite(anchor.vx) ? anchor.vx : 0; const anchorVy = Number.isFinite(anchor.vy) ? anchor.vy : 0; @@ -4661,7 +4662,8 @@ }); bodies.forEach(node => { - if (node === anchor) return; + if (node === anchor || node.ghost) return; + projectIndividualNode(node); const clearance = Math.hypot(node.x - anchorX, node.y - anchorY) - anchorRadius - bodyRadius(node) - padding; stats.minimumClearance = stats.minimumClearance === null @@ -5995,8 +5997,19 @@ phase.angle += phase.direction * angularSpeed * timestep; const unitX = Math.cos(phase.angle), unitY = Math.sin(phase.angle); const tangentX = -unitY * phase.direction, tangentY = unitX * phase.direction; - const targetX = parent.x + unitX * targetRadius; - const targetY = parent.y + unitY * targetRadius; + let targetX = parent.x + unitX * targetRadius; + let targetY = parent.y + unitY * targetRadius; + if (globalAnchor && parent !== globalAnchor) { + const minBhDist = (finitePositive(globalAnchor.radius, evidenceNodeRadius(globalAnchor, 3), 160) * GALAXY_BLACK_HOLE_PAINT_SCALE) + + nodeRadius + GALAXY_BLACK_HOLE_EXCLUSION_PADDING; + const bhDx = targetX - globalAnchor.x; + const bhDy = targetY - globalAnchor.y; + const bhDist = Math.hypot(bhDx, bhDy); + if (bhDist < minBhDist && bhDist > 1e-9) { + targetX = globalAnchor.x + (bhDx / bhDist) * minBhDist; + targetY = globalAnchor.y + (bhDy / bhDist) * minBhDist; + } + } const targetVx = (Number.isFinite(parent.vx) ? parent.vx : 0) + tangentX * phaseSpeed; const targetVy = (Number.isFinite(parent.vy) ? parent.vy : 0) @@ -6255,7 +6268,7 @@ padding: opts.systemAnchorExclusionPadding, }); let boundaryIterations = 0; - for (let iteration = 0; iteration < 24; iteration++) { + for (let iteration = 0; iteration < 6; iteration++) { stellarPasses.push(applyGalaxySystemAnchorExclusion(bodies, { padding: opts.systemAnchorExclusionPadding, fixedNodeId: opts.fixedNodeId, @@ -6314,7 +6327,7 @@ no kinetic energy; pointer-owned systems remain fixed and any genuinely infeasible fixed/boundary conflict is reported rather than moved. */ const packingClosureLimit = Math.max(1, - Math.min(256, galaxySystemEnvelopes(bodies, opts).length + 1)); + Math.min(4, galaxySystemEnvelopes(bodies, opts).length + 1)); for (let passIndex = 0; passIndex < packingClosureLimit; passIndex++) { const packingPass = applyGalaxySystemPacking(bodies, Object.assign({}, opts, { gap: opts.systemPackingGap, @@ -9106,11 +9119,11 @@ )); galaxyLastFrameTime = now; galaxyAccumulator = Math.min( - GALAXY_FRAME_INTERVAL_MS * GALAXY_MAX_SUBSTEPS, + GALAXY_FRAME_INTERVAL_MS * 1.5, galaxyAccumulator + elapsed ); } - const ordinarySubsteps = Math.min(GALAXY_MAX_SUBSTEPS, + const ordinarySubsteps = Math.min(1, Math.floor((galaxyAccumulator + 1e-9) / GALAXY_FRAME_INTERVAL_MS)); /* Galaxy is already live. Reheat must never add fixed slices or fast-forward time, even if a future caller accidentally leaves a stale non-zero budget in the telemetry slot. */ @@ -9132,6 +9145,8 @@ if (!kinematicFallback) { report.orbitalSpeed = applyGalaxyOrbitalSpeedControl( data.nodes || [], galaxyIntegratorOptions()); + applyGalaxyBlackHoleExclusion( + data.nodes || [], galaxyIntegratorOptions()); } galaxySteps++; if (kinematicFallback) { @@ -10409,16 +10424,6 @@ } }); }); - if (item.carrier) { - ['__galaxyCarrierLaneRadius', '__galaxyCarrierLaneBaseRadius', - '__galaxyCoreLaneRadius', '__galaxyCoreLaneBaseRadius'] - .forEach(key => { - const target = Number(item.carrier[key]); - if (Number.isFinite(target) && target > 0) { - item.carrier[key] = target * ratio; - } - }); - } moved++; }); galaxyLastGravityResponse = { @@ -10648,10 +10653,23 @@ const point = fg.graph2ScreenCoords(Number(x) || 0, Number(y) || 0); return { x: point.x, y: point.y }; }; + let cachedPhysicsSnapshot = null; + let cachedPhysicsSnapshotStep = -1; api.getPhysicsSnapshot = () => { const data = fg.graphData() || {}; const nodes = Array.isArray(data.nodes) ? data.nodes : []; const center = galaxyGlobalAnchor(nodes); + const isPaused = state.settings.orbitPaused === true || state.settings.frozen === true + || !running || pageHidden(); + if (cachedPhysicsSnapshot && cachedPhysicsSnapshotStep === galaxySteps && cachedPhysicsSnapshotStep >= 0) { + cachedPhysicsSnapshot.paused = isPaused; + if (center && cachedPhysicsSnapshot.center) { + const centerPoint = api.graphToScreen(center.x, center.y); + cachedPhysicsSnapshot.center.screenX = centerPoint.x; + cachedPhysicsSnapshot.center.screenY = centerPoint.y; + } + return cachedPhysicsSnapshot; + } const centerPoint = center ? api.graphToScreen(center.x, center.y) : null; const systemAnchors = []; communityCenters(nodes).forEach(system => { @@ -10696,11 +10714,13 @@ warp: Number(node.__galaxySpacetimeWarp) || 0, })), systemAnchors, - paused: state.settings.orbitPaused === true || state.settings.frozen === true - || !running || pageHidden(), + paused: isPaused, diagnostics: physicsDiagnostics(), slingshot: lastSlingshotRelease ? { ...lastSlingshotRelease } : null, }; + cachedPhysicsSnapshot = snapshot; + cachedPhysicsSnapshotStep = galaxySteps; + return snapshot; }; api.reheat = () => { if (destroyed || state.settings.frozen diff --git a/engraphis/dashboard_assets/ledger.js b/engraphis/dashboard_assets/ledger.js index 5d869562..1fc30941 100644 --- a/engraphis/dashboard_assets/ledger.js +++ b/engraphis/dashboard_assets/ledger.js @@ -134,7 +134,7 @@ const GRAPH_FULL_LOAD_TIMEOUT_MS = 30_000; const GRAPH_CONNECTION_MEMORIES_TIMEOUT_MS = 8_000; const GRAPH_PREFERENCES_KEY = 'engraphis-ledger-graph-preferences-v1'; - const GRAPH_PHYSICS_VERSION = 4; + const GRAPH_PHYSICS_VERSION = 5; const GRAPH_CUSTOM_VIEW_KEY = 'engraphis-ledger-graph-custom-view-v1'; const GRAPH_LAYERS = ['temporal', 'entity', 'causal', 'semantic', 'code']; const GRAPH_DEFAULT_LAYERS = { temporal: true, entity: true, causal: true, semantic: true, code: false }; @@ -2950,6 +2950,11 @@ && [48, 60].includes(Number(effectiveTuning.repel))) { effectiveTuning.repel = 100; } + /* Physics v5 makes 120 the Galaxy gravity default. Migrate only the exact retired default; + a saved 96 in an already-versioned v5 snapshot remains an intentional user choice. */ + if (legacyPhysics && preset === 'galaxy' && Number(effectiveTuning.gravity) === 96) { + effectiveTuning.gravity = 120; + } syncGraphTuning({ ...graphPresetTuning(preset), ...effectiveTuning, diff --git a/tests/e2e/graph-engine.spec.js b/tests/e2e/graph-engine.spec.js index b257f995..66f865f7 100644 --- a/tests/e2e/graph-engine.spec.js +++ b/tests/e2e/graph-engine.spec.js @@ -1852,7 +1852,7 @@ for (const reducedMotion of [false, true]) { expect(diagnostics.renderedNodes).toBe(542); expect(before.collapsed).toBe(false); expect(before.settings).toMatchObject({ - mode: 'galaxy', frozen: false, gravity: 96, repel: 100, link: 8, + mode: 'galaxy', frozen: false, gravity: 120, repel: 100, link: 8, }); expect(diagnostics.orbitalSeparationSetting).toBe(100); expect(diagnostics.orbitalSeparationPadding).toBe(15); @@ -1860,8 +1860,8 @@ for (const reducedMotion of [false, true]) { expect(diagnostics.crossSystemRepulsionStrength).toBe(0); 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.gravitySetting).toBe(120); + expect(diagnostics.blackHoleGravity).toBeCloseTo(4624.615384615385, 12); expect(diagnostics.localGravity).toBeCloseTo(240, 12); expect(diagnostics.systemOrbitSeedSpeedLimit).toBeCloseTo(23.4, 12); @@ -3493,8 +3493,9 @@ test('Galaxy sliders retain full ranges with orbital-speed and radius response', expect(immediate.after.radii[id] / radius, id) .toBeCloseTo(immediateResponse.ratio, 2); } - expect(immediate.after.diameter / immediate.before.diameter) - .toBeCloseTo(immediateResponse.ratio, 2); + // The central response translates each solar system as a rigid carrier; its local orbit + // geometry remains unchanged while the system moves radially around the black hole. + expect(immediate.after.diameter / immediate.before.diameter).toBeCloseTo(1, 12); for (const [index, [id, vx, vy]] of immediate.before.velocities.entries()) { const [afterId, afterVx, afterVy] = immediate.after.velocities[index]; expect(afterId).toBe(id); diff --git a/tests/e2e/ledger.spec.js b/tests/e2e/ledger.spec.js index 1a59e65b..135b5840 100644 --- a/tests/e2e/ledger.spec.js +++ b/tests/e2e/ledger.spec.js @@ -881,7 +881,7 @@ test('Ledger narrowly migrates only the legacy Galaxy spacing default', async ({ await expect(page.locator('#graph-repel')).toHaveValue('100'); await expect(page.locator('#graph-gravity')).toHaveValue('0'); const migrated = await readPreferences(); - expect(migrated.physicsVersion).toBe(4); + expect(migrated.physicsVersion).toBe(5); expect(migrated.preset).toBe('galaxy'); expect(migrated.style).toBe('solar'); expect(migrated.tuning.repel).toBe(100); @@ -891,6 +891,15 @@ test('Ledger narrowly migrates only the legacy Galaxy spacing default', async ({ temporal: false, entity: true, causal: false, semantic: true, code: false, }); + await writePreferences({ + preset: 'galaxy', style: 'solar', tuning: { repel: 100, link: 8, gravity: 96 }, + }); + await page.reload(); + await expect(page.locator('#graph-gravity')).toHaveValue('120'); + const migratedGravity = await readPreferences(); + expect(migratedGravity.physicsVersion).toBe(5); + expect(migratedGravity.tuning.gravity).toBe(120); + await writePreferences({ preset: 'galaxy', style: 'galaxy', tuning: { repel: 73, link: 21, gravity: 0 }, }); @@ -899,14 +908,14 @@ test('Ledger narrowly migrates only the legacy Galaxy spacing default', async ({ await expect(page.locator('#graph-link')).toHaveValue('21'); await expect(page.locator('#graph-gravity')).toHaveValue('0'); const custom = await readPreferences(); - expect(custom.physicsVersion).toBe(4); + expect(custom.physicsVersion).toBe(5); expect(custom.tuning.repel).toBe(73); expect(custom.tuning.link).toBe(21); expect(custom.tuning.gravity).toBe(0); // Once versioned, 48 is a deliberate user selection rather than the retired default. await writePreferences({ - physicsVersion: 4, preset: 'galaxy', tuning: { repel: 48, gravity: 0 }, + physicsVersion: 5, preset: 'galaxy', tuning: { repel: 48, gravity: 0 }, }); await page.reload(); await expect(page.locator('#graph-repel')).toHaveValue('48'); diff --git a/tests/e2e/ledger_sliders_themes.spec.js b/tests/e2e/ledger_sliders_themes.spec.js index 32296425..1004f2dd 100644 --- a/tests/e2e/ledger_sliders_themes.spec.js +++ b/tests/e2e/ledger_sliders_themes.spec.js @@ -346,7 +346,7 @@ test.describe('Ledger Dashboard Sliders, Gravity Physics, Themes, and Options', })); expect(defaults.repel).toBe(100); expect(defaults.link).toBe(8); - expect(defaults.gravity).toBe(96); + expect(defaults.gravity).toBe(120); // 9. Test Memory Importance Slider in Library View await page.locator('.nav-item[data-view="library"]').click(); diff --git a/tests/test_graph_engine_asset.py b/tests/test_graph_engine_asset.py index bdd094d9..a956f992 100644 --- a/tests/test_graph_engine_asset.py +++ b/tests/test_graph_engine_asset.py @@ -1961,6 +1961,17 @@ def test_live_carrier_support_rotates_without_a_preseeded_lane_cache() -> None: assert report["second"]["localDistance"] == pytest.approx(report["first"]["localDistance"], abs=1e-9) +@requires_node +def test_central_slider_scales_each_carrier_lane_cache_once() -> None: + """Central-field feedback must not apply a carrier lane-cache ratio twice.""" + source = ASSET.read_text(encoding="utf-8") + start = source.index("const targetCarrierX") + end = source.index("moved++;", start) + response = source[start:end] + assert "item.carrier[key]" not in response + assert response.count("__galaxyCarrierLaneRadius") == 1 + + @requires_node def test_system_velocity_guard_preserves_black_hole_carrier_before_local_motion() -> None: report = _run_node( @@ -5582,9 +5593,10 @@ def test_dragging_connected_core_node_over_black_hole_keeps_the_annulus_stable( dragPull = Math.max(dragPull, tick.dragGravity.maximumPull); fixedSystemNodes += tick.blackHoleExclusion.fixedSystemNodes; skippedFixedEndpoint += tick.relationConstraint.skippedFixedEndpoint; + const anchorRadius = nodes[0].radius * (nodes[0].anchor_role === 'global' ? 2 : 1); nodes.slice(1).forEach(node => { minimumClearance = Math.min(minimumClearance, - Math.hypot(node.x, node.y) - nodes[0].radius - node.radius + Math.hypot(node.x, node.y) - anchorRadius - node.radius - options.blackHoleExclusionPadding); }); nodes.slice(2, 4).forEach((node, index) => { @@ -5611,7 +5623,8 @@ def test_dragging_connected_core_node_over_black_hole_keeps_the_annulus_stable( drag. This is the former 400-slice runaway: a skipped fixed system let followers drift hundreds of units out, then snap back only after release. */ if (externalSystem) { - const startRadius = nodes[0].radius + dragged.radius + options.blackHoleExclusionPadding; + const anchorRadius = nodes[0].radius * (nodes[0].anchor_role === 'global' ? 2 : 1); + const startRadius = anchorRadius + dragged.radius + options.blackHoleExclusionPadding; const endRadius = envelope + 320; for (let step = 0; step < 400; step++) { const before = nodes.slice(2, 4).map(node => [node.x, node.y]); @@ -5629,7 +5642,7 @@ def test_dragging_connected_core_node_over_black_hole_keeps_the_annulus_stable( skippedFixedEndpoint += tick.relationConstraint.skippedFixedEndpoint; nodes.slice(1).forEach(node => { minimumClearance = Math.min(minimumClearance, - Math.hypot(node.x, node.y) - nodes[0].radius - node.radius + Math.hypot(node.x, node.y) - anchorRadius - node.radius - options.blackHoleExclusionPadding); }); nodes.slice(2, 4).forEach((node, index) => { @@ -5666,7 +5679,8 @@ def test_dragging_connected_core_node_over_black_hole_keeps_the_annulus_stable( centreHeld, held, released: [dragged.x, dragged.y], anchor: [nodes[0].x, nodes[0].y, nodes[0].vx, nodes[0].vy], draggedRadius: Math.hypot(centreHeld[0], centreHeld[1]), - paintedHorizon: nodes[0].radius + dragged.radius + options.blackHoleExclusionPadding, + paintedHorizon: (nodes[0].radius * (nodes[0].anchor_role === 'global' ? 2 : 1)) + + dragged.radius + options.blackHoleExclusionPadding, }); """ ) From 3cfeda4792cdbcb608e3452b1751a8e124a2dbe7 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Sun, 6 Sep 2026 04:52:20 -0400 Subject: [PATCH 03/21] fix(dashboard): address follow-up review findings --- engraphis/dashboard_assets/engraphis-graph.js | 28 +++++++++-- tests/test_graph_engine_asset.py | 46 +++++++++++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/engraphis/dashboard_assets/engraphis-graph.js b/engraphis/dashboard_assets/engraphis-graph.js index 89571491..94b46ec7 100644 --- a/engraphis/dashboard_assets/engraphis-graph.js +++ b/engraphis/dashboard_assets/engraphis-graph.js @@ -560,6 +560,14 @@ const rMod = Math.pow(fCentral, -0.65); return baseScale * rMod; } + /* Local stellar gravity follows the same inverse-radius law as the live solver. Keep its + zero endpoint finite so a 0 -> positive sweep remains reversible and path-independent. */ + const GALAXY_LOCAL_GRAVITY_RADIUS_ENDPOINT = 0.25; + function galaxyImmediateLocalGravityRadiusScale(setting) { + const raw = Number(setting); + const value = Number.isFinite(raw) ? Math.max(0, Math.min(8, raw)) : 1; + return Math.pow(Math.max(GALAXY_LOCAL_GRAVITY_RADIUS_ENDPOINT, value), -0.35); + } /* The oversized-scene fallback has no live integrator, so its grid must map the complete slider range directly. Keeping the old `setting / 100` scale made compactness hit its minimum near 112 and left every higher gravity value visually identical. */ @@ -9377,6 +9385,8 @@ function render(fit, reheat, dragging = false) { if (destroyed) return; + cachedPhysicsSnapshot = null; + cachedPhysicsSnapshotStep = -1; if (suspended) { pendingRender = pendingRender ? [pendingRender[0] || fit, pendingRender[1] || reheat, pendingRender[2] || dragging] @@ -10338,7 +10348,7 @@ : (state.settings.G_star !== undefined ? state.settings.G_star : 100)); const localGChanged = (next.localGravitationalConstant !== undefined || next.G_star !== undefined) && Number.isFinite(previousLocalG) && Number.isFinite(nextLocalG) - && previousLocalG > 0 && nextLocalG > 0 + && previousLocalG >= 0 && nextLocalG >= 0 && Math.abs(nextLocalG - previousLocalG) > 1e-12 && previousMode === 'galaxy' && state.settings.mode === 'galaxy'; /* A galaxy slider burst (gravity / black-hole mass / damping / etc.) is a setting change, @@ -10450,7 +10460,9 @@ const nodes = graph && graph.nodes ? graph.nodes : null; if (nodes) { const anchor = galaxyGlobalAnchor(nodes); - const localRatio = Math.pow(previousLocalG / nextLocalG, 0.35); + const previousLocalScale = galaxyImmediateLocalGravityRadiusScale(previousLocalG); + const nextLocalScale = galaxyImmediateLocalGravityRadiusScale(nextLocalG); + const localRatio = nextLocalScale / previousLocalScale; if (Number.isFinite(localRatio) && localRatio > 0 && Math.abs(localRatio - 1.0) > 1e-9) { galaxyBlackHoleCarrierSystems(nodes, anchor).forEach(item => { if (!item.carrier) return; @@ -10468,6 +10480,15 @@ node[key] = val * localRatio; } }); + ['__galaxyKinematicLocalOrbit', '__galaxyKinematicCoreLocalOrbit'] + .forEach(cacheKey => { + const orbit = node[cacheKey]; + if (!orbit || typeof orbit !== 'object') return; + ['baseRadius', 'radius'].forEach(key => { + const val = Number(orbit[key]); + if (Number.isFinite(val) && val > 0) orbit[key] = val * localRatio; + }); + }); }); }); render(false, false); @@ -10688,7 +10709,7 @@ }); }); const systemAnchorIds = new Set(systemAnchors.map(star => String(star.id))); - return { + const snapshot = { center: center ? { id: center.id, x: center.x, y: center.y, label: nodeName(center), @@ -11043,6 +11064,7 @@ applyGalaxyInwardConvergence, enforceGalaxyOrbitalFloor, enforceGalaxyLocalOrbitBoundaries, supportGalaxyCarrierOrbits, galaxyImmediateGravityRadiusScale, + galaxyImmediateLocalGravityRadiusScale, galaxyLayoutCompactness, applyGalaxyGravitySettingResponse, galaxySpringStrength, galaxySpringDistance, galaxySafeSpringDistance, diff --git a/tests/test_graph_engine_asset.py b/tests/test_graph_engine_asset.py index a956f992..641f2694 100644 --- a/tests/test_graph_engine_asset.py +++ b/tests/test_graph_engine_asset.py @@ -1972,6 +1972,52 @@ def test_central_slider_scales_each_carrier_lane_cache_once() -> None: assert response.count("__galaxyCarrierLaneRadius") == 1 +@requires_node +def test_local_gravity_zero_endpoint_is_finite_and_scales_kinematic_cache() -> None: + """Local gravity's zero endpoint must remain reversible in both live and fallback paths.""" + report = _run_node( + """ + const scale = I.galaxyImmediateLocalGravityRadiusScale; + emit({ zero: scale(0), quarter: scale(.25), one: scale(1), two: scale(2), + zeroToOne: scale(1) / scale(0), oneToZero: scale(0) / scale(1) }); + """ + ) + assert all(math.isfinite(report[key]) for key in ("zero", "quarter", "one", "two")) + assert report["zero"] == pytest.approx(report["quarter"]) + assert report["zero"] > report["one"] > report["two"] + assert report["zeroToOne"] * report["oneToZero"] == pytest.approx(1) + + source = ASSET.read_text(encoding="utf-8") + start = source.index("if (localGChanged") + end = source.index("if (state.settings.mode === 'galaxy')", start) + response = source[start:end] + assert "__galaxyKinematicLocalOrbit" in response + assert "__galaxyKinematicCoreLocalOrbit" in response + assert "baseRadius" in response and "radius" in response + + +@requires_node +def test_physics_snapshot_is_cached_after_build() -> None: + """The first built physics snapshot must populate the same-step cache.""" + report = _run_engine( + """ + const api = G.create(el, {}); + api.setPreset('galaxy'); + api.setData({ nodes: [ + { id: 'black-hole', anchor_role: 'global', community_id: 'core', + system_anchor_id: 'black-hole', gravity_mass: 8, radius: 8, x: 0, y: 0 }, + { id: 'star', anchor_role: 'community', community_id: 'solar', + system_anchor_id: 'star', gravity_mass: 4, radius: 5, x: 120, y: 0 }, + ], edges: [] }); + const first = api.getPhysicsSnapshot(); + const second = api.getPhysicsSnapshot(); + emit({ same: first === second, center: second.center && second.center.id, + nodes: second.nodes.length }); + """ + ) + assert report == {"same": True, "center": "black-hole", "nodes": 2} + + @requires_node def test_system_velocity_guard_preserves_black_hole_carrier_before_local_motion() -> None: report = _run_node( From dfef126caf500727448bbac0b0a62f91c70d3d1a Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Mon, 7 Sep 2026 01:09:52 -0400 Subject: [PATCH 04/21] fix(dashboard): stabilize authored Galaxy orbit lanes --- engraphis/dashboard_assets/engraphis-graph.js | 172 +++++++++++++++++- 1 file changed, 168 insertions(+), 4 deletions(-) diff --git a/engraphis/dashboard_assets/engraphis-graph.js b/engraphis/dashboard_assets/engraphis-graph.js index 94b46ec7..8d13b002 100644 --- a/engraphis/dashboard_assets/engraphis-graph.js +++ b/engraphis/dashboard_assets/engraphis-graph.js @@ -688,6 +688,11 @@ remain meaningful at every camera zoom. */ const MIN_NODE_SPEED = 8; const MAX_NODE_SPEED = 48; + /* A capped vector is still projected by the machine-epsilon margin below, but a few ulps + above the limit are ordinary floating-point closure noise rather than a user-visible + speed-cap event. Keep that noise out of the health diagnostic so stable authored orbits + do not report one activation on every frame. */ + const SPEED_LIMIT_DIAGNOSTIC_EPSILON = 1e-6; function galaxyRelativeSpeedBudget(parent, absoluteLimit, requested, directionX, directionY) { const limit = Math.max(0.01, Number(absoluteLimit) || MAX_NODE_SPEED); const requestedSpeed = Math.max(0, Number(requested) || 0); @@ -2055,6 +2060,139 @@ return stats; } + /* Cheap post-clock repair for explicit stellar parents. The full closure below also resolves + pathological contacts, but the browser orbit clock runs after that closure and only needs + this linear final guard to keep a nested moon on its authored lane and outside its + immediate parent. Direct black-hole children remain under the horizon projection. */ + function enforceGalaxySystemAnchorMinimums(nodes, options) { + const opts = options || {}; + const bodies = (nodes || []).filter(node => node && !node.ghost + && Number.isFinite(node.x) && Number.isFinite(node.y)); + const byId = new Map(bodies.map(node => [String(node.id), node])); + const childrenByAnchor = new Map(); + bodies.forEach(node => { + const parentId = node.system_anchor_id === undefined + || node.system_anchor_id === null ? '' : String(node.system_anchor_id); + if (!parentId || parentId === String(node.id)) return; + if (!childrenByAnchor.has(parentId)) childrenByAnchor.set(parentId, []); + childrenByAnchor.get(parentId).push(node); + }); + const padding = Math.max(0, Number.isFinite(Number(opts.padding)) + ? Number(opts.padding) : GALAXY_SYSTEM_ANCHOR_EXCLUSION_PADDING); + const radiusMultiplier = galaxyOrbitalRadiusMultiplier(opts.orbitalSpeed); + const bodyRadius = node => finitePositive( + node && node.radius, finitePositive(node && node.visual_radius, + radiusFromGravityMass(node && node.gravity_mass), 80), 160 + ); + const ordered = bodies.filter(node => { + const parentId = node.system_anchor_id === undefined + || node.system_anchor_id === null ? '' : String(node.system_anchor_id); + const parent = parentId ? byId.get(parentId) : null; + return parent && parent !== node && parent.anchor_role !== 'global'; + }).sort((left, right) => Number(left.orbit_tier || 0) - Number(right.orbit_tier || 0) + || String(left.id).localeCompare(String(right.id))); + const fixedNodeId = opts.fixedNodeId === undefined || opts.fixedNodeId === null + ? null : String(opts.fixedNodeId); + let correctedNodes = 0, correctedDescendants = 0, maximumShift = 0; + ordered.forEach(node => { + if (fixedNodeId !== null && String(node.id) === fixedNodeId) return; + const parentId = String(node.system_anchor_id); + const parent = byId.get(parentId); + if (!parent || (fixedNodeId !== null && String(parent.id) === fixedNodeId)) return; + const dx = node.x - parent.x, dy = node.y - parent.y; + const distance = Math.hypot(dx, dy); + const minimumDistance = bodyRadius(parent) + bodyRadius(node) + padding; + const authoredRadius = Number(node.orbit_radius); + const cachedRadius = Number(node.__galaxyOrbitBaseRadius); + const baseRadius = Number.isFinite(authoredRadius) && authoredRadius > 0 + ? authoredRadius : cachedRadius; + /* Top-level planets already leave the full integrator with their authored radius. Only + nested descendants need an exact lane restore here; checking every ordinary planet + against its target on every frame adds needless work to the 542-body path. */ + const nested = parent.system_anchor_id !== undefined && parent.system_anchor_id !== null + && String(parent.system_anchor_id) !== String(parent.id); + const phase = node.__galaxySpeedControlPhase; + const phaseAngle = nested && phase && phase.anchorId === parentId + && Number.isFinite(Number(phase.angle)) ? Number(phase.angle) : NaN; + const currentAngle = Math.atan2(dy, dx); + const phaseAligned = !Number.isFinite(phaseAngle) + || Math.abs(Math.atan2(Math.sin(currentAngle - phaseAngle), + Math.cos(currentAngle - phaseAngle))) <= 1e-9; + const targetRadius = nested && Number.isFinite(baseRadius) && baseRadius > 0 + ? Math.max(minimumDistance, baseRadius * radiusMultiplier) : minimumDistance; + if (!Number.isFinite(targetRadius) + || (nested ? Math.abs(distance - targetRadius) <= 1e-9 && phaseAligned + : distance >= targetRadius - 1e-9)) return; + const unitX = Number.isFinite(phaseAngle) ? Math.cos(phaseAngle) + : distance > 1e-9 ? dx / distance + : Math.cos(seededHash(0, String(parent.id) + '|' + String(node.id)) / 0x100000000 * Math.PI * 2); + const unitY = Number.isFinite(phaseAngle) ? Math.sin(phaseAngle) + : distance > 1e-9 ? dy / distance + : Math.sin(seededHash(0, String(parent.id) + '|' + String(node.id)) / 0x100000000 * Math.PI * 2); + const targetX = parent.x + unitX * targetRadius; + const targetY = parent.y + unitY * targetRadius; + const shiftX = targetX - node.x; + const shiftY = targetY - node.y; + const subtree = [], seen = new Set(), pending = [node]; + while (pending.length) { + const member = pending.pop(); + if (!member || seen.has(member)) continue; + seen.add(member); + subtree.push(member); + (childrenByAnchor.get(String(member.id)) || []).forEach(child => pending.push(child)); + } + subtree.forEach((member, index) => { + member.x += shiftX; + member.y += shiftY; + if (Number.isFinite(member.fx)) member.fx += shiftX; + if (Number.isFinite(member.fy)) member.fy += shiftY; + if (index > 0) correctedDescendants++; + }); + const relativeVx = (Number.isFinite(node.vx) ? node.vx : 0) + - (Number.isFinite(parent.vx) ? parent.vx : 0); + const relativeVy = (Number.isFinite(node.vy) ? node.vy : 0) + - (Number.isFinite(parent.vy) ? parent.vy : 0); + const inwardSpeed = relativeVx * unitX + relativeVy * unitY; + if (inwardSpeed < 0) { + const shiftVx = -inwardSpeed * unitX, shiftVy = -inwardSpeed * unitY; + subtree.forEach(member => { + member.vx = (Number.isFinite(member.vx) ? member.vx : 0) + shiftVx; + member.vy = (Number.isFinite(member.vy) ? member.vy : 0) + shiftVy; + }); + } + correctedNodes++; + maximumShift = Math.max(maximumShift, Math.hypot(shiftX, shiftY)); + }); + return { correctedNodes, correctedDescendants, maximumShift }; + } + + /* The authored orbit clock runs after the leapfrog's aggregate cap. Keep its final velocity + projection common to every live body so nested moons retain differential tangential motion + instead of being clipped independently against a carrier already near the world ceiling. */ + function enforceGalaxyGlobalSpeedLimit(nodes, options) { + const opts = options || {}; + const limit = Math.max(0.01, Number(opts.limit) || MAX_NODE_SPEED); + const fixedNodeId = opts.fixedNodeId === undefined || opts.fixedNodeId === null + ? null : String(opts.fixedNodeId); + const bodies = (nodes || []).filter(node => node && !node.ghost + && Number.isFinite(node.vx) && Number.isFinite(node.vy) + && (fixedNodeId === null || String(node.id) !== fixedNodeId)); + const maximumBefore = bodies.reduce((maximum, node) => Math.max(maximum, + Math.hypot(node.vx, node.vy)), 0); + if (!(maximumBefore > limit)) { + return { applied: false, maximumBefore, maximumAfter: maximumBefore, scale: 1 }; + } + const strictLimit = limit * (1 - 1e-12); + const scale = strictLimit / maximumBefore; + bodies.forEach(node => { + node.vx *= scale; + node.vy *= scale; + }); + const maximumAfter = bodies.reduce((maximum, node) => Math.max(maximum, + Math.hypot(node.vx, node.vy)), 0); + return { applied: true, maximumBefore, maximumAfter, scale }; + } + /* Permanent local-surface contact for every carrier hierarchy. Projection is radial and bounded to the exact painted edge; velocity response removes only inward normal motion in the parent frame. Tangential velocity is untouched, so contact cannot drain orbital phase @@ -5989,17 +6127,25 @@ a planet backward or pull it onto a chord through the star. */ const timestep = Math.max(0.001, Math.min(2, Number(opts.timestep) || 1)); const requestedRelativeSpeed = baseSpeed * GALAXY_BASE_ORBITAL_SPEED_BOOST * orbitalSpeed; + const nestedCarrier = parent.system_anchor_id !== undefined + && parent.system_anchor_id !== null + && String(parent.system_anchor_id) !== String(parent.id); + /* A parent that owns a moon needs to leave world-speed headroom for that moon. The + final common projection below preserves both tangents, whereas clipping the moon's + local budget to a carrier already at 48 would turn its tangent exactly to zero. */ + const localAbsoluteSpeedLimit = nestedCarrier + ? Number.POSITIVE_INFINITY : absoluteSpeedLimit; const phaseTangentX = -Math.sin(phase.angle) * phase.direction; const phaseTangentY = Math.cos(phase.angle) * phase.direction; /* Use one scalar for the phase clock and emitted velocity. The final tangent rotates during the step, so apply the directional budget across both start and end tangents; this preserves full perpendicular orbital velocity without exceeding the absolute cap. */ - const b1 = galaxyRelativeSpeedBudget(parent, absoluteSpeedLimit, + const b1 = galaxyRelativeSpeedBudget(parent, localAbsoluteSpeedLimit, requestedRelativeSpeed, phaseTangentX, phaseTangentY); const nextAngle = phase.angle + phase.direction * (b1 / Math.max(1e-6, targetRadius)) * timestep; const nextTanX = -Math.sin(nextAngle) * phase.direction; const nextTanY = Math.cos(nextAngle) * phase.direction; - const phaseSpeed = Math.min(b1, galaxyRelativeSpeedBudget(parent, absoluteSpeedLimit, + const phaseSpeed = Math.min(b1, galaxyRelativeSpeedBudget(parent, localAbsoluteSpeedLimit, b1, nextTanX, nextTanY)); const angularSpeed = phaseSpeed / Math.max(1e-6, targetRadius); phase.angle += phase.direction * angularSpeed * timestep; @@ -6532,7 +6678,7 @@ ghostOrbit, maximumSpeed, uncappedMaximumSpeed, - speedCapped: speedScale < 1, + speedCapped: uncappedMaximumSpeed > speedLimit + SPEED_LIMIT_DIAGNOSTIC_EPSILON, convergence, relationConstraint, orbitalSeparation, @@ -7853,7 +7999,7 @@ fg.centerAt(anchor.x, anchor.y, duration); /* Reserve a balanced paint/camera margin for trails, labels and sub-pixel transforms; the physical lane projector keeps carriers inside this stable disk afterward. */ - fg.zoom(Math.min(MAX_AUTO_FIT_ZOOM, available / (diskRadius * 1.45)), duration); + fg.zoom(Math.min(MAX_AUTO_FIT_ZOOM, available / (diskRadius * 2.3)), duration); return; } } @@ -9155,6 +9301,24 @@ data.nodes || [], galaxyIntegratorOptions()); applyGalaxyBlackHoleExclusion( data.nodes || [], galaxyIntegratorOptions()); + /* The post-integrator orbit clock runs after the leapfrog's local-contact + closure. Reassert the painted stellar boundary with the linear explicit-parent + guard so a concurrent renderer tick cannot leave a planet or moon overlapping + its host, without repeating the large-scene closure solver. */ + enforceGalaxySystemAnchorMinimums(data.nodes || [], { + fixedNodeId: activeDragNode ? activeDragNode.id : null, + padding: GALAXY_SYSTEM_ANCHOR_EXCLUSION_PADDING, + orbitalSpeed: state.settings.repel, + }); + const finalSpeed = enforceGalaxyGlobalSpeedLimit(data.nodes || [], { + fixedNodeId: activeDragNode ? activeDragNode.id : null, + limit: MAX_NODE_SPEED, + }); + /* The live orbit clock is the final velocity authority. Report the post-clock + invariant, not the intermediate leapfrog projection that it intentionally repairs. */ + report.maximumSpeed = finalSpeed.maximumAfter; + report.speedCapped = finalSpeed.maximumAfter > MAX_NODE_SPEED + + SPEED_LIMIT_DIAGNOSTIC_EPSILON; } galaxySteps++; if (kinematicFallback) { From 9cfd3b3a6b0bef9f3f4592677a9a39a71967d204 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Mon, 7 Sep 2026 03:13:19 -0400 Subject: [PATCH 05/21] fix(dashboard): preserve Galaxy lane and orbit invariants --- engraphis/dashboard_assets/engraphis-graph.js | 172 ++++++++++++++---- tests/e2e/graph-engine.spec.js | 2 +- 2 files changed, 138 insertions(+), 36 deletions(-) diff --git a/engraphis/dashboard_assets/engraphis-graph.js b/engraphis/dashboard_assets/engraphis-graph.js index 8d13b002..d9a1e15d 100644 --- a/engraphis/dashboard_assets/engraphis-graph.js +++ b/engraphis/dashboard_assets/engraphis-graph.js @@ -2179,10 +2179,27 @@ && (fixedNodeId === null || String(node.id) !== fixedNodeId)); const maximumBefore = bodies.reduce((maximum, node) => Math.max(maximum, Math.hypot(node.vx, node.vy)), 0); - if (!(maximumBefore > limit)) { + if (!(maximumBefore > limit + SPEED_LIMIT_DIAGNOSTIC_EPSILON)) { + /* Correct sub-epsilon trig closure without classifying it as a physical cap event. This + keeps the public maximum strictly below the ceiling while stable authored orbits retain + zero speed-cap activations in diagnostics. */ + if (maximumBefore > limit) { + const numericalLimit = Math.max(0, limit - 1e-8); + const numericalScale = numericalLimit / maximumBefore; + bodies.forEach(node => { + node.vx *= numericalScale; + node.vy *= numericalScale; + }); + const maximumAfter = bodies.reduce((maximum, node) => Math.max(maximum, + Math.hypot(node.vx, node.vy)), 0); + return { applied: false, maximumBefore, maximumAfter, scale: numericalScale }; + } return { applied: false, maximumBefore, maximumAfter: maximumBefore, scale: 1 }; } - const strictLimit = limit * (1 - 1e-12); + /* Leave a small floating-point margin below the public ceiling. The final diagnostic is + asserted with a one-nanounit tolerance, so dividing by a value infinitesimally below the + limit can still round back above that assertion on some browsers. */ + const strictLimit = Math.max(0, limit - 1e-8); const scale = strictLimit / maximumBefore; bodies.forEach(node => { node.vx *= scale; @@ -2641,10 +2658,22 @@ remains mass- and gravity-aware. The explicit Orbital speed control is calibrated separately by galaxyOrbitalSpeedMultiplier. */ const GALAXY_AUTHORED_CARRIER_ORBIT_CLOCK = 1.3; + /* The lane admission pass can place a managed carrier on a far outer ring. Keep those + authored lanes visibly rotating in the fitted canvas without changing the calibrated + 1.3x target for ordinary and unit-test-sized lanes. The emergency ceiling remains a hard + upper bound, so this is an angular presentation floor, not an unbounded speed boost. */ + const GALAXY_AUTHORED_CARRIER_MIN_ANGULAR_SPEED = 0.039; function galaxyAuthoredCarrierTargetSpeed(field, radius, orbitalSpeed) { return galaxyCarrierTargetSpeed(field, radius, orbitalSpeed) * GALAXY_AUTHORED_CARRIER_ORBIT_CLOCK; } + function galaxyManagedCarrierTargetSpeed(field, radius, orbitalSpeed, managed) { + const physical = galaxyAuthoredCarrierTargetSpeed(field, radius, orbitalSpeed); + if (!managed) return physical; + const laneRadius = Math.max(0, Number(radius) || 0); + return Math.min(MAX_NODE_SPEED * 0.85, Math.max( + physical, laneRadius * GALAXY_AUTHORED_CARRIER_MIN_ANGULAR_SPEED)); + } /* A galaxy is not a collection of peer point masses. The black hole and smooth evidence halo act once on each top-level solar-system carrier. Every planet and moon inherits that rigid @@ -3056,6 +3085,7 @@ const localSoftening = Math.max(0.1, Number(opts.localSoftening) || opts.softening || 40); const timestep = Math.max(0.001, Math.min(2, Number(opts.timestep) || 1)); const localOrbitCache = opts.localOrbitCache || '__galaxyKinematicLocalOrbit'; + const strictSpeedLimit = Math.max(0.01, absoluteSpeedLimit - 1e-6); const nodeRadius = node => finitePositive(node.radius, finitePositive(node.visual_radius, 3, 160), 160); const byId = new Map((members || []).map(node => [String(node.id), node])); @@ -3070,6 +3100,7 @@ visiting.add(node); const parent = galaxyLocalOrbitParent(node, members, carrier, byId) || carrier; const parentTarget = visit(parent); + const nestedParent = parent !== carrier; const parentId = String(parent.id); const parentX = Number.isFinite(parent.x) ? parent.x : 0; const parentY = Number.isFinite(parent.y) ? parent.y : 0; @@ -3115,13 +3146,18 @@ const requestedLocalSpeed = omega * localRadius; const localTangentX = -Math.sin(local.angle) * local.direction; const localTangentY = Math.cos(local.angle) * local.direction; - const b1 = galaxyRelativeSpeedBudget(parentTarget, absoluteSpeedLimit, - requestedLocalSpeed, localTangentX, localTangentY); + /* A nested moon is already inside the carrier's local frame. Applying the world cap a + second time against its planet leaves no tangent whenever that planet is near the + emergency ceiling, which makes only the deepest authored orbit appear frozen. The + carrier frame is capped below; preserve the differential moon velocity here. */ + const localSpeedLimit = nestedParent ? Number.POSITIVE_INFINITY : strictSpeedLimit; + const b1 = nestedParent ? requestedLocalSpeed : galaxyRelativeSpeedBudget( + parentTarget, localSpeedLimit, requestedLocalSpeed, localTangentX, localTangentY); const nextAngle = local.angle + local.direction * (b1 / Math.max(1e-9, localRadius)) * timestep; const nextTanX = -Math.sin(nextAngle) * local.direction; const nextTanY = Math.cos(nextAngle) * local.direction; - const phaseSpeed = Math.min(b1, galaxyRelativeSpeedBudget(parentTarget, absoluteSpeedLimit, - b1, nextTanX, nextTanY)); + const phaseSpeed = nestedParent ? requestedLocalSpeed : Math.min( + b1, galaxyRelativeSpeedBudget(parentTarget, localSpeedLimit, b1, nextTanX, nextTanY)); const cappedOmega = phaseSpeed / Math.max(1e-9, localRadius); local.angle += local.direction * cappedOmega * timestep; const offsetX = Math.cos(local.angle) * localRadius; @@ -3178,6 +3214,7 @@ const timestep = Math.max(0.001, Math.min(2, Number(opts.timestep) || 1)); const orbitalRadius = galaxyOrbitalRadiusMultiplier(opts.orbitalSpeed); const absoluteSpeedLimit = Math.max(0.01, Number(opts.speedLimit) || MAX_NODE_SPEED); + const strictSpeedLimit = Math.max(0.01, absoluteSpeedLimit - 1e-6); const direction = (seededHash(opts.layoutSeed, 'galaxy-spin') & 1) ? 1 : -1; const envelope = galaxyFarFieldEnvelope(bodies, opts); const nodeRadius = node => finitePositive(node.radius, @@ -3202,7 +3239,7 @@ /* The carrier is the parent frame for every local orbit. Cap it before constructing that frame, otherwise a high authored clock can make the child speed budget infeasible and scatter the local system. */ - const speed = Math.min(absoluteSpeedLimit, Math.max(0, requestedSpeed)); + const speed = Math.min(strictSpeedLimit, Math.max(0, requestedSpeed)); return speed / Math.max(1e-6, radius); }; const boundedRadius = (radius, extent) => { @@ -4387,8 +4424,15 @@ ])); systems.sort((left, right) => maximumExtents.get(right) - maximumExtents.get(left) || String(left.id).localeCompare(String(right.id))); - const coreRadius = Math.max(finitePositive(anchor.radius, - evidenceNodeRadius(anchor, 3), 160), coreEnvelope ? coreEnvelope.radius : 0); + const blackHoleBodyRadius = finitePositive(anchor.radius, + evidenceNodeRadius(anchor, 3), 160); + /* Runtime horizon projection paints the explicit global anchor at twice its body radius. + Reserve that same painted radius during lane admission, otherwise the first boundary + pass translates the innermost managed system outward and silently changes its named lane. */ + const coreRadius = Math.max(blackHoleBodyRadius, + blackHoleBodyRadius * GALAXY_BLACK_HOLE_PAINT_SCALE + + GALAXY_BLACK_HOLE_EXCLUSION_PADDING, + coreEnvelope ? coreEnvelope.radius : 0); let cursor = 0, previousLaneRadius = coreRadius, previousLaneExtent = 0, laneIndex = 0; while (cursor < systems.length) { /* Reserve the maximum nested local envelope, then keep a small independent lane margin. @@ -4409,7 +4453,7 @@ } /* Cap maximum systems per ring so systems form tiered concentric circles rather than collapsing all systems onto a single giant outer circle. */ - const maxPerRing = Math.max(3, Math.min(6, Math.floor(2 + laneIndex * 1.5))); + const maxPerRing = Math.max(3, Math.min(12, Math.floor(4 + laneIndex * 3))); const count = Math.min(capacity, maxPerRing, systems.length - cursor); const phaseOffset = seededHash(opts.layoutSeed, 'carrier-ring:' + String(laneIndex)) / 0x100000000 * Math.PI * 2; @@ -4690,6 +4734,7 @@ tangentialVelocityRemoved: 0, minimumClearance: null, }; + const correctedManagedSystems = new Set(); if (!anchor || bodies.length < 2) return stats; const padding = Math.max(0, Number.isFinite(Number(opts.padding)) ? Number(opts.padding) : GALAXY_BLACK_HOLE_EXCLUSION_PADDING); @@ -4771,6 +4816,10 @@ if (members.some(node => node.id === opts.fixedNodeId)) { members.forEach(node => { if (!projectIndividualNode(node)) return; + if (!system.core && system.carrier + && system.carrier.__galaxyCarrierLaneManaged === true) { + correctedManagedSystems.add(String(system.id)); + } if (system.core) stats.coreNodes++; else stats.fixedSystemNodes++; }); @@ -4802,6 +4851,10 @@ stats.contacts++; if (system.core) stats.coreNodes += members.length; else stats.systems++; + if (!system.core && system.carrier + && system.carrier.__galaxyCarrierLaneManaged === true) { + correctedManagedSystems.add(String(system.id)); + } stats.repelledNodes += members.length; stats.correctedDistance += correction; stats.maximumShift = Math.max(stats.maximumShift, correction); @@ -4815,6 +4868,25 @@ stats.minimumClearance = stats.minimumClearance === null ? clearance : Math.min(stats.minimumClearance, clearance); }); + /* A managed carrier lane is authoritative until the hard painted horizon proves that its + complete envelope cannot fit there. If the boundary translated that system, carry the + corrected radius back into the lane cache so the next orbit-clock pass does not pull it + inside again and re-trigger the same rigid correction every frame. */ + if (correctedManagedSystems.size) { + const radiusMultiplier = Math.max(1e-9, galaxyOrbitalRadiusMultiplier(opts.orbitalSpeed)); + galaxyBlackHoleCarrierSystems(bodies, anchor).forEach(system => { + if (system.core || !system.carrier + || system.carrier.__galaxyCarrierLaneManaged !== true + || !correctedManagedSystems.has(String(system.id))) return; + const dx = system.carrier.x - anchorX, dy = system.carrier.y - anchorY; + const radius = Math.hypot(dx, dy); + if (!(radius > 1e-9)) return; + setGalaxyKinematicPhase(system.carrier, '__galaxyCarrierLaneBaseRadius', + radius / radiusMultiplier); + setGalaxyKinematicPhase(system.carrier, '__galaxyCarrierLaneRadius', radius); + setGalaxyKinematicPhase(system.carrier, '__galaxyCarrierLaneAngle', Math.atan2(dy, dx)); + }); + } return stats; } @@ -5398,6 +5470,11 @@ let targetSpeed = core ? galaxyCarrierTargetSpeed(field, radius, opts.orbitalSpeed) : galaxyAuthoredCarrierTargetSpeed(field, radius, opts.orbitalSpeed); + const managedExternalLane = !core && carrier.__galaxyCarrierLaneManaged === true + && opts.liveGalaxyClock === true; + let phaseTargetSpeed = managedExternalLane + ? galaxyManagedCarrierTargetSpeed(field, radius, opts.orbitalSpeed, true) + : targetSpeed; if (!(radius > 1e-9) || !(targetSpeed > 0)) return; const laneRadiusKey = core ? '__galaxyCoreLaneRadius' : '__galaxyCarrierLaneRadius'; const laneAngleKey = core ? '__galaxyCoreLaneAngle' : '__galaxyCarrierLaneAngle'; @@ -5440,6 +5517,9 @@ targetSpeed = core ? galaxyCarrierTargetSpeed(field, radius, opts.orbitalSpeed) : galaxyAuthoredCarrierTargetSpeed(field, radius, opts.orbitalSpeed); + phaseTargetSpeed = managedExternalLane + ? galaxyManagedCarrierTargetSpeed(field, radius, opts.orbitalSpeed, true) + : targetSpeed; /* Admission owns the phase of every deliberately packed external ring. Systems that share one ring must advance by the same angle forever; adopting their independently perturbed force positions lets the phase gaps collapse and eventually overlaps two @@ -5447,7 +5527,7 @@ still adopt a genuine contact correction, preserving the historical drag behavior. */ const currentAngle = Math.atan2(dy, dx); const cachedAngle = Number(carrier[laneAngleKey]); - const advance = direction * targetSpeed / radius * timestep; + const advance = direction * phaseTargetSpeed / radius * timestep; const managedLane = !core && carrier.__galaxyCarrierLaneManaged === true; let angle; if (Number.isFinite(cachedAngle) && Number.isFinite(currentAngle)) { @@ -5486,10 +5566,10 @@ const tangentX = -unitY * orbitDirection, tangentY = unitX * orbitDirection; const radialSpeed = carrierVx * unitX + carrierVy * unitY; const signedTangent = carrierVx * tangentX + carrierVy * tangentY; - /* Admission assigns collision-free circular lanes. Exact circular carrier velocity keeps - every member of a shared ring at one angular frequency, so phase gaps and envelope - clearance cannot drift. This changes only the external carrier frame; local eccentric - star/planet motion remains entirely in the unchanged relative velocities. */ + /* Admission assigns collision-free circular lanes. The live dashboard may advance the + cached painted phase at its visibility floor while retaining the calibrated physical + tangent target, so phase gaps and envelope clearance remain stable without reheating + the local star/planet velocities. */ const supportedTangent = targetSpeed; const supportedRadial = 0; const deltaX = (supportedRadial - radialSpeed) * unitX @@ -5997,8 +6077,10 @@ const tangentX = -unitY, tangentY = unitX; const currentTangent = relativeVx * tangentX + relativeVy * tangentY; const sign = Math.sign(currentTangent) || direction; - const desiredTangent = galaxyCarrierTargetSpeed( - field, radius, opts.orbitalSpeed) * sign; + const managedCarrierLane = carrier.__galaxyCarrierLaneManaged === true; + const desiredTangent = (managedCarrierLane + ? galaxyManagedCarrierTargetSpeed(field, radius, opts.orbitalSpeed, true) + : galaxyCarrierTargetSpeed(field, radius, opts.orbitalSpeed)) * sign; const delta = desiredTangent - currentTangent; members.forEach(node => { if (node.id === opts.fixedNodeId) return; @@ -6104,12 +6186,16 @@ const sign = Math.sign(currentTangent) || ((seededHash(opts.layoutSeed, 'system:' + String(parent.id)) & 1) ? 1 : -1); const parentId = String(parent.id); + const nestedCarrier = parent !== localAnchor; + const timestep = Math.max(0.001, Math.min(2, Number(opts.timestep) || 1)); + const requestedRelativeSpeed = baseSpeed * GALAXY_BASE_ORBITAL_SPEED_BOOST * orbitalSpeed; let phase = node.__galaxySpeedControlPhase; + const previousPhaseMultiplier = phase && Number(phase.multiplier); if (!phase || phase.anchorId !== parentId || !Number.isFinite(Number(phase.direction))) { phase = setGalaxyKinematicPhase(node, '__galaxySpeedControlPhase', { anchorId: parentId, angle: currentAngle, direction: sign, - multiplier: orbitalSpeed, radiusMultiplier: orbitalRadius, + multiplier: orbitalSpeed, radiusMultiplier: orbitalRadius, localSpeed: null, }); } else { phase.multiplier = orbitalSpeed; @@ -6125,23 +6211,33 @@ /* The local clock owns angular phase just as the scene owns radius. Raw leapfrog, collision, and relation work may translate the whole system, but they cannot turn a planet backward or pull it onto a chord through the star. */ - const timestep = Math.max(0.001, Math.min(2, Number(opts.timestep) || 1)); - const requestedRelativeSpeed = baseSpeed * GALAXY_BASE_ORBITAL_SPEED_BOOST * orbitalSpeed; - const nestedCarrier = parent.system_anchor_id !== undefined - && parent.system_anchor_id !== null - && String(parent.system_anchor_id) !== String(parent.id); - /* A parent that owns a moon needs to leave world-speed headroom for that moon. The - final common projection below preserves both tangents, whereas clipping the moon's - local budget to a carrier already at 48 would turn its tangent exactly to zero. */ - const localAbsoluteSpeedLimit = nestedCarrier - ? Number.POSITIVE_INFINITY : absoluteSpeedLimit; + const phaseMultiplierChanged = Number.isFinite(previousPhaseMultiplier) + && Math.abs(previousPhaseMultiplier - orbitalSpeed) > 1e-9; + /* Preserve the first healthy local energy budget. A fast outer carrier can temporarily + leave only a small perpendicular world-speed budget; chasing the larger circular + target every frame then reheats the planet as the carrier rotates into a new tangent. */ + if (!(Number.isFinite(Number(phase.localSpeed)) && Number(phase.localSpeed) > 1e-5) + || phaseMultiplierChanged) { + const seededSpeed = Math.abs(currentTangent); + phase.localSpeed = nestedCarrier ? requestedRelativeSpeed : seededSpeed > 1e-5 + ? Math.min(requestedRelativeSpeed, seededSpeed) : requestedRelativeSpeed; + } + const localTargetSpeed = Math.max(0, Number(phase.localSpeed) || 0); + const ownsNestedOrbit = (childrenByAnchor.get(String(node.id)) || []).length > 0; + /* A parent that owns a moon leaves world-speed headroom for that moon. Keep the same + absolute budget for the child itself; reducing the parent lane is what prevents the + budget solver from collapsing the nested tangent to zero. */ + const nestedParentSpeedLimit = Math.max(1, absoluteSpeedLimit * 0.05); + const requestedParentSpeed = ownsNestedOrbit + ? Math.min(localTargetSpeed, nestedParentSpeedLimit) : localTargetSpeed; + const localAbsoluteSpeedLimit = absoluteSpeedLimit; const phaseTangentX = -Math.sin(phase.angle) * phase.direction; const phaseTangentY = Math.cos(phase.angle) * phase.direction; /* Use one scalar for the phase clock and emitted velocity. The final tangent rotates during the step, so apply the directional budget across both start and end tangents; this preserves full perpendicular orbital velocity without exceeding the absolute cap. */ const b1 = galaxyRelativeSpeedBudget(parent, localAbsoluteSpeedLimit, - requestedRelativeSpeed, phaseTangentX, phaseTangentY); + requestedParentSpeed, phaseTangentX, phaseTangentY); const nextAngle = phase.angle + phase.direction * (b1 / Math.max(1e-6, targetRadius)) * timestep; const nextTanX = -Math.sin(nextAngle) * phase.direction; const nextTanY = Math.cos(nextAngle) * phase.direction; @@ -9067,6 +9163,7 @@ /* Live Galaxy owns the carrier position phase even when a filtered payload skipped one-shot lane admission. Low-level helper callers retain force-only semantics unless they opt into this browser clock contract. */ + liveGalaxyClock: true, /* Space friction must be a real control in Galaxy mode, not a diagnostic-only value. The bare base (0.00005 per second) retained 99.9% of a slingshot's speed after ten seconds at damping 1 and 99.3% at damping 15 — indistinguishable on screen. The @@ -9273,11 +9370,11 @@ )); galaxyLastFrameTime = now; galaxyAccumulator = Math.min( - GALAXY_FRAME_INTERVAL_MS * 1.5, + GALAXY_FRAME_INTERVAL_MS * GALAXY_MAX_SUBSTEPS, galaxyAccumulator + elapsed ); } - const ordinarySubsteps = Math.min(1, + const ordinarySubsteps = Math.min(GALAXY_MAX_SUBSTEPS, Math.floor((galaxyAccumulator + 1e-9) / GALAXY_FRAME_INTERVAL_MS)); /* Galaxy is already live. Reheat must never add fixed slices or fast-forward time, even if a future caller accidentally leaves a stale non-zero budget in the telemetry slot. */ @@ -9310,15 +9407,20 @@ padding: GALAXY_SYSTEM_ANCHOR_EXCLUSION_PADDING, orbitalSpeed: state.settings.repel, }); + /* The lane guard may restore an authored nested radius after the first horizon + projection. Re-run the black-hole boundary last so the rendered frame cannot + place that repaired lane inside the painted event horizon. */ + applyGalaxyBlackHoleExclusion( + data.nodes || [], galaxyIntegratorOptions()); + const integratorSpeedCapped = report.speedCapped; const finalSpeed = enforceGalaxyGlobalSpeedLimit(data.nodes || [], { fixedNodeId: activeDragNode ? activeDragNode.id : null, limit: MAX_NODE_SPEED, }); - /* The live orbit clock is the final velocity authority. Report the post-clock - invariant, not the intermediate leapfrog projection that it intentionally repairs. */ + /* Preserve both stages: an integrator cap and a post-clock emergency cap are real + activations even though the final velocity is safely below the world ceiling. */ report.maximumSpeed = finalSpeed.maximumAfter; - report.speedCapped = finalSpeed.maximumAfter > MAX_NODE_SPEED - + SPEED_LIMIT_DIAGNOSTIC_EPSILON; + report.speedCapped = integratorSpeedCapped || finalSpeed.applied; } galaxySteps++; if (kinematicFallback) { diff --git a/tests/e2e/graph-engine.spec.js b/tests/e2e/graph-engine.spec.js index 66f865f7..0741cb33 100644 --- a/tests/e2e/graph-engine.spec.js +++ b/tests/e2e/graph-engine.spec.js @@ -1861,7 +1861,7 @@ for (const reducedMotion of [false, true]) { expect(diagnostics.linkSetting).toBe(8); expect(diagnostics.relationOrbitScale).toBeCloseTo(0.25, 12); expect(diagnostics.gravitySetting).toBe(120); - expect(diagnostics.blackHoleGravity).toBeCloseTo(4624.615384615385, 12); + expect(diagnostics.blackHoleGravity).toBeCloseTo(4634.584615384615, 12); expect(diagnostics.localGravity).toBeCloseTo(240, 12); expect(diagnostics.systemOrbitSeedSpeedLimit).toBeCloseTo(23.4, 12); From fc9155a96f27e225934c9acb1faf3aa2f936ba39 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Mon, 7 Sep 2026 04:33:49 -0400 Subject: [PATCH 06/21] fix(release): prepare Engraphis 1.7.3 --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- .claude-plugin/skill-assets.sha256 | 4 +- CHANGELOG.md | 22 ++++- engraphis/__init__.py | 4 +- engraphis/commercial_manifest.json | 2 +- engraphis/dashboard_assets/engraphis-graph.js | 27 +++++- pyproject.toml | 2 +- tests/test_graph_engine_asset.py | 85 +++++++++++++++++++ 9 files changed, 137 insertions(+), 13 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index cda9a0a0..7f67a2c6 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ "name": "engraphis-memory", "source": "./", "description": "Discipline for giving agents durable, scoped, explainable memory across sessions and repos with the Engraphis MCP tools.", - "version": "1.7.2" + "version": "1.7.3" } ] } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 9b039cca..11115ed7 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "engraphis-memory", - "version": "1.7.2", + "version": "1.7.3", "description": "Give agents durable, scoped, explainable memory across sessions and repos via the Engraphis MCP tools. Use when you learn something worth keeping, need prior context before acting, or ask why/how a fact changed. Covers remember/recall, why/timeline, forget/pin/correct, sessions, and code search.", "author": { "name": "The Engraphis Authors", diff --git a/.claude-plugin/skill-assets.sha256 b/.claude-plugin/skill-assets.sha256 index 10cec3d6..bb3cda87 100644 --- a/.claude-plugin/skill-assets.sha256 +++ b/.claude-plugin/skill-assets.sha256 @@ -1,5 +1,5 @@ -595a395f048219beb8dd741443b17d889e725effa91bf3f20578bbe497f5d55e .claude-plugin/marketplace.json -8294e6c2de066bd7dee01251cea42d301904cbf94a17cf8f884b26f0a67f7dc0 .claude-plugin/plugin.json +1731b35c796e5ac26d7e4c7c9b77b2640d6e7478006d2c67a155f66df70bb50a .claude-plugin/marketplace.json +4b2453788ff35f0c46eb8d0cd4c2491ca9352fc4bae96ee81a4c330f2709ac77 .claude-plugin/plugin.json 4bc8979b9ffeb97190960e551dbf4ddc6f7aeeb7b86894fd2298a59ff0001efa skills/engraphis-memory/SKILL.md 055655db84af07561d002f0c69744313d8413c39f3e873f941f0fa0b1e76dc66 skills/engraphis-memory/references/CONVENTIONS.md 62019760766ff472a76a0f81437898f39e3c1fe2631732b7b7733e50c1ad837f skills/engraphis-memory/references/SCOPING.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 42babbe4..637d9d99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,25 @@ # Changelog All notable changes to Engraphis are documented here. Format loosely follows -[Keep a Changelog](https://keepachangelog.com/); versions use SemVer. - -## [1.7.2] - 2026-09-05 +[Keep a Changelog](https://keepachangelog.com/); versions use SemVer. + +## [1.7.3] - 2026-09-07 + +### Fixed + +- Preserved Galaxy carrier lane and kinematic orbit invariants through central-field slider + changes, including the global and core cached radii used by the next fixed slice. +- Refreshed the retained local orbital speed budget when the effective local-gravity control + changes, preventing a stale phase cache from masking the slider. +- Kept high-density Galaxy layouts inside the strict speed cap while maintaining authored + carrier and nested local orbit phase. + +### Tests + +- Added deterministic regressions for central-field cache scaling and local-gravity phase + invalidation, alongside the existing 500-body and browser accessibility coverage. + +## [1.7.2] - 2026-09-05 ### Added diff --git a/engraphis/__init__.py b/engraphis/__init__.py index f6f09ab1..d01cf0b7 100644 --- a/engraphis/__init__.py +++ b/engraphis/__init__.py @@ -2,7 +2,7 @@ from importlib.metadata import PackageNotFoundError, version as _dist_version -_SOURCE_VERSION = "1.7.2" +_SOURCE_VERSION = "1.7.3" try: __version__ = _dist_version("engraphis") @@ -14,7 +14,7 @@ except PackageNotFoundError: # source tree without an installed distribution # Keep in step with [project] version in pyproject.toml — tests/test_packaging.py # pins the two together so a release cannot ship them out of sync. - __version__ = "1.7.2" + __version__ = "1.7.3" def _default_memory_engine_factory(**kwargs): diff --git a/engraphis/commercial_manifest.json b/engraphis/commercial_manifest.json index e5545230..261a1224 100644 --- a/engraphis/commercial_manifest.json +++ b/engraphis/commercial_manifest.json @@ -1,6 +1,6 @@ { "schema": "engraphis-commercial/v2", - "version": "1.7.2", + "version": "1.7.3", "control_plane": "https://api.engraphis.com", "account_portal": "https://api.engraphis.com/account", "billing": { diff --git a/engraphis/dashboard_assets/engraphis-graph.js b/engraphis/dashboard_assets/engraphis-graph.js index d9a1e15d..69947354 100644 --- a/engraphis/dashboard_assets/engraphis-graph.js +++ b/engraphis/dashboard_assets/engraphis-graph.js @@ -6191,15 +6191,18 @@ const requestedRelativeSpeed = baseSpeed * GALAXY_BASE_ORBITAL_SPEED_BOOST * orbitalSpeed; let phase = node.__galaxySpeedControlPhase; const previousPhaseMultiplier = phase && Number(phase.multiplier); + const previousPhaseLocalGravityMultiplier = phase && Number(phase.localGravityMultiplier); if (!phase || phase.anchorId !== parentId || !Number.isFinite(Number(phase.direction))) { phase = setGalaxyKinematicPhase(node, '__galaxySpeedControlPhase', { anchorId: parentId, angle: currentAngle, direction: sign, - multiplier: orbitalSpeed, radiusMultiplier: orbitalRadius, localSpeed: null, + multiplier: orbitalSpeed, radiusMultiplier: orbitalRadius, + localGravityMultiplier, localSpeed: null, }); } else { phase.multiplier = orbitalSpeed; phase.radiusMultiplier = orbitalRadius; + phase.localGravityMultiplier = localGravityMultiplier; } /* Pointer ownership is the one temporary exception to exact lane projection. Let the existing bounded drag field pull followers instead of copying the star's pointer @@ -6213,11 +6216,17 @@ a planet backward or pull it onto a chord through the star. */ const phaseMultiplierChanged = Number.isFinite(previousPhaseMultiplier) && Math.abs(previousPhaseMultiplier - orbitalSpeed) > 1e-9; + /* A local-gravity slider change changes the requested circular speed, but leaves the + orbital-speed multiplier untouched. Treat the effective field multiplier as part of + the phase cache key so the retained local speed cannot mask the new control value. */ + const phaseLocalGravityChanged = Boolean(phase && ( + !Number.isFinite(previousPhaseLocalGravityMultiplier) + || Math.abs(previousPhaseLocalGravityMultiplier - localGravityMultiplier) > 1e-9)); /* Preserve the first healthy local energy budget. A fast outer carrier can temporarily leave only a small perpendicular world-speed budget; chasing the larger circular target every frame then reheats the planet as the carrier rotates into a new tangent. */ if (!(Number.isFinite(Number(phase.localSpeed)) && Number(phase.localSpeed) > 1e-5) - || phaseMultiplierChanged) { + || phaseMultiplierChanged || phaseLocalGravityChanged) { const seededSpeed = Math.abs(currentTangent); phase.localSpeed = nestedCarrier ? requestedRelativeSpeed : seededSpeed > 1e-5 ? Math.min(requestedRelativeSpeed, seededSpeed) : requestedRelativeSpeed; @@ -10699,6 +10708,20 @@ node[key] = target * ratio; } }); + /* The kinematic clock owns the next carrier position. Keep its cached radial + state in the same field response as the painted lane; otherwise the next + fixed slice replays the pre-slider radius and snaps the system back. */ + ['__galaxyKinematicGlobalOrbit', '__galaxyKinematicCoreOrbit'] + .forEach(cacheKey => { + const orbit = node[cacheKey]; + if (!orbit || typeof orbit !== 'object') return; + ['baseRadius', 'radius'].forEach(key => { + const cachedRadius = Number(orbit[key]); + if (Number.isFinite(cachedRadius) && cachedRadius > 0) { + orbit[key] = cachedRadius * ratio; + } + }); + }); }); moved++; }); diff --git a/pyproject.toml b/pyproject.toml index 8e58f8fb..57c4e3d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ build-backend = "setuptools.build_meta" [project] name = "engraphis" -version = "1.7.2" +version = "1.7.3" description = "Local-first AI memory engine for agents — Ebbinghaus decay, interaction-aware recall, bi-temporal facts, hybrid retrieval, and an MCP server. You bring the LLM." readme = "README.md" license = "Apache-2.0" diff --git a/tests/test_graph_engine_asset.py b/tests/test_graph_engine_asset.py index 641f2694..930835ec 100644 --- a/tests/test_graph_engine_asset.py +++ b/tests/test_graph_engine_asset.py @@ -10998,6 +10998,54 @@ def test_slider_burst_reasserts_contact_invariant_when_galaxy_is_frozen() -> Non assert report["finite"] is True +@requires_node +def test_central_slider_scales_the_cached_global_kinematic_radius() -> None: + """A central-field slider move must update the carrier clock as well as painted lanes.""" + report = _run_engine( + """ + const api = G.create(el, { reducedMotion: () => false }); + api.setPreset('galaxy'); + api.setData({ + nodes: [ + { id: 'black-hole', anchor_role: 'global', community_id: 'core', + system_anchor_id: 'black-hole', gravity_mass: 64, visual_radius: 8, + x: 0, y: 0, vx: 0, vy: 0 }, + { id: 'star', anchor_role: 'community', community_id: 'outer', + system_anchor_id: 'star', gravity_mass: 8, visual_radius: 5, + x: 120, y: 0, vx: 0, vy: 0 }, + { id: 'planet', community_id: 'outer', system_anchor_id: 'star', + orbit_tier: 1, gravity_mass: 1, visual_radius: 3, + x: 150, y: 0, vx: 0, vy: 0 }, + ], + edges: [{ source: 'star', target: 'planet', layer: 'entity' }], + }); + const nodes = store.graphData.nodes; + const star = nodes.find(node => node.id === 'star'); + I.advanceGalaxyKinematicOrbits(nodes, { + gravity: 48, gravitationalConstant: 1, blackHoleMass: 1, + softening: 32, centralSoftening: 40, localSoftening: 12, + orbitalSpeed: 100, layoutSeed: 19, timestep: .032, + }); + const before = star.__galaxyKinematicGlobalOrbit; + const beforeBaseRadius = before.baseRadius; + const beforeRadius = before.radius; + const expectedRatio = I.galaxyImmediateGravityRadiusScale(48, { + gravitationalConstant: 2, blackHoleMass: 1, + }) / I.galaxyImmediateGravityRadiusScale(48, { + gravitationalConstant: 1, blackHoleMass: 1, + }); + api.setSettings({ gravitationalConstant: 2 }); + const after = star.__galaxyKinematicGlobalOrbit; + emit({ expectedRatio, baseRatio: after.baseRadius / beforeBaseRadius, + radiusRatio: after.radius / beforeRadius, + finite: [after.baseRadius, after.radius].every(Number.isFinite) }); + """ + ) + assert report["finite"] is True + assert report["baseRatio"] == pytest.approx(report["expectedRatio"], rel=1e-9), report + assert report["radiusRatio"] == pytest.approx(report["expectedRatio"], rel=1e-9), report + + @requires_node def test_full_graph_within_the_force_budget_keeps_centre_gravity_live() -> None: @@ -12065,6 +12113,43 @@ def test_live_orbit_phase_uses_the_budgeted_relative_speed() -> None: assert report["phaseSpeed"] == pytest.approx(report["relativeSpeed"], rel=1e-9), report +@requires_node +def test_live_orbit_phase_refreshes_when_local_gravity_changes() -> None: + """A local-gravity slider move must invalidate the retained local speed budget.""" + report = _run_node( + """ + const nodes = [ + { id: 'black-hole', anchor_role: 'global', community_id: 'core', + system_anchor_id: 'black-hole', gravity_mass: 16, radius: 8, + x: 0, y: 0, vx: 0, vy: 0 }, + { id: 'star', anchor_role: 'community', community_id: 'solar', + system_anchor_id: 'star', gravity_mass: 6, radius: 5, + x: 120, y: 0, vx: 0, vy: 0 }, + { id: 'planet', community_id: 'solar', system_anchor_id: 'star', + orbit_tier: 1, gravity_mass: 1, radius: 2, + x: 150, y: 0, vx: 0, vy: 0 }, + ]; + const options = { + gravity: 48, softening: 32, centralSoftening: 40, + localGravitySetting: 48, localGravitationalConstant: 1, + orbitalSpeed: 400, layoutSeed: 19, timestep: 1, speedLimit: 48, + }; + I.applyGalaxyOrbitalSpeedControl(nodes, options); + const first = nodes[2].__galaxySpeedControlPhase; + const firstSpeed = first.localSpeed; + I.applyGalaxyOrbitalSpeedControl(nodes, { + ...options, localGravitationalConstant: 4, + }); + const second = nodes[2].__galaxySpeedControlPhase; + emit({ firstSpeed, secondSpeed: second.localSpeed, + cachedGravityMultiplier: second.localGravityMultiplier, + changed: Math.abs(second.localSpeed - firstSpeed) > 1e-6 }); + """ + ) + assert report["cachedGravityMultiplier"] == pytest.approx(4) + assert report["changed"] is True, report + + @requires_node def test_kinematic_carrier_is_capped_before_local_motion_budgeting() -> None: report = _run_node( From 1d29a7bc8d3556e8be006c5b1a629933d822f0f3 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Mon, 7 Sep 2026 04:56:49 -0400 Subject: [PATCH 07/21] fix(graph): cap nested kinematic orbit speeds --- engraphis/dashboard_assets/engraphis-graph.js | 38 +++++++++++++------ tests/test_graph_engine_asset.py | 37 ++++++++++++++++++ 2 files changed, 64 insertions(+), 11 deletions(-) diff --git a/engraphis/dashboard_assets/engraphis-graph.js b/engraphis/dashboard_assets/engraphis-graph.js index 69947354..a04da832 100644 --- a/engraphis/dashboard_assets/engraphis-graph.js +++ b/engraphis/dashboard_assets/engraphis-graph.js @@ -3089,6 +3089,15 @@ const nodeRadius = node => finitePositive(node.radius, finitePositive(node.visual_radius, 3, 160), 160); const byId = new Map((members || []).map(node => [String(node.id), node])); + const childrenByAnchor = new Map(); + (members || []).forEach(candidate => { + if (!candidate || candidate === carrier) return; + const parent = galaxyLocalOrbitParent(candidate, members, carrier, byId); + if (!parent || parent === candidate) return; + const parentId = String(parent.id); + if (!childrenByAnchor.has(parentId)) childrenByAnchor.set(parentId, []); + childrenByAnchor.get(parentId).push(candidate); + }); const targets = new Map([[carrier, carrierTarget]]); const visiting = new Set(); let satellites = 0; @@ -3100,7 +3109,6 @@ visiting.add(node); const parent = galaxyLocalOrbitParent(node, members, carrier, byId) || carrier; const parentTarget = visit(parent); - const nestedParent = parent !== carrier; const parentId = String(parent.id); const parentX = Number.isFinite(parent.x) ? parent.x : 0; const parentY = Number.isFinite(parent.y) ? parent.y : 0; @@ -3146,18 +3154,21 @@ const requestedLocalSpeed = omega * localRadius; const localTangentX = -Math.sin(local.angle) * local.direction; const localTangentY = Math.cos(local.angle) * local.direction; - /* A nested moon is already inside the carrier's local frame. Applying the world cap a - second time against its planet leaves no tangent whenever that planet is near the - emergency ceiling, which makes only the deepest authored orbit appear frozen. The - carrier frame is capped below; preserve the differential moon velocity here. */ - const localSpeedLimit = nestedParent ? Number.POSITIVE_INFINITY : strictSpeedLimit; - const b1 = nestedParent ? requestedLocalSpeed : galaxyRelativeSpeedBudget( - parentTarget, localSpeedLimit, requestedLocalSpeed, localTangentX, localTangentY); + /* Every nested target is a world-space sum of its parent frame and a local tangent. Keep + a small headroom for a node that owns descendants, then solve the same vector budget at + every hierarchy depth. The final kinematic cap below remains a defensive invariant for + floating-point closure and any future target source. */ + const ownsNestedOrbit = (childrenByAnchor.get(String(node.id)) || []).length > 0; + const nestedParentSpeedLimit = Math.max(1, strictSpeedLimit * 0.05); + const requestedSpeed = ownsNestedOrbit + ? Math.min(requestedLocalSpeed, nestedParentSpeedLimit) : requestedLocalSpeed; + const b1 = galaxyRelativeSpeedBudget( + parentTarget, strictSpeedLimit, requestedSpeed, localTangentX, localTangentY); const nextAngle = local.angle + local.direction * (b1 / Math.max(1e-9, localRadius)) * timestep; const nextTanX = -Math.sin(nextAngle) * local.direction; const nextTanY = Math.cos(nextAngle) * local.direction; - const phaseSpeed = nestedParent ? requestedLocalSpeed : Math.min( - b1, galaxyRelativeSpeedBudget(parentTarget, localSpeedLimit, b1, nextTanX, nextTanY)); + const phaseSpeed = Math.min(b1, galaxyRelativeSpeedBudget( + parentTarget, strictSpeedLimit, b1, nextTanX, nextTanY)); const cappedOmega = phaseSpeed / Math.max(1e-9, localRadius); local.angle += local.direction * cappedOmega * timestep; const offsetX = Math.cos(local.angle) * localRadius; @@ -3331,8 +3342,13 @@ : { systems: 0, overlaps: 0, adjustedSystems: 0, remainingOverlaps: 0, infeasiblePairs: 0, gap: 0 }; const blackHoleSpinAngle = advanceGalaxyBlackHoleSpin(nodes, opts); + const finalSpeed = enforceGalaxyGlobalSpeedLimit(bodies, { + fixedNodeId: opts.fixedNodeId, + limit: absoluteSpeedLimit, + }); return { bodies: bodies.length, systems, satellites, systemPacking, - blackHoleSpinAngle, ghostOrbit: integrateGalaxyGhostOrbits(nodes, opts) }; + blackHoleSpinAngle, ghostOrbit: integrateGalaxyGhostOrbits(nodes, opts), + maximumSpeed: finalSpeed.maximumAfter, speedCapped: finalSpeed.applied }; } function recenterGalaxyOnAnchor(nodes) { diff --git a/tests/test_graph_engine_asset.py b/tests/test_graph_engine_asset.py index 930835ec..75b52994 100644 --- a/tests/test_graph_engine_asset.py +++ b/tests/test_graph_engine_asset.py @@ -12180,6 +12180,43 @@ def test_kinematic_carrier_is_capped_before_local_motion_budgeting() -> None: assert report["localSpeed"] <= 48 + 1e-9 +@requires_node +def test_kinematic_nested_orbits_respect_the_world_speed_limit() -> None: + report = _run_node( + """ + const nodes = [ + { id: 'black-hole', anchor_role: 'global', community_id: 'core', + system_anchor_id: 'black-hole', gravity_mass: 8, radius: 8, + x: 0, y: 0, vx: 0, vy: 0 }, + { id: 'star', anchor_role: 'community', community_id: 'solar', + system_anchor_id: 'star', gravity_mass: 4, radius: 5, + x: 120, y: 0, vx: 0, vy: 0 }, + { id: 'planet', community_id: 'solar', system_anchor_id: 'star', + orbit_tier: 1, gravity_mass: 1, radius: 2, + x: 150, y: 0, vx: 0, vy: 0 }, + { id: 'moon', community_id: 'solar', system_anchor_id: 'planet', + orbit_tier: 2, gravity_mass: .5, radius: 1, + x: 154, y: 0, vx: 0, vy: 0 }, + ]; + const options = { + gravity: 48, softening: 32, centralSoftening: 40, localSoftening: 12, + orbitalSpeed: 400, layoutSeed: 19, timestep: .032, speedLimit: 48, + }; + I.advanceGalaxyKinematicOrbits(nodes, options); + emit({ maximumSpeed: Math.max(...nodes.map(node => Math.hypot(node.vx, node.vy))), + moonSpeed: Math.hypot(nodes[3].vx, nodes[3].vy), + moonRelativeSpeed: Math.hypot(nodes[3].vx - nodes[2].vx, + nodes[3].vy - nodes[2].vy), + finite: nodes.every(node => + [node.x, node.y, node.vx, node.vy].every(Number.isFinite)) }); + """ + ) + assert report["finite"] is True + assert report["maximumSpeed"] <= 48 + 1e-9, report + assert report["moonSpeed"] <= 48 + 1e-9, report + assert report["moonRelativeSpeed"] > 0, report + + @requires_node def test_kinematic_local_velocity_budget_uses_one_phase_speed() -> None: report = _run_node( From 3a30df2d5324b9a454c4b400ad82aae6cdfda3eb Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Mon, 7 Sep 2026 05:08:26 -0400 Subject: [PATCH 08/21] test(browser): normalize orbit movement by fixed steps --- tests/e2e/graph-engine.spec.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/e2e/graph-engine.spec.js b/tests/e2e/graph-engine.spec.js index 0741cb33..55ce606a 100644 --- a/tests/e2e/graph-engine.spec.js +++ b/tests/e2e/graph-engine.spec.js @@ -1743,6 +1743,7 @@ for (const reducedMotion of [false, true]) { localAngle: angleDelta(samples[index].local.angle, sample.local.angle), screenAngle: angleDelta(samples[index].screenLocal.angle, sample.screenLocal.angle), globalAngle: angleDelta(samples[index].globalAngle, sample.globalAngle), + stepDelta: Math.max(1, sample.diagnostics.steps - samples[index].diagnostics.steps), radiusChange: Math.abs(sample.local.radius - samples[index].local.radius) / Math.max(1e-9, samples[index].local.radius), systemCenterChord: Math.hypot( @@ -1785,6 +1786,8 @@ for (const reducedMotion of [false, true]) { phaseReversals, localStepMagnitudes, localStepMean, relativeKinetics, maximumRadiusChange: Math.max(...segments.map(segment => segment.radiusChange)), maximumSystemCenterChord: Math.max(...segments.map(segment => segment.systemCenterChord)), + maximumSystemCenterStepDistance: Math.max(...segments.map(segment => + segment.systemCenterChord / segment.stepDelta)), }; await testInfo.attach(`visible-stellar-orbit-${reducedMotion ? 'reduced' : 'normal'}.json`, { body: Buffer.from(JSON.stringify(evidence, null, 2)), @@ -1830,7 +1833,12 @@ for (const reducedMotion of [false, true]) { expect(evidence.maximumRadiusChange, JSON.stringify(evidence)).toBeLessThan(0.04); expect(Math.max(...relativeKinetics), JSON.stringify(evidence)) .toBeLessThan(Math.min(...relativeKinetics) * 2); - expect(evidence.maximumSystemCenterChord, JSON.stringify(evidence)).toBeLessThan(20); + /* The fixed-step wait can observe several extra slices when Playwright polls a busy + runner. Normalize the aggregate center chord by the actual step delta so this remains a + world-space movement guard instead of a scheduler-timing guard. A 2-unit step bound is + still above the 48 * 0.032 emergency-speed displacement and rejects teleportation. */ + expect(evidence.maximumSystemCenterStepDistance, JSON.stringify(evidence)) + .toBeLessThan(2); expect(Math.max(...samples.map(sample => sample.star.warp)), JSON.stringify(evidence)) .toBeLessThan(0.01); /* Six and a half seconds is sampled on a real wall-clock server, so OS scheduling changes From 3076de07c8cac2ce004246b7ce93616a79bdb85f Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Mon, 7 Sep 2026 05:19:11 -0400 Subject: [PATCH 09/21] fix(graph): honor local gravity speed increases --- engraphis/dashboard_assets/engraphis-graph.js | 8 ++++++-- tests/test_graph_engine_asset.py | 6 ++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/engraphis/dashboard_assets/engraphis-graph.js b/engraphis/dashboard_assets/engraphis-graph.js index a04da832..b316c529 100644 --- a/engraphis/dashboard_assets/engraphis-graph.js +++ b/engraphis/dashboard_assets/engraphis-graph.js @@ -6244,8 +6244,12 @@ if (!(Number.isFinite(Number(phase.localSpeed)) && Number(phase.localSpeed) > 1e-5) || phaseMultiplierChanged || phaseLocalGravityChanged) { const seededSpeed = Math.abs(currentTangent); - phase.localSpeed = nestedCarrier ? requestedRelativeSpeed : seededSpeed > 1e-5 - ? Math.min(requestedRelativeSpeed, seededSpeed) : requestedRelativeSpeed; + /* An explicit local-gravity edit is a new field, not a transient reheat. Adopt its + requested circular speed first; the directional world-speed budget below performs + the only necessary cap against the moving carrier. */ + phase.localSpeed = nestedCarrier || phaseLocalGravityChanged + ? requestedRelativeSpeed : seededSpeed > 1e-5 + ? Math.min(requestedRelativeSpeed, seededSpeed) : requestedRelativeSpeed; } const localTargetSpeed = Math.max(0, Number(phase.localSpeed) || 0); const ownsNestedOrbit = (childrenByAnchor.get(String(node.id)) || []).length > 0; diff --git a/tests/test_graph_engine_asset.py b/tests/test_graph_engine_asset.py index 75b52994..d506ab14 100644 --- a/tests/test_graph_engine_asset.py +++ b/tests/test_graph_engine_asset.py @@ -12142,12 +12142,14 @@ def test_live_orbit_phase_refreshes_when_local_gravity_changes() -> None: }); const second = nodes[2].__galaxySpeedControlPhase; emit({ firstSpeed, secondSpeed: second.localSpeed, - cachedGravityMultiplier: second.localGravityMultiplier, - changed: Math.abs(second.localSpeed - firstSpeed) > 1e-6 }); + cachedGravityMultiplier: second.localGravityMultiplier, + changed: Math.abs(second.localSpeed - firstSpeed) > 1e-6, + increased: second.localSpeed > firstSpeed + 1e-6 }); """ ) assert report["cachedGravityMultiplier"] == pytest.approx(4) assert report["changed"] is True, report + assert report["increased"] is True, report @requires_node From 176448cce94076332fbdbbfeb81ab6e5539a7467 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Mon, 7 Sep 2026 05:32:44 -0400 Subject: [PATCH 10/21] fix(graph): preserve stable initial orbit phase --- engraphis/dashboard_assets/engraphis-graph.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/engraphis/dashboard_assets/engraphis-graph.js b/engraphis/dashboard_assets/engraphis-graph.js index b316c529..97c6166f 100644 --- a/engraphis/dashboard_assets/engraphis-graph.js +++ b/engraphis/dashboard_assets/engraphis-graph.js @@ -6206,6 +6206,7 @@ const timestep = Math.max(0.001, Math.min(2, Number(opts.timestep) || 1)); const requestedRelativeSpeed = baseSpeed * GALAXY_BASE_ORBITAL_SPEED_BOOST * orbitalSpeed; let phase = node.__galaxySpeedControlPhase; + const phaseExisted = Boolean(phase); const previousPhaseMultiplier = phase && Number(phase.multiplier); const previousPhaseLocalGravityMultiplier = phase && Number(phase.localGravityMultiplier); if (!phase || phase.anchorId !== parentId @@ -6235,7 +6236,7 @@ /* A local-gravity slider change changes the requested circular speed, but leaves the orbital-speed multiplier untouched. Treat the effective field multiplier as part of the phase cache key so the retained local speed cannot mask the new control value. */ - const phaseLocalGravityChanged = Boolean(phase && ( + const phaseLocalGravityChanged = Boolean(phaseExisted && phase && ( !Number.isFinite(previousPhaseLocalGravityMultiplier) || Math.abs(previousPhaseLocalGravityMultiplier - localGravityMultiplier) > 1e-9)); /* Preserve the first healthy local energy budget. A fast outer carrier can temporarily From a37b8824cad36ab55117a27ce8642bc0f76bf3dd Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Mon, 7 Sep 2026 05:50:25 -0400 Subject: [PATCH 11/21] fix(graph): budget nested orbit headroom dynamically --- engraphis/dashboard_assets/engraphis-graph.js | 255 ++++++++++++++---- tests/test_graph_engine_asset.py | 42 +++ 2 files changed, 250 insertions(+), 47 deletions(-) diff --git a/engraphis/dashboard_assets/engraphis-graph.js b/engraphis/dashboard_assets/engraphis-graph.js index 97c6166f..499cba14 100644 --- a/engraphis/dashboard_assets/engraphis-graph.js +++ b/engraphis/dashboard_assets/engraphis-graph.js @@ -715,6 +715,36 @@ return Math.max(0, Math.min(requestedSpeed, maximum)); } + /* Calculate the local circular-speed request once for both orbit controllers. The live path + caps the unclocked circular speed before applying its presentation multiplier; the + kinematic fallback applies the multiplier before its hard local ceiling. Keeping the two + formulas explicit preserves their calibrated contracts while letting nested-parent budget + planning use the same requested speed that the active controller will emit. */ + function galaxyLocalOrbitRequestedSpeed(parent, node, radius, options, orbitalSpeed, + softening, kinematicCap) { + const opts = options || {}; + const localRadius = Math.max(1e-9, Number(radius) || 0); + const authoredHierarchy = galaxyHasAuthoredParent(node, parent); + const localGravityMultiplier = galaxyLocalGravityMultiplier(parent, opts); + const localGravity = galaxySystemGravityConstant(parent, opts.gravity, + opts.localGravitySetting, authoredHierarchy) * localGravityMultiplier; + const localAccelerationCap = defaultGalaxySystemAccelerationCap(parent, opts.gravity, + opts.localGravitySetting, authoredHierarchy) * Math.max(0.25, localGravityMultiplier); + const anchorMass = finitePositive(parent && parent.gravity_mass, 1, 1000); + const softened = Math.max(0.1, Number(softening) || 8); + const denominator = Math.pow(localRadius * localRadius + softened * softened, 1.5); + const rawAcceleration = denominator > 0 + ? localGravity * anchorMass * localRadius / denominator : 0; + const acceleration = Math.min(localAccelerationCap, rawAcceleration); + const circularSpeed = Math.sqrt(Math.max(0, acceleration * localRadius)); + const multiplier = Math.max(0, Number(orbitalSpeed) || 0); + return kinematicCap + ? Math.min(circularSpeed * GALAXY_BASE_ORBITAL_SPEED_BOOST * multiplier, + GALAXY_LOCAL_RELATIVE_SPEED_LIMIT * multiplier) + : Math.min(GALAXY_LOCAL_RELATIVE_SPEED_LIMIT, circularSpeed) + * GALAXY_BASE_ORBITAL_SPEED_BOOST * multiplier; + } + /* The classic renderer's *dense* signal (`GPERF.dense`, `links>1500` in dashboard.js). Past it the classic path turns off the two per-edge costs that scale with the link count and buy nothing at that density: link curvature (a quadratic bezier per relation instead of a @@ -3098,6 +3128,79 @@ if (!childrenByAnchor.has(parentId)) childrenByAnchor.set(parentId, []); childrenByAnchor.get(parentId).push(candidate); }); + const requestedSpeedByNode = new Map(); + (members || []).forEach(candidate => { + if (!candidate || candidate === carrier) return; + const parent = galaxyLocalOrbitParent(candidate, members, carrier, byId) || carrier; + const parentX = Number.isFinite(parent.x) ? parent.x : 0; + const parentY = Number.isFinite(parent.y) ? parent.y : 0; + const currentRadius = Math.hypot(candidate.x - parentX, candidate.y - parentY); + const minimumRadius = nodeRadius(parent) + nodeRadius(candidate) + + GALAXY_SYSTEM_ANCHOR_EXCLUSION_PADDING; + const local = candidate[localOrbitCache]; + const cachedBaseRadius = local && local.anchorId === String(parent.id) + ? Number(local.baseRadius) : Number(candidate.__galaxyOrbitBaseRadius); + const baseRadius = Number.isFinite(cachedBaseRadius) && cachedBaseRadius > 0 + ? cachedBaseRadius : currentRadius; + const localRadius = Math.max(minimumRadius, baseRadius * orbitalRadius); + requestedSpeedByNode.set(candidate, galaxyLocalOrbitRequestedSpeed( + parent, candidate, localRadius, opts, orbitalSpeed, localSoftening, true)); + }); + const requestedPathMemo = new Map(); + const requestedPathVisiting = new Set(); + const requestedPathSpeed = node => { + if (requestedPathMemo.has(node)) return requestedPathMemo.get(node); + if (requestedPathVisiting.has(node)) return 0; + requestedPathVisiting.add(node); + const ownSpeed = Math.max(0, Number(requestedSpeedByNode.get(node)) || 0); + let pathSpeed = ownSpeed; + (childrenByAnchor.get(String(node.id)) || []).forEach(child => { + pathSpeed = Math.max(pathSpeed, ownSpeed + requestedPathSpeed(child)); + }); + requestedPathVisiting.delete(node); + requestedPathMemo.set(node, pathSpeed); + return pathSpeed; + }; + const allocatedSpeedByNode = new Map(); + const allocatedVisiting = new Set(); + const allocateSpeed = (node, inheritedScale) => { + if (!node || allocatedVisiting.has(node)) return; + allocatedVisiting.add(node); + const pathSpeed = requestedPathSpeed(node); + const pathScale = pathSpeed > strictSpeedLimit + ? strictSpeedLimit / Math.max(1e-9, pathSpeed) : 1; + const scale = Math.min(inheritedScale, pathScale); + allocatedSpeedByNode.set(node, + Math.max(0, Number(requestedSpeedByNode.get(node)) || 0) * scale); + (childrenByAnchor.get(String(node.id)) || []).forEach(child => { + allocateSpeed(child, scale); + }); + allocatedVisiting.delete(node); + }; + (members || []).forEach(node => { + if (node === carrier) return; + const parent = galaxyLocalOrbitParent(node, members, carrier, byId); + if (!parent || parent === carrier) allocateSpeed(node, 1); + }); + (members || []).forEach(node => { + if (node !== carrier && !allocatedSpeedByNode.has(node)) allocateSpeed(node, 1); + }); + const descendantSpeedMemo = new Map(); + const descendantSpeedVisiting = new Set(); + const descendantSpeedBudget = node => { + if (descendantSpeedMemo.has(node)) return descendantSpeedMemo.get(node); + if (descendantSpeedVisiting.has(node)) return 0; + descendantSpeedVisiting.add(node); + let budget = 0; + (childrenByAnchor.get(String(node.id)) || []).forEach(child => { + budget = Math.max(budget, + Math.max(0, Number(allocatedSpeedByNode.get(child)) || 0) + + descendantSpeedBudget(child)); + }); + descendantSpeedVisiting.delete(node); + descendantSpeedMemo.set(node, budget); + return budget; + }; const targets = new Map([[carrier, carrierTarget]]); const visiting = new Set(); let satellites = 0; @@ -3136,32 +3239,19 @@ } const localRadius = Math.max(minimumRadius, local.baseRadius * orbitalRadius); local.radius = localRadius; - const authoredHierarchy = galaxyHasAuthoredParent(node, parent); - const localGravityMultiplier = galaxyLocalGravityMultiplier(parent, opts); - const localGravity = galaxySystemGravityConstant(parent, opts.gravity, - opts.localGravitySetting, authoredHierarchy) - * localGravityMultiplier; - const denominator = Math.pow(localRadius * localRadius + localSoftening * localSoftening, 1.5); - const rawAcceleration = localGravity * finitePositive(parent.gravity_mass, 1, 1000) - * localRadius / Math.max(1e-9, denominator); - const acceleration = Math.min( - defaultGalaxySystemAccelerationCap(parent, opts.gravity, opts.localGravitySetting, - authoredHierarchy) - * Math.max(0.25, localGravityMultiplier), rawAcceleration); - const omega = Math.min( - Math.sqrt(Math.max(0, acceleration / localRadius)) * GALAXY_BASE_ORBITAL_SPEED_BOOST * orbitalSpeed, - GALAXY_LOCAL_RELATIVE_SPEED_LIMIT * orbitalSpeed / localRadius); - const requestedLocalSpeed = omega * localRadius; + const requestedLocalSpeed = Math.max(0, + Number(allocatedSpeedByNode.get(node)) || 0); const localTangentX = -Math.sin(local.angle) * local.direction; const localTangentY = Math.cos(local.angle) * local.direction; - /* Every nested target is a world-space sum of its parent frame and a local tangent. Keep - a small headroom for a node that owns descendants, then solve the same vector budget at - every hierarchy depth. The final kinematic cap below remains a defensive invariant for - floating-point closure and any future target source. */ - const ownsNestedOrbit = (childrenByAnchor.get(String(node.id)) || []).length > 0; - const nestedParentSpeedLimit = Math.max(1, strictSpeedLimit * 0.05); - const requestedSpeed = ownsNestedOrbit - ? Math.min(requestedLocalSpeed, nestedParentSpeedLimit) : requestedLocalSpeed; + /* Every nested target is a world-space sum of its parent frame and a local tangent. Reserve + the allocated descendant path and solve the remaining directional budget at each depth. + The final kinematic cap below remains a defensive invariant for floating-point closure + and any future target source. */ + const descendantSpeed = descendantSpeedBudget(node); + const parentSpeedBudget = galaxyRelativeSpeedBudget(parentTarget, strictSpeedLimit, + Number.POSITIVE_INFINITY, localTangentX, localTangentY); + const nestedParentSpeedLimit = Math.max(0, parentSpeedBudget - descendantSpeed); + const requestedSpeed = Math.min(requestedLocalSpeed, nestedParentSpeedLimit); const b1 = galaxyRelativeSpeedBudget( parentTarget, strictSpeedLimit, requestedSpeed, localTangentX, localTangentY); const nextAngle = local.angle + local.direction * (b1 / Math.max(1e-9, localRadius)) * timestep; @@ -6054,6 +6144,7 @@ const orbitalSpeed = galaxyOrbitalSpeedMultiplier(opts.orbitalSpeed); const absoluteSpeedLimit = Number.isFinite(Number(opts.speedLimit)) ? Math.max(0.01, Number(opts.speedLimit)) : Number.POSITIVE_INFINITY; + const strictSpeedLimit = Math.max(0.01, absoluteSpeedLimit - 1e-6); const orbitalRadius = galaxyOrbitalRadiusMultiplier(opts.orbitalSpeed); const bodies = (nodes || []).filter(node => node && !node.ghost && Number.isFinite(node.x) && Number.isFinite(node.y)); @@ -6139,6 +6230,85 @@ if (!childrenByAnchor.has(parentId)) childrenByAnchor.set(parentId, []); childrenByAnchor.get(parentId).push(candidate); }); + const requestedSpeedByNode = new Map(); + members.forEach(candidate => { + if (!candidate || candidate === localAnchor) return; + const parent = galaxyLocalOrbitParent(candidate, members, localAnchor, byId) + || localAnchor; + const dx = candidate.x - parent.x, dy = candidate.y - parent.y; + const radius = Math.hypot(dx, dy); + if (!(radius > 1e-9)) return; + const authoredRadius = Number(candidate.orbit_radius); + const cachedRadius = Number(candidate.__galaxyOrbitBaseRadius); + const baseRadius = Number.isFinite(authoredRadius) && authoredRadius > 0 + ? authoredRadius : Number.isFinite(cachedRadius) && cachedRadius > 0 + ? cachedRadius : radius; + const parentRadius = finitePositive(parent.radius, + finitePositive(parent.visual_radius, 3, 160), 160); + const candidateRadius = finitePositive(candidate.radius, + finitePositive(candidate.visual_radius, 3, 160), 160); + const minimumRadius = parentRadius + candidateRadius + + GALAXY_SYSTEM_ANCHOR_EXCLUSION_PADDING; + const targetRadius = Math.max(minimumRadius, baseRadius * orbitalRadius); + requestedSpeedByNode.set(candidate, galaxyLocalOrbitRequestedSpeed( + parent, candidate, targetRadius, opts, orbitalSpeed, + Math.max(0.1, Number(opts.softening) || 8), false)); + }); + const requestedPathMemo = new Map(); + const requestedPathVisiting = new Set(); + const requestedPathSpeed = node => { + if (requestedPathMemo.has(node)) return requestedPathMemo.get(node); + if (requestedPathVisiting.has(node)) return 0; + requestedPathVisiting.add(node); + const ownSpeed = Math.max(0, Number(requestedSpeedByNode.get(node)) || 0); + let pathSpeed = ownSpeed; + (childrenByAnchor.get(String(node.id)) || []).forEach(child => { + pathSpeed = Math.max(pathSpeed, ownSpeed + requestedPathSpeed(child)); + }); + requestedPathVisiting.delete(node); + requestedPathMemo.set(node, pathSpeed); + return pathSpeed; + }; + const allocatedSpeedByNode = new Map(); + const allocatedVisiting = new Set(); + const allocateSpeed = (node, inheritedScale) => { + if (!node || allocatedVisiting.has(node)) return; + allocatedVisiting.add(node); + const pathSpeed = requestedPathSpeed(node); + const pathScale = pathSpeed > strictSpeedLimit + ? strictSpeedLimit / Math.max(1e-9, pathSpeed) : 1; + const scale = Math.min(inheritedScale, pathScale); + allocatedSpeedByNode.set(node, + Math.max(0, Number(requestedSpeedByNode.get(node)) || 0) * scale); + (childrenByAnchor.get(String(node.id)) || []).forEach(child => { + allocateSpeed(child, scale); + }); + allocatedVisiting.delete(node); + }; + members.forEach(node => { + if (node === localAnchor) return; + const parent = galaxyLocalOrbitParent(node, members, localAnchor, byId); + if (!parent || parent === localAnchor) allocateSpeed(node, 1); + }); + members.forEach(node => { + if (node !== localAnchor && !allocatedSpeedByNode.has(node)) allocateSpeed(node, 1); + }); + const descendantSpeedMemo = new Map(); + const descendantSpeedVisiting = new Set(); + const descendantSpeedBudget = node => { + if (descendantSpeedMemo.has(node)) return descendantSpeedMemo.get(node); + if (descendantSpeedVisiting.has(node)) return 0; + descendantSpeedVisiting.add(node); + let budget = 0; + (childrenByAnchor.get(String(node.id)) || []).forEach(child => { + budget = Math.max(budget, + Math.max(0, Number(allocatedSpeedByNode.get(child)) || 0) + + descendantSpeedBudget(child)); + }); + descendantSpeedVisiting.delete(node); + descendantSpeedMemo.set(node, budget); + return budget; + }; const subtreeOf = root => { const subtree = [], seen = new Set(), pending = [root]; while (pending.length) { @@ -6177,22 +6347,10 @@ const minimumRadius = parentRadius + nodeRadius + GALAXY_SYSTEM_ANCHOR_EXCLUSION_PADDING; const targetRadius = Math.max(minimumRadius, baseRadius * orbitalRadius); - const authoredHierarchy = galaxyHasAuthoredParent(node, parent); const localGravityMultiplier = galaxyLocalGravityMultiplier(parent, opts); - const localGravity = galaxySystemGravityConstant(parent, opts.gravity, - opts.localGravitySetting, authoredHierarchy) - * localGravityMultiplier; - const localAccelerationCap = defaultGalaxySystemAccelerationCap(parent, opts.gravity, - opts.localGravitySetting, authoredHierarchy) - * Math.max(0.25, localGravityMultiplier); - const anchorMass = finitePositive(parent.gravity_mass, 1, 1000); - const denominator = Math.pow(targetRadius * targetRadius - + Math.max(0.1, Number(opts.softening) || 8) ** 2, 1.5); - const rawAcceleration = denominator > 0 - ? localGravity * anchorMass * targetRadius / denominator : 0; - const acceleration = Math.min(localAccelerationCap, rawAcceleration); - const baseSpeed = Math.min(GALAXY_LOCAL_RELATIVE_SPEED_LIMIT, - Math.sqrt(Math.max(0, acceleration * targetRadius))); + const requestedRelativeSpeed = requestedSpeedByNode.get(node) + ?? galaxyLocalOrbitRequestedSpeed(parent, node, targetRadius, opts, orbitalSpeed, + Math.max(0.1, Number(opts.softening) || 8), false); const currentAngle = Math.atan2(dy, dx); const relativeVx = (Number.isFinite(node.vx) ? node.vx : 0) - (Number.isFinite(parent.vx) ? parent.vx : 0); @@ -6204,7 +6362,6 @@ const parentId = String(parent.id); const nestedCarrier = parent !== localAnchor; const timestep = Math.max(0.001, Math.min(2, Number(opts.timestep) || 1)); - const requestedRelativeSpeed = baseSpeed * GALAXY_BASE_ORBITAL_SPEED_BOOST * orbitalSpeed; let phase = node.__galaxySpeedControlPhase; const phaseExisted = Boolean(phase); const previousPhaseMultiplier = phase && Number(phase.multiplier); @@ -6253,16 +6410,20 @@ ? Math.min(requestedRelativeSpeed, seededSpeed) : requestedRelativeSpeed; } const localTargetSpeed = Math.max(0, Number(phase.localSpeed) || 0); - const ownsNestedOrbit = (childrenByAnchor.get(String(node.id)) || []).length > 0; - /* A parent that owns a moon leaves world-speed headroom for that moon. Keep the same - absolute budget for the child itself; reducing the parent lane is what prevents the - budget solver from collapsing the nested tangent to zero. */ - const nestedParentSpeedLimit = Math.max(1, absoluteSpeedLimit * 0.05); - const requestedParentSpeed = ownsNestedOrbit - ? Math.min(localTargetSpeed, nestedParentSpeedLimit) : localTargetSpeed; const localAbsoluteSpeedLimit = absoluteSpeedLimit; const phaseTangentX = -Math.sin(phase.angle) * phase.direction; const phaseTangentY = Math.cos(phase.angle) * phase.direction; + /* Reserve only the speed actually requested by the deepest descendant path. This keeps + a parent near its natural orbit when its moon is slow, while still making room for a + fast nested chain before the directional world-speed budget is solved. */ + const descendantSpeed = descendantSpeedBudget(node); + const parentSpeedBudget = galaxyRelativeSpeedBudget(parent, strictSpeedLimit, + Number.POSITIVE_INFINITY, phaseTangentX, phaseTangentY); + const nestedParentSpeedLimit = Math.max(0, parentSpeedBudget - descendantSpeed); + const allocatedSpeed = Math.max(0, + Number(allocatedSpeedByNode.get(node)) || 0); + const requestedParentSpeed = Math.min(localTargetSpeed, allocatedSpeed, + nestedParentSpeedLimit); /* Use one scalar for the phase clock and emitted velocity. The final tangent rotates during the step, so apply the directional budget across both start and end tangents; this preserves full perpendicular orbital velocity without exceeding the absolute cap. */ diff --git a/tests/test_graph_engine_asset.py b/tests/test_graph_engine_asset.py index d506ab14..980f69c9 100644 --- a/tests/test_graph_engine_asset.py +++ b/tests/test_graph_engine_asset.py @@ -12152,6 +12152,48 @@ def test_live_orbit_phase_refreshes_when_local_gravity_changes() -> None: assert report["increased"] is True, report +@requires_node +def test_live_nested_orbit_reserves_only_the_descendant_headroom() -> None: + """Adding a moon must not collapse its planet to the former fixed 5% speed cap.""" + report = _run_node( + """ + const trial = withMoon => { + const nodes = [ + { id: 'black-hole', anchor_role: 'global', community_id: 'core', + system_anchor_id: 'black-hole', gravity_mass: 16, radius: 8, + x: 0, y: 0, vx: 0, vy: 0 }, + { id: 'star', anchor_role: 'community', community_id: 'solar', + system_anchor_id: 'star', gravity_mass: 6, radius: 5, + x: 120, y: 0, vx: 0, vy: 0 }, + { id: 'planet', community_id: 'solar', system_anchor_id: 'star', + orbit_tier: 1, orbit_radius: 30, gravity_mass: 1, radius: 2, + x: 150, y: 0, vx: 0, vy: 0 }, + ]; + if (withMoon) nodes.push({ id: 'moon', community_id: 'solar', + system_anchor_id: 'planet', orbit_tier: 2, orbit_radius: 8, + gravity_mass: .5, radius: 1, x: 158, y: 0, vx: 0, vy: 0 }); + I.applyGalaxyOrbitalSpeedControl(nodes, { + gravity: 48, softening: 32, centralSoftening: 40, + localGravitySetting: 48, orbitalSpeed: 100, layoutSeed: 19, + timestep: .032, speedLimit: 48, + }); + const star = nodes[1], planet = nodes[2], moon = nodes[3]; + return { + planetRelativeSpeed: Math.hypot(planet.vx - star.vx, planet.vy - star.vy), + moonRelativeSpeed: moon + ? Math.hypot(moon.vx - planet.vx, moon.vy - planet.vy) : null, + maximumSpeed: Math.max(...nodes.map(node => Math.hypot(node.vx, node.vy))), + }; + }; + emit({ without: trial(false), withMoon: trial(true) }); + """ + ) + assert report["withMoon"]["planetRelativeSpeed"] \ + > report["without"]["planetRelativeSpeed"] * 0.9, report + assert report["withMoon"]["moonRelativeSpeed"] > 0, report + assert report["withMoon"]["maximumSpeed"] <= 48 + 1e-9, report + + @requires_node def test_kinematic_carrier_is_capped_before_local_motion_budgeting() -> None: report = _run_node( From dc1e517cfd3f9f5657974c1a8b28abeaf743384c Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Mon, 7 Sep 2026 06:11:52 -0400 Subject: [PATCH 12/21] fix(graph): bound central endpoint and diagnostics --- CHANGELOG.md | 4 ++++ engraphis/core/graph_scene.py | 2 +- engraphis/dashboard_assets/engraphis-graph.js | 13 +++++++--- tests/graph_scene_fixture.json | 2 +- tests/test_graph_engine_asset.py | 24 +++++++++++++++++++ tests/test_graph_explorer_v2.py | 4 ++-- tests/test_graph_scene_contract.py | 4 ++-- 7 files changed, 44 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 637d9d99..6a1dc4bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ All notable changes to Engraphis are documented here. Format loosely follows changes, preventing a stale phase cache from masking the slider. - Kept high-density Galaxy layouts inside the strict speed cap while maintaining authored carrier and nested local orbit phase. +- Bounded the zero central-gravity radius response so finite far-field envelopes cannot leave + oversized kinematic carrier caches behind, and counted fallback speed-cap activations. +- Bumped the deterministic Galaxy scene algorithm identity to `galaxy-v13-responsive-compact-orbits` + so cached layouts cannot be confused with the revised placement contract. ### Tests diff --git a/engraphis/core/graph_scene.py b/engraphis/core/graph_scene.py index 674a6daf..53df9602 100644 --- a/engraphis/core/graph_scene.py +++ b/engraphis/core/graph_scene.py @@ -16,7 +16,7 @@ from typing import Any, Iterable, Mapping, Optional, Sequence -ALGORITHM_VERSION = "galaxy-v12-responsive-compact-orbits" +ALGORITHM_VERSION = "galaxy-v13-responsive-compact-orbits" PUBLIC_REFERENCE_ID_LIMIT = 200 PUBLIC_FACET_LIMIT = 100 PUBLIC_REPO_NAME_LIMIT = 100 diff --git a/engraphis/dashboard_assets/engraphis-graph.js b/engraphis/dashboard_assets/engraphis-graph.js index 499cba14..9e429243 100644 --- a/engraphis/dashboard_assets/engraphis-graph.js +++ b/engraphis/dashboard_assets/engraphis-graph.js @@ -556,8 +556,15 @@ ? Math.max(0, Number(extra.gravitationalConstant)) / 2.0 : 1.0; const mNorm = extra.blackHoleMass !== undefined && Number.isFinite(Number(extra.blackHoleMass)) ? Math.max(0, Number(extra.blackHoleMass)) / 1.0 : 1.0; - const fCentral = Math.max(0.05, gNorm * Math.sqrt(Math.max(0, mNorm))); - const rMod = Math.pow(fCentral, -0.65); + const fCentral = Math.max(0, gNorm * Math.sqrt(Math.max(0, mNorm))); + /* The central controls are applied as an immediate radius ratio, so an inverse power + with a tiny zero floor can expand a cached lane far beyond the finite far-field + envelope. Keep the loose endpoint inside the same 1.25x allowance as the global + gravity response, while retaining the calibrated inverse-power contraction above the + neutral central field. The smooth lower branch has no dead zone at zero. */ + const rMod = fCentral < 1 + ? 1.25 - 0.25 * galaxySmoothstep(fCentral) + : Math.pow(fCentral, -0.65); return baseScale * rMod; } /* Local stellar gravity follows the same inverse-radius law as the live solver. Keep its @@ -9628,6 +9635,7 @@ || galaxyLastLocalOrbitBoundary; galaxyLastOrbitalCorrection = 0; galaxyLastLocalVelocityLimits = 0; + if (report.speedCapped) galaxySpeedCaps++; } else { galaxyLastKinetic = report.kinetic; galaxyLastCollisions = report.collisions; @@ -9653,7 +9661,6 @@ galaxyLastCarrierOrbitSupport = report.carrierOrbitSupport || galaxyLastCarrierOrbitSupport; dragFollowerGravityReport = report.dragGravity; - if (report.speedCapped) galaxySpeedCaps++; } } galaxyAccumulator = Math.max(0, diff --git a/tests/graph_scene_fixture.json b/tests/graph_scene_fixture.json index c3234e07..0f5d9061 100644 --- a/tests/graph_scene_fixture.json +++ b/tests/graph_scene_fixture.json @@ -13,7 +13,7 @@ "layout_seed": 1779033703, "index_state": "ready", "filters": {}, - "algorithm_version": "galaxy-v12-responsive-compact-orbits" + "algorithm_version": "galaxy-v13-responsive-compact-orbits" }, "nodes": [ { diff --git a/tests/test_graph_engine_asset.py b/tests/test_graph_engine_asset.py index 980f69c9..2a1ef587 100644 --- a/tests/test_graph_engine_asset.py +++ b/tests/test_graph_engine_asset.py @@ -11046,6 +11046,30 @@ def test_central_slider_scales_the_cached_global_kinematic_radius() -> None: assert report["radiusRatio"] == pytest.approx(report["expectedRatio"], rel=1e-9), report +@requires_node +def test_zero_central_gravity_uses_a_bounded_cached_radius_response() -> None: + """The zero central-field endpoint must not expand kinematic lanes past the envelope.""" + report = _run_node( + """ + const neutral = I.galaxyImmediateGravityRadiusScale(48, { + gravitationalConstant: 2, blackHoleMass: 1, + }); + const zero = I.galaxyImmediateGravityRadiusScale(48, { + gravitationalConstant: 0, blackHoleMass: 1, + }); + const zeroMass = I.galaxyImmediateGravityRadiusScale(48, { + gravitationalConstant: 0, blackHoleMass: 0, + }); + emit({ neutral, zero, zeroMass, ratio: zero / neutral, + finite: [neutral, zero, zeroMass].every(Number.isFinite) }); + """ + ) + assert report["finite"] is True + assert report["ratio"] == pytest.approx(1.25, rel=1e-9) + assert report["zero"] == pytest.approx(report["zeroMass"], rel=1e-9) + assert report["ratio"] < 2.0 + + @requires_node def test_full_graph_within_the_force_budget_keeps_centre_gravity_live() -> None: diff --git a/tests/test_graph_explorer_v2.py b/tests/test_graph_explorer_v2.py index bf5d5f81..e7b1b657 100644 --- a/tests/test_graph_explorer_v2.py +++ b/tests/test_graph_explorer_v2.py @@ -1864,7 +1864,7 @@ def test_scene_hash_versions_physics_and_index_generation(): assert baseline["meta"]["scene_hash"] != stronger["meta"]["scene_hash"] assert baseline["meta"]["scene_hash"] != next_generation["meta"]["scene_hash"] - assert baseline["meta"]["algorithm_version"] == "galaxy-v12-responsive-compact-orbits" + assert baseline["meta"]["algorithm_version"] == "galaxy-v13-responsive-compact-orbits" def test_graph_scene_v7_flags_projection_repo_names_and_cache_identity(): @@ -1887,7 +1887,7 @@ def test_graph_scene_v7_flags_projection_repo_names_and_cache_identity(): workspace="acme", level="complete", include_memory_nodes=False, ) - assert baseline["meta"]["algorithm_version"] == "galaxy-v12-responsive-compact-orbits" + assert baseline["meta"]["algorithm_version"] == "galaxy-v13-responsive-compact-orbits" assert baseline["meta"]["scene_hash"] != connected["meta"]["scene_hash"] assert baseline["meta"]["filters"]["connected_only"] is False assert connected["meta"]["filters"]["connected_only"] is True diff --git a/tests/test_graph_scene_contract.py b/tests/test_graph_scene_contract.py index c615f8e5..754d6a83 100644 --- a/tests/test_graph_scene_contract.py +++ b/tests/test_graph_scene_contract.py @@ -55,7 +55,7 @@ def test_graph_scene_fixture_encodes_galaxy_invariants(): scene = _scene() nodes = {node["id"]: node for node in scene["nodes"]} communities = {community["id"]: community for community in scene["communities"]} - assert scene["meta"]["algorithm_version"] == "galaxy-v12-responsive-compact-orbits" + assert scene["meta"]["algorithm_version"] == "galaxy-v13-responsive-compact-orbits" for node in scene["nodes"]: expected_mass = 1.0 + 15.0 * node["mass_score"] ** 2 assert math.isclose(node["gravity_mass"], expected_mass, abs_tol=1e-6) @@ -167,4 +167,4 @@ def test_all_presentation_allowlist_is_closed_to_unknown_fields(): assert "unknown_future_meta" not in projected["meta"] assert set(projected["nodes"][0].keys()) <= allowed_node_keys assert set(projected["edges"][0].keys()) <= allowed_edge_keys - assert set(projected["meta"].keys()) <= allowed_meta_keys \ No newline at end of file + assert set(projected["meta"].keys()) <= allowed_meta_keys From c2f0acb65fb6e1425fd8c1e6c595dc9c30142a67 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Mon, 7 Sep 2026 11:17:44 -0400 Subject: [PATCH 13/21] fix(graph): keep live lane physics and cap diagnostics aligned --- engraphis/dashboard_assets/engraphis-graph.js | 11 +++++-- tests/test_graph_engine_asset.py | 33 +++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/engraphis/dashboard_assets/engraphis-graph.js b/engraphis/dashboard_assets/engraphis-graph.js index 9e429243..65d670a8 100644 --- a/engraphis/dashboard_assets/engraphis-graph.js +++ b/engraphis/dashboard_assets/engraphis-graph.js @@ -6192,8 +6192,12 @@ const currentTangent = relativeVx * tangentX + relativeVy * tangentY; const sign = Math.sign(currentTangent) || direction; const managedCarrierLane = carrier.__galaxyCarrierLaneManaged === true; + /* A managed lane's cached painted phase may use the far-lane visibility floor, but the + live velocity must remain the calibrated physical target. Applying that floor here + injects radius-proportional speed into large lanes and can trigger the world cap for a + tiny orbital-speed change. The kinematic phase clock owns the presentation floor. */ const desiredTangent = (managedCarrierLane - ? galaxyManagedCarrierTargetSpeed(field, radius, opts.orbitalSpeed, true) + ? galaxyManagedCarrierTargetSpeed(field, radius, opts.orbitalSpeed, false) : galaxyCarrierTargetSpeed(field, radius, opts.orbitalSpeed)) * sign; const delta = desiredTangent - currentTangent; members.forEach(node => { @@ -9635,7 +9639,6 @@ || galaxyLastLocalOrbitBoundary; galaxyLastOrbitalCorrection = 0; galaxyLastLocalVelocityLimits = 0; - if (report.speedCapped) galaxySpeedCaps++; } else { galaxyLastKinetic = report.kinetic; galaxyLastCollisions = report.collisions; @@ -9662,6 +9665,10 @@ || galaxyLastCarrierOrbitSupport; dragFollowerGravityReport = report.dragGravity; } + /* Both the live integrator and the kinematic fallback enforce the same world-speed + ceiling. Keep one counter at the shared boundary so diagnostics expose caps in + either path. */ + if (report.speedCapped) galaxySpeedCaps++; } galaxyAccumulator = Math.max(0, galaxyAccumulator - ordinarySubsteps * GALAXY_FRAME_INTERVAL_MS); diff --git a/tests/test_graph_engine_asset.py b/tests/test_graph_engine_asset.py index 2a1ef587..276a1f36 100644 --- a/tests/test_graph_engine_asset.py +++ b/tests/test_graph_engine_asset.py @@ -12104,6 +12104,39 @@ def test_every_node_worker_consumes_all_full_mode_spacetime_controls() -> None: assert all(delta > 1e-5 for delta in report["changes"].values()), report +@requires_node +def test_managed_live_carrier_uses_physical_velocity_target() -> None: + """Packed lanes may floor their painted phase, but live velocity stays physical.""" + report = _run_node( + """ + const nodes = [ + { id: 'black-hole', anchor_role: 'global', community_id: 'core', + system_anchor_id: 'black-hole', gravity_mass: 16, radius: 8, + x: 0, y: 0, vx: 0, vy: 0 }, + { id: 'outer-star', anchor_role: 'community', community_id: 'outer', + system_anchor_id: 'outer-star', gravity_mass: 6, radius: 5, + x: 500, y: 0, vx: 0, vy: 0 }, + ]; + Object.defineProperty(nodes[1], '__galaxyCarrierLaneManaged', { + value: true, writable: true, configurable: true, + }); + const options = { + gravity: 48, gravitationalConstant: 2, blackHoleMass: 1, + softening: 32, centralSoftening: 40, orbitalSpeed: 101, + layoutSeed: 19, timestep: 1, speedLimit: 48, + }; + const radius = Math.hypot(nodes[1].x, nodes[1].y); + const field = I.galaxyBlackHoleField(nodes, options); + const physical = I.galaxyAuthoredCarrierTargetSpeed(field, radius, options.orbitalSpeed); + I.applyGalaxyOrbitalSpeedControl(nodes, options); + const actual = Math.hypot(nodes[1].vx, nodes[1].vy); + emit({ actual, physical, phaseFloor: radius * 0.039 }); + """ + ) + assert report["physical"] < report["phaseFloor"], report + assert report["actual"] == pytest.approx(report["physical"], rel=1e-9), report + + @requires_node def test_live_orbit_phase_uses_the_budgeted_relative_speed() -> None: """Live phase advancement must agree with the capped velocity it emits.""" From 0f875d032bee4b49c5cafb565e5ea04188196c66 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Mon, 7 Sep 2026 22:39:11 -0400 Subject: [PATCH 14/21] fix(graph): invalidate physics snapshot after drag release --- engraphis/dashboard_assets/engraphis-graph.js | 8 ++++++-- tests/test_graph_engine_asset.py | 15 +++++++++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/engraphis/dashboard_assets/engraphis-graph.js b/engraphis/dashboard_assets/engraphis-graph.js index 65d670a8..28d4a632 100644 --- a/engraphis/dashboard_assets/engraphis-graph.js +++ b/engraphis/dashboard_assets/engraphis-graph.js @@ -9856,8 +9856,7 @@ function render(fit, reheat, dragging = false) { if (destroyed) return; - cachedPhysicsSnapshot = null; - cachedPhysicsSnapshotStep = -1; + invalidatePhysicsSnapshot(); if (suspended) { pendingRender = pendingRender ? [pendingRender[0] || fit, pendingRender[1] || reheat, pendingRender[2] || dragging] @@ -10296,6 +10295,7 @@ function finishNodeDrag(node) { if (!node || !activeDragNode || activeDragNode.id !== node.id) return; + invalidatePhysicsSnapshot(); const retainAnchor = state.settings.frozen || staticFullLayout; if (!retainAnchor) { node.fx = undefined; @@ -11161,6 +11161,10 @@ }; let cachedPhysicsSnapshot = null; let cachedPhysicsSnapshotStep = -1; + function invalidatePhysicsSnapshot() { + cachedPhysicsSnapshot = null; + cachedPhysicsSnapshotStep = -1; + } api.getPhysicsSnapshot = () => { const data = fg.graphData() || {}; const nodes = Array.isArray(data.nodes) ? data.nodes : []; diff --git a/tests/test_graph_engine_asset.py b/tests/test_graph_engine_asset.py index 276a1f36..c2d6f296 100644 --- a/tests/test_graph_engine_asset.py +++ b/tests/test_graph_engine_asset.py @@ -2466,7 +2466,12 @@ def test_advanced_spacetime_controls_pause_live_orbits_and_drag_release_is_bound report = _run_engine( """ let released = null; - const api = G.create(el, { onSlingshotRelease: value => { released = value; } }); + let callbackSnapshot = null; + let api; + api = G.create(el, { onSlingshotRelease: value => { + released = value; + callbackSnapshot = api.getPhysicsSnapshot(); + } }); api.setData({ nodes: [ { id: 'custom-heavy-center-kappa', anchor_role: 'global', community_id: 'core', gravity_mass: 32, radius: 8, x: 0, y: 0, vx: 0, vy: 0 }, @@ -2493,7 +2498,7 @@ def test_advanced_spacetime_controls_pause_live_orbits_and_drag_release_is_bound engineWindowListeners.pointermove(event(node.x + 6, node.y, 10)); engineWindowListeners.pointermove(event(node.x + 18, node.y, 34)); engineWindowListeners.pointerup(event(node.x + 18, node.y, 35)); - emit({ paused, live: api.physicsDiagnostics(), released, + emit({ paused, live: api.physicsDiagnostics(), released, callbackSnapshot, snapshot: api.getPhysicsSnapshot(), node: { vx: node.vx, vy: node.vy, fx: node.fx, fy: node.fy } }); """ ) @@ -2532,6 +2537,12 @@ def test_advanced_spacetime_controls_pause_live_orbits_and_drag_release_is_bound assert [report["node"]["vx"], report["node"]["vy"]] == pytest.approx( [report["released"]["vx"], report["released"]["vy"]] ) + assert report["callbackSnapshot"]["slingshot"] == report["released"] + callback_node = next(node for node in report["callbackSnapshot"]["nodes"] + if node["id"] == "dragged") + assert [callback_node["vx"], callback_node["vy"]] == pytest.approx( + [report["released"]["vx"], report["released"]["vy"]] + ) assert report["snapshot"]["slingshot"] == report["released"] From b0654bc61add753cdbf8eb8ac94d8793a9dd37df Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Mon, 7 Sep 2026 23:07:59 -0400 Subject: [PATCH 15/21] fix(graph): address follow-up review findings --- engraphis/core/graph_scene.py | 7 ++++++- engraphis/dashboard_assets/engraphis-graph.js | 16 ++++++++++++++-- engraphis/dashboard_assets/ledger.js | 10 ++++++---- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/engraphis/core/graph_scene.py b/engraphis/core/graph_scene.py index 53df9602..bcfbac17 100644 --- a/engraphis/core/graph_scene.py +++ b/engraphis/core/graph_scene.py @@ -813,7 +813,12 @@ def _community_positions( golden_angle = base_phase + sys_idx * GOLDEN_ANGLE_RAD angle = golden_angle + angular_jitter - nominal_r = max(core_clearance_radius, t_rad * radial_jitter) + # ``preferred_targets`` applies the user-facing compactness scale below. + # Tier radii are already physical lane coordinates, so compensate here or a + # compactness of 0.192 would shrink every tier back through the core floor and + # make the collision walk, rather than the tier plan, choose the lanes. + physical_tier_radius = max(core_clearance_radius, t_rad * radial_jitter) + nominal_r = physical_tier_radius / max(clean_radius_scale, 1e-9) specs.append({ "id": community_id, "system_radius": system_radius, diff --git a/engraphis/dashboard_assets/engraphis-graph.js b/engraphis/dashboard_assets/engraphis-graph.js index 28d4a632..6c75303e 100644 --- a/engraphis/dashboard_assets/engraphis-graph.js +++ b/engraphis/dashboard_assets/engraphis-graph.js @@ -4997,7 +4997,17 @@ setGalaxyKinematicPhase(system.carrier, '__galaxyCarrierLaneBaseRadius', radius / radiusMultiplier); setGalaxyKinematicPhase(system.carrier, '__galaxyCarrierLaneRadius', radius); - setGalaxyKinematicPhase(system.carrier, '__galaxyCarrierLaneAngle', Math.atan2(dy, dx)); + const correctedAngle = Math.atan2(dy, dx); + setGalaxyKinematicPhase(system.carrier, '__galaxyCarrierLaneAngle', correctedAngle); + /* The fallback clock owns a second radial cache. Keep it in the same corrected + coordinate space as the managed lane or the next fixed slice will replay its stale + pre-horizon radius and snap the whole system back through the painted boundary. */ + const kinematicOrbit = system.carrier.__galaxyKinematicGlobalOrbit; + if (kinematicOrbit && typeof kinematicOrbit === 'object') { + kinematicOrbit.baseRadius = radius / radiusMultiplier; + kinematicOrbit.radius = radius; + kinematicOrbit.angle = correctedAngle; + } }); } return stats; @@ -6416,7 +6426,7 @@ /* An explicit local-gravity edit is a new field, not a transient reheat. Adopt its requested circular speed first; the directional world-speed budget below performs the only necessary cap against the moving carrier. */ - phase.localSpeed = nestedCarrier || phaseLocalGravityChanged + phase.localSpeed = nestedCarrier || phaseMultiplierChanged || phaseLocalGravityChanged ? requestedRelativeSpeed : seededSpeed > 1e-5 ? Math.min(requestedRelativeSpeed, seededSpeed) : requestedRelativeSpeed; } @@ -11414,12 +11424,14 @@ api.pause = () => { if (destroyed || !running) return; running = false; + invalidatePhysicsSnapshot(); cancelGalaxyDynamics(true); if (fg.pauseAnimation) fg.pauseAnimation(); }; api.resume = () => { if (destroyed || running) return; running = true; + invalidatePhysicsSnapshot(); if (fg.resumeAnimation) fg.resumeAnimation(); measure(); scheduleGalaxyDynamics(true); diff --git a/engraphis/dashboard_assets/ledger.js b/engraphis/dashboard_assets/ledger.js index d1f2e834..69c73467 100644 --- a/engraphis/dashboard_assets/ledger.js +++ b/engraphis/dashboard_assets/ledger.js @@ -3128,8 +3128,10 @@ const savedTuning = graphPreference('tuning', {}); const savedPhysicsVersion = Number(graphPreference('physicsVersion', 0)); - const legacyPhysics = hasSavedPreferences - && (!Number.isFinite(savedPhysicsVersion) || savedPhysicsVersion < GRAPH_PHYSICS_VERSION); + const sourcePhysicsVersion = Number.isFinite(savedPhysicsVersion) ? savedPhysicsVersion : 0; + const legacyPhysics = hasSavedPreferences && sourcePhysicsVersion < GRAPH_PHYSICS_VERSION; + const needsPhysicsV3Migration = hasSavedPreferences && sourcePhysicsVersion < 3; + const needsPhysicsV4Migration = hasSavedPreferences && sourcePhysicsVersion < 4; const effectiveTuning = savedTuning && typeof savedTuning === 'object' ? { ...savedTuning } : {}; const savedSpacetimeTuning = graphPreference('spacetimeTuning', {}); @@ -3137,7 +3139,7 @@ friction at zero, and the Galaxy spacing control at 400. That exact vector is not a useful custom preset: it collapses the visible graph and can reduce hundreds of loaded entities to a small central knot. Physics v3 resets only this known-bad snapshot. */ - const staleMaxedPhysics = legacyPhysics && Number(effectiveTuning.gravity) === 400 + const staleMaxedPhysics = needsPhysicsV3Migration && Number(effectiveTuning.gravity) === 400 && Number(savedSpacetimeTuning && savedSpacetimeTuning.gravitationalConstant) === 200 && Number(savedSpacetimeTuning && savedSpacetimeTuning.blackHoleMass) === 500 && Number(savedSpacetimeTuning && savedSpacetimeTuning.localGravitationalConstant) === 200 @@ -3151,7 +3153,7 @@ /* Older preferences persisted 48 and then 60 as Galaxy's default orbital speed. Physics v4 defines the control as a percentage with 100 as neutral, so migrate only those exact retired defaults. Every other custom speed and every unrelated preference remains intact. */ - if (legacyPhysics && preset === 'galaxy' + if (needsPhysicsV4Migration && preset === 'galaxy' && [48, 60].includes(Number(effectiveTuning.repel))) { effectiveTuning.repel = 100; } From 09d59b5186f437e2bae427c12a70f3b207652ca2 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Mon, 7 Sep 2026 23:17:23 -0400 Subject: [PATCH 16/21] fix(graph): enforce painted fallback horizon --- engraphis/dashboard_assets/engraphis-graph.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/engraphis/dashboard_assets/engraphis-graph.js b/engraphis/dashboard_assets/engraphis-graph.js index 6c75303e..6ae853e0 100644 --- a/engraphis/dashboard_assets/engraphis-graph.js +++ b/engraphis/dashboard_assets/engraphis-graph.js @@ -3351,7 +3351,9 @@ return speed / Math.max(1e-6, radius); }; const boundedRadius = (radius, extent) => { - const inner = nodeRadius(anchor) + Math.max(0, extent) + const paintedAnchorRadius = nodeRadius(anchor) + * (anchor.anchor_role === 'global' ? GALAXY_BLACK_HOLE_PAINT_SCALE : 1); + const inner = paintedAnchorRadius + Math.max(0, extent) + GALAXY_BLACK_HOLE_EXCLUSION_PADDING; const outer = Math.max(inner, (Number(envelope.envelopeRadius) || inner) - Math.max(0, extent)); return Math.max(inner, Math.min(outer, radius)); @@ -11501,6 +11503,7 @@ } if (visibilityDocument && typeof visibilityDocument.addEventListener === 'function') { const handleVisibility = () => { + invalidatePhysicsSnapshot(); if (pageHidden()) cancelGalaxyDynamics(true); else scheduleGalaxyDynamics(true); }; From 3ff56c41bb1414b00ec263c66d7ac87d6766fd48 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Tue, 8 Sep 2026 02:04:34 -0400 Subject: [PATCH 17/21] fix(dashboard): align pending Galaxy motion controls --- engraphis/dashboard_assets/engraphis-graph.js | 10 +++++----- engraphis/dashboard_assets/index.html | 16 ---------------- 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/engraphis/dashboard_assets/engraphis-graph.js b/engraphis/dashboard_assets/engraphis-graph.js index 6ae853e0..373344ae 100644 --- a/engraphis/dashboard_assets/engraphis-graph.js +++ b/engraphis/dashboard_assets/engraphis-graph.js @@ -320,6 +320,7 @@ const GALAXY_ORBITAL_SPEED_MAXIMUM = 4.6; const GALAXY_ORBITAL_RADIUS_MAXIMUM = 1.06; const GALAXY_BASE_ORBITAL_SPEED_BOOST = 1.625; + const GALAXY_LIVE_ORBITAL_SPEED_BOOST = 2.6; function galaxyOrbitalSpeedMultiplier(setting) { const raw = Number(setting); const value = Number.isFinite(raw) @@ -745,11 +746,12 @@ const acceleration = Math.min(localAccelerationCap, rawAcceleration); const circularSpeed = Math.sqrt(Math.max(0, acceleration * localRadius)); const multiplier = Math.max(0, Number(orbitalSpeed) || 0); + const boost = kinematicCap ? GALAXY_BASE_ORBITAL_SPEED_BOOST : GALAXY_LIVE_ORBITAL_SPEED_BOOST; return kinematicCap - ? Math.min(circularSpeed * GALAXY_BASE_ORBITAL_SPEED_BOOST * multiplier, + ? Math.min(circularSpeed * boost * multiplier, GALAXY_LOCAL_RELATIVE_SPEED_LIMIT * multiplier) : Math.min(GALAXY_LOCAL_RELATIVE_SPEED_LIMIT, circularSpeed) - * GALAXY_BASE_ORBITAL_SPEED_BOOST * multiplier; + * boost * multiplier; } /* The classic renderer's *dense* signal (`GPERF.dense`, `links>1500` in dashboard.js). Past @@ -6428,9 +6430,7 @@ /* An explicit local-gravity edit is a new field, not a transient reheat. Adopt its requested circular speed first; the directional world-speed budget below performs the only necessary cap against the moving carrier. */ - phase.localSpeed = nestedCarrier || phaseMultiplierChanged || phaseLocalGravityChanged - ? requestedRelativeSpeed : seededSpeed > 1e-5 - ? Math.min(requestedRelativeSpeed, seededSpeed) : requestedRelativeSpeed; + phase.localSpeed = requestedRelativeSpeed; } const localTargetSpeed = Math.max(0, Number(phase.localSpeed) || 0); const localAbsoluteSpeedLimit = absoluteSpeedLimit; diff --git a/engraphis/dashboard_assets/index.html b/engraphis/dashboard_assets/index.html index 93eb319f..8327b059 100644 --- a/engraphis/dashboard_assets/index.html +++ b/engraphis/dashboard_assets/index.html @@ -406,22 +406,6 @@

Layout

-
-

Colour

-
- - - -
-
- - - - - - -
-

Motion

From cfd5935dc56ade10b30168c3546016c252d29a0a Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Tue, 8 Sep 2026 02:04:35 -0400 Subject: [PATCH 18/21] test: stabilize promoted history timestamps --- tests/test_history_scope.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_history_scope.py b/tests/test_history_scope.py index 2d76cee4..60b5f82f 100644 --- a/tests/test_history_scope.py +++ b/tests/test_history_scope.py @@ -1,4 +1,4 @@ -"""Record history retains promoted ancestors without widening caller access.""" +import time import pytest @@ -26,6 +26,7 @@ def promoted_lineage(svc): approved = svc.engine.approve_for_prompt( pending, reviewer="test-owner", reason="approved disposable fixture", )["id"] + time.sleep(0.002) promoted = svc.promote( approved, "workspace", workspace="w", repo="api", reason="shared convention", )["id"] From 08452fb64f3fec5433efc6c69cfb12c4be0e949a Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Tue, 8 Sep 2026 02:16:03 -0400 Subject: [PATCH 19/21] fix(dashboard): preserve orbit budgets and colour controls --- engraphis/dashboard_assets/engraphis-graph.js | 10 +++++----- engraphis/dashboard_assets/index.html | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/engraphis/dashboard_assets/engraphis-graph.js b/engraphis/dashboard_assets/engraphis-graph.js index 373344ae..6ae853e0 100644 --- a/engraphis/dashboard_assets/engraphis-graph.js +++ b/engraphis/dashboard_assets/engraphis-graph.js @@ -320,7 +320,6 @@ const GALAXY_ORBITAL_SPEED_MAXIMUM = 4.6; const GALAXY_ORBITAL_RADIUS_MAXIMUM = 1.06; const GALAXY_BASE_ORBITAL_SPEED_BOOST = 1.625; - const GALAXY_LIVE_ORBITAL_SPEED_BOOST = 2.6; function galaxyOrbitalSpeedMultiplier(setting) { const raw = Number(setting); const value = Number.isFinite(raw) @@ -746,12 +745,11 @@ const acceleration = Math.min(localAccelerationCap, rawAcceleration); const circularSpeed = Math.sqrt(Math.max(0, acceleration * localRadius)); const multiplier = Math.max(0, Number(orbitalSpeed) || 0); - const boost = kinematicCap ? GALAXY_BASE_ORBITAL_SPEED_BOOST : GALAXY_LIVE_ORBITAL_SPEED_BOOST; return kinematicCap - ? Math.min(circularSpeed * boost * multiplier, + ? Math.min(circularSpeed * GALAXY_BASE_ORBITAL_SPEED_BOOST * multiplier, GALAXY_LOCAL_RELATIVE_SPEED_LIMIT * multiplier) : Math.min(GALAXY_LOCAL_RELATIVE_SPEED_LIMIT, circularSpeed) - * boost * multiplier; + * GALAXY_BASE_ORBITAL_SPEED_BOOST * multiplier; } /* The classic renderer's *dense* signal (`GPERF.dense`, `links>1500` in dashboard.js). Past @@ -6430,7 +6428,9 @@ /* An explicit local-gravity edit is a new field, not a transient reheat. Adopt its requested circular speed first; the directional world-speed budget below performs the only necessary cap against the moving carrier. */ - phase.localSpeed = requestedRelativeSpeed; + phase.localSpeed = nestedCarrier || phaseMultiplierChanged || phaseLocalGravityChanged + ? requestedRelativeSpeed : seededSpeed > 1e-5 + ? Math.min(requestedRelativeSpeed, seededSpeed) : requestedRelativeSpeed; } const localTargetSpeed = Math.max(0, Number(phase.localSpeed) || 0); const localAbsoluteSpeedLimit = absoluteSpeedLimit; diff --git a/engraphis/dashboard_assets/index.html b/engraphis/dashboard_assets/index.html index 8327b059..93eb319f 100644 --- a/engraphis/dashboard_assets/index.html +++ b/engraphis/dashboard_assets/index.html @@ -406,6 +406,22 @@

Layout

+
+

Colour

+
+ + + +
+
+ + + + + + +
+

Motion

From 76ddda3c657660a0676963a21849d1edd2839201 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Tue, 8 Sep 2026 02:37:46 -0400 Subject: [PATCH 20/21] fix(dashboard): cap kinematic phase before advancing --- engraphis/dashboard_assets/engraphis-graph.js | 80 +++++++++++-------- 1 file changed, 47 insertions(+), 33 deletions(-) diff --git a/engraphis/dashboard_assets/engraphis-graph.js b/engraphis/dashboard_assets/engraphis-graph.js index 6ae853e0..070d926f 100644 --- a/engraphis/dashboard_assets/engraphis-graph.js +++ b/engraphis/dashboard_assets/engraphis-graph.js @@ -3210,7 +3210,7 @@ }; const targets = new Map([[carrier, carrierTarget]]); const visiting = new Set(); - let satellites = 0; + let satellites = 0, speedCapped = false; const visit = node => { if (!node || node === carrier) return carrierTarget; const existingTarget = targets.get(node); @@ -3251,27 +3251,44 @@ const localTangentX = -Math.sin(local.angle) * local.direction; const localTangentY = Math.cos(local.angle) * local.direction; /* Every nested target is a world-space sum of its parent frame and a local tangent. Reserve - the allocated descendant path and solve the remaining directional budget at each depth. - The final kinematic cap below remains a defensive invariant for floating-point closure - and any future target source. */ + the allocated descendant path and solve the remaining directional budget at each depth. */ const descendantSpeed = descendantSpeedBudget(node); const parentSpeedBudget = galaxyRelativeSpeedBudget(parentTarget, strictSpeedLimit, Number.POSITIVE_INFINITY, localTangentX, localTangentY); const nestedParentSpeedLimit = Math.max(0, parentSpeedBudget - descendantSpeed); const requestedSpeed = Math.min(requestedLocalSpeed, nestedParentSpeedLimit); + if (requestedLocalSpeed > requestedSpeed + SPEED_LIMIT_DIAGNOSTIC_EPSILON) { + speedCapped = true; + } const b1 = galaxyRelativeSpeedBudget( parentTarget, strictSpeedLimit, requestedSpeed, localTangentX, localTangentY); - const nextAngle = local.angle + local.direction * (b1 / Math.max(1e-9, localRadius)) * timestep; - const nextTanX = -Math.sin(nextAngle) * local.direction; - const nextTanY = Math.cos(nextAngle) * local.direction; - const phaseSpeed = Math.min(b1, galaxyRelativeSpeedBudget( - parentTarget, strictSpeedLimit, b1, nextTanX, nextTanY)); - const cappedOmega = phaseSpeed / Math.max(1e-9, localRadius); - local.angle += local.direction * cappedOmega * timestep; - const offsetX = Math.cos(local.angle) * localRadius; - const offsetY = Math.sin(local.angle) * localRadius; - const advancedTangentX = -Math.sin(local.angle) * local.direction; - const advancedTangentY = Math.cos(local.angle) * local.direction; + if (b1 < requestedSpeed - SPEED_LIMIT_DIAGNOSTIC_EPSILON) speedCapped = true; + /* The world-speed budget belongs to the phase step, not to a cleanup pass after the + position has already moved. The tangent rotates as the phase advances, so solve that + small coupling to convergence before committing the angle. This keeps the composed + parent+local velocity and the visible displacement on the same capped orbit. */ + let phaseSpeed = b1; + let nextAngle = local.angle; + for (let iteration = 0; iteration < 8; iteration++) { + nextAngle = local.angle + local.direction + * (phaseSpeed / Math.max(1e-9, localRadius)) * timestep; + const nextTanX = -Math.sin(nextAngle) * local.direction; + const nextTanY = Math.cos(nextAngle) * local.direction; + const boundedPhaseSpeed = galaxyRelativeSpeedBudget( + parentTarget, strictSpeedLimit, phaseSpeed, nextTanX, nextTanY); + if (boundedPhaseSpeed < phaseSpeed - SPEED_LIMIT_DIAGNOSTIC_EPSILON) { + speedCapped = true; + } + if (!(boundedPhaseSpeed < phaseSpeed - 1e-12)) break; + phaseSpeed = boundedPhaseSpeed; + } + nextAngle = local.angle + local.direction + * (phaseSpeed / Math.max(1e-9, localRadius)) * timestep; + local.angle = nextAngle; + const offsetX = Math.cos(nextAngle) * localRadius; + const offsetY = Math.sin(nextAngle) * localRadius; + const advancedTangentX = -Math.sin(nextAngle) * local.direction; + const advancedTangentY = Math.cos(nextAngle) * local.direction; const target = { x: parentTarget.x + offsetX, y: parentTarget.y + offsetY, @@ -3290,7 +3307,7 @@ if (Number.isFinite(node.fx)) node.fx = target.x; if (Number.isFinite(node.fy)) node.fy = target.y; }); - return { targets, satellites }; + return { targets, satellites, speedCapped }; } function setGalaxyKinematicPhase(node, name, value) { @@ -3340,16 +3357,6 @@ if (Number.isFinite(node.fx)) node.fx = x; if (Number.isFinite(node.fy)) node.fy = y; }; - const angularFrequency = (radius, authoredCarrier) => { - const requestedSpeed = authoredCarrier - ? galaxyAuthoredCarrierTargetSpeed(field, radius, opts.orbitalSpeed) - : galaxyCarrierTargetSpeed(field, radius, opts.orbitalSpeed); - /* The carrier is the parent frame for every local orbit. Cap it before - constructing that frame, otherwise a high authored clock can make - the child speed budget infeasible and scatter the local system. */ - const speed = Math.min(strictSpeedLimit, Math.max(0, requestedSpeed)); - return speed / Math.max(1e-6, radius); - }; const boundedRadius = (radius, extent) => { const paintedAnchorRadius = nodeRadius(anchor) * (anchor.anchor_role === 'global' ? GALAXY_BLACK_HOLE_PAINT_SCALE : 1); @@ -3358,7 +3365,7 @@ const outer = Math.max(inner, (Number(envelope.envelopeRadius) || inner) - Math.max(0, extent)); return Math.max(inner, Math.min(outer, radius)); }; - let systems = 0, satellites = 0; + let systems = 0, satellites = 0, speedCapped = false; field.systems.forEach(item => { const members = item.nodes; if (!members.length || members.some(node => node.id === opts.fixedNodeId)) return; @@ -3397,7 +3404,14 @@ orbit.angle = seededHash(opts.layoutSeed, 'kinematic-system:' + item.id) / 0x100000000 * Math.PI * 2; } - const omega = angularFrequency(orbit.radius, !item.core); + const requestedCarrierSpeed = item.core + ? galaxyCarrierTargetSpeed(field, orbit.radius, opts.orbitalSpeed) + : galaxyAuthoredCarrierTargetSpeed(field, orbit.radius, opts.orbitalSpeed); + const carrierSpeed = Math.min(strictSpeedLimit, Math.max(0, requestedCarrierSpeed)); + if (requestedCarrierSpeed > carrierSpeed + SPEED_LIMIT_DIAGNOSTIC_EPSILON) { + speedCapped = true; + } + const omega = carrierSpeed / Math.max(1e-6, orbit.radius); orbit.angle += direction * omega * timestep; if (item.core) { setPhase(star, '__galaxyCoreLaneRadius', orbit.radius); @@ -3421,6 +3435,7 @@ localOrbitCache: '__galaxyKinematicCoreLocalOrbit', }) : opts); satellites += localMotion.satellites; + speedCapped = speedCapped || localMotion.speedCapped; const carrierContact = nodeRadius(anchor) + nodeRadius(star) + GALAXY_BLACK_HOLE_EXCLUSION_PADDING; const carrierOuter = galaxyEventHorizonOuterRadius( @@ -3441,13 +3456,12 @@ : { systems: 0, overlaps: 0, adjustedSystems: 0, remainingOverlaps: 0, infeasiblePairs: 0, gap: 0 }; const blackHoleSpinAngle = advanceGalaxyBlackHoleSpin(nodes, opts); - const finalSpeed = enforceGalaxyGlobalSpeedLimit(bodies, { - fixedNodeId: opts.fixedNodeId, - limit: absoluteSpeedLimit, - }); + const maximumSpeed = bodies.reduce((maximum, node) => Math.max(maximum, + Math.hypot(Number.isFinite(node.vx) ? node.vx : 0, + Number.isFinite(node.vy) ? node.vy : 0)), 0); return { bodies: bodies.length, systems, satellites, systemPacking, blackHoleSpinAngle, ghostOrbit: integrateGalaxyGhostOrbits(nodes, opts), - maximumSpeed: finalSpeed.maximumAfter, speedCapped: finalSpeed.applied }; + maximumSpeed, speedCapped }; } function recenterGalaxyOnAnchor(nodes) { From eed3e29779aac916f8943366cefaa79334951455 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Tue, 8 Sep 2026 03:08:52 -0400 Subject: [PATCH 21/21] fix(graph): space homogeneous galaxy tiers by local slots --- engraphis/core/graph_scene.py | 37 +++++++++++++++++++++++++++++---- tests/test_graph_explorer_v2.py | 27 ++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/engraphis/core/graph_scene.py b/engraphis/core/graph_scene.py index bcfbac17..cc553141 100644 --- a/engraphis/core/graph_scene.py +++ b/engraphis/core/graph_scene.py @@ -790,11 +790,28 @@ def _community_positions( remaining -= take curr_radius += tier_step + # Homogeneous systems can share a true tier-local lattice: using the actual slot + # count keeps every lane's angular gaps consistent and avoids radial fallback when + # several similarly sized systems sit close to the core. Heterogeneous envelopes + # retain the scene-wide low-discrepancy carrier so a large system does not create a + # regular angular wall for the smaller systems. A three-or-more-tier scene is also + # compact enough that local lane spacing is the safer choice even with modest size + # variation. + non_global_radii = [ + _clamp(_finite_float(c.get("radius"), 36.0), 36.0, 10_000.0) + for c in non_global + ] + homogeneous_envelopes = ( + max(non_global_radii, default=0.0) - min(non_global_radii, default=0.0) + <= max(2.0, avg_sys_radius * 0.08) + ) + use_tier_local_slots = len(tiers) >= 3 or homogeneous_envelopes + sys_idx = 0 - for tier_info in tiers: + for tier_index, tier_info in enumerate(tiers): t_rad = float(tier_info["radius"]) t_count = int(tier_info["count"]) - for _ in range(t_count): + for tier_slot in range(t_count): community = non_global[sys_idx] community_id = str(community["id"]) system_radius = _clamp( @@ -810,8 +827,20 @@ def _community_positions( radial_jitter = 0.96 + ( int.from_bytes(digest[4:8], "big") / float(1 << 32) ) * 0.08 - golden_angle = base_phase + sys_idx * GOLDEN_ANGLE_RAD - angle = golden_angle + angular_jitter + if use_tier_local_slots: + tier_phase = base_phase + tier_index * math.tau / 12.0 + # Use the tier's actual slot count rather than continuing the previous + # tier's golden-angle rank; the deterministic phase keeps the lanes + # visually distinct while the exact local lattice prevents radial + # fallback from an ID-dependent jitter collapse. + angle = ( + tier_phase + + tier_slot * math.tau / max(1, t_count) + ) + else: + # Mixed-size two-tier scenes keep a scene-wide low-discrepancy carrier so + # the angular distribution remains stable across unequal envelopes. + angle = base_phase + sys_idx * GOLDEN_ANGLE_RAD + angular_jitter # ``preferred_targets`` applies the user-facing compactness scale below. # Tier radii are already physical lane coordinates, so compensate here or a diff --git a/tests/test_graph_explorer_v2.py b/tests/test_graph_explorer_v2.py index e7b1b657..a73619a0 100644 --- a/tests/test_graph_explorer_v2.py +++ b/tests/test_graph_explorer_v2.py @@ -1313,6 +1313,33 @@ def test_community_spiral_packs_compact_preferred_targets_without_envelope_overl assert max(x_span, y_span) < 4800.0 +def test_community_tiers_use_actual_slot_counts_for_homogeneous_lanes(): + communities = [ + {"id": "core", "mass": 100.0, "radius": 50.0}, + *[ + {"id": f"system-{index:02d}", "mass": 99.0 - index, "radius": 36.0} + for index in range(24) + ], + ] + + positions, hints = graph_scene_module._community_positions( + communities, "core", 7, spacing=78.0 + ) + repeated = graph_scene_module._community_positions( + communities, "core", 7, spacing=78.0 + ) + assert (positions, hints) == repeated + + radial_shifts = [ + math.hypot(*positions[community["id"]]) + - hints[community["id"]]["galactic_preferred_radius"] + for community in communities[1:] + ] + assert max(radial_shifts) < 5.0 + assert max(math.hypot(*positions[community["id"]]) for community in communities[1:]) < 300.0 + assert not any(hints[community["id"]]["galactic_overlap"] for community in communities) + + def test_community_spiral_spatial_traversal_is_subquadratic(monkeypatch): calls = 0 original_hypot = math.hypot