Skip to content

Commit 4a6ebe2

Browse files
committed
checkpoint: per-population meshes + analytics/physics correctness
Client viz: - GemNodes renders one InstancedMesh per population (knowledge/ontology/ agent) via forceMode + instanceIdBase; GraphManager partitions visible nodes by visual mode so a mixed graph shows each population's primitive instead of a single dominant geometry. KnowledgeRings + meshProxy track the knowledge-population mesh. - Population SSOT: metadata.type is the authoritative classification (T1), resolved consistently across visual-state, filtering, and scaling. Backend (analytics + GPU): - Louvain modularity + parallel-race fixes, PageRank FFI, SSSP feed, anomaly routing; single-writer node_analytics; physics bounds SSOT and compact-settle profile in PhysicsConfig::default(). Docs + tests: - Verified architecture diagrams, KNOWN_ISSUES, regenerated client/backend architecture docs; T1/T2/T4/T5 repro + wire-snapshot tests. Co-Authored-By: jjohare <github@thedreamlab.uk>
1 parent 06221af commit 4a6ebe2

71 files changed

Lines changed: 6226 additions & 716 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ Research + NotebookLM? --> /notebooklm, /perplexity-research, /gemini-url-contex
7676
Deep cited research? --> /deep-research (parallel agents, provenance, verification)
7777
Optimize a metric iteratively? --> /autoresearch (experiment loop, keep/discard)
7878
Add source verification? --> /provenance-tracking (.provenance.md sidecar)
79+
Owner's personal email (invoices, threads, "did X email me", find a name)? --> /email-search (ask_email; local gateway, privacy-filtered; NOT work mail/calendar)
7980
Security / compliance? --> /defense-security
8081
AEC (building architecture)? --> /studio [task]
8182
SEO / content optimisation? --> /toprank
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* Regression tests for T4: validation-ceiling consistency (TypeScript/client side).
3+
*
4+
* RESOLVED 2026-06-03. Previously the actor path-pattern ceilings were narrower
5+
* than the canonical client defaults (repelK 120 > 100, maxVelocity 100 > 50,
6+
* springK 12 > 10), so boot-time defaults were silently clamped. The backend now
7+
* reads every (MIN, MAX) from a single source of truth,
8+
* src/actors/gpu/physics_bounds.rs, so the actor path validator, the route
9+
* validator and the canonical client defaults can never diverge again.
10+
*
11+
* These tests pin the UNIFIED ceilings (kept in sync with physics_bounds.rs) and
12+
* assert the canonical DEFAULT_PHYSICS_SETTINGS values are ACCEPTED — i.e. each
13+
* default sits inside the legal range and would not be clamped on boot. The
14+
* Rust-side counterpart (tests/repro_t4_ceiling_consistency.rs) drives the real
15+
* validate_physics_settings() with these same magnitudes.
16+
*
17+
* Fix spec: docs/architecture/diagrams/qe-T2-T4-writepaths-ceilings.md
18+
*/
19+
20+
import { describe, it, expect } from 'vitest';
21+
import { DEFAULT_PHYSICS_SETTINGS } from '../defaults';
22+
23+
// ---------------------------------------------------------------------------
24+
// Unified ceilings — single source of truth is
25+
// src/actors/gpu/physics_bounds.rs (MAX of each (MIN, MAX) Bound). Kept in sync
26+
// with the Rust constants; the Rust test pins the same magnitudes against the
27+
// real validator so a drift on either side is caught.
28+
// ---------------------------------------------------------------------------
29+
const SPRING_K_MAX = 500.0; // physics_bounds::SPRING_K.1
30+
const REPEL_K_MAX = 500.0; // physics_bounds::REPEL_K.1
31+
const MAX_VELOCITY_MAX = 1000.0; // physics_bounds::MAX_VELOCITY.1
32+
const MAX_FORCE_MAX = 5000.0; // physics_bounds::MAX_FORCE.1
33+
34+
// Rust backstop constant — force_compute_actor.rs. Held equal to the unified
35+
// max_velocity ceiling so the backstop is only ever a safety net.
36+
const RUST_BACKSTOP_MAX_VELOCITY = 1000.0;
37+
38+
// Canonical default magnitudes the fix must keep accepting (defaults.ts).
39+
const CANONICAL = { springK: 12.0, repelK: 120.0, maxVelocity: 100.0, maxForce: 150.0 };
40+
41+
describe('T4 (resolved): canonical physics defaults are accepted by the unified ceilings', () => {
42+
it('defaults.ts still carries the canonical magnitudes', () => {
43+
expect(DEFAULT_PHYSICS_SETTINGS.springK).toBe(CANONICAL.springK);
44+
expect(DEFAULT_PHYSICS_SETTINGS.repelK).toBe(CANONICAL.repelK);
45+
expect(DEFAULT_PHYSICS_SETTINGS.maxVelocity).toBe(CANONICAL.maxVelocity);
46+
expect(DEFAULT_PHYSICS_SETTINGS.maxForce).toBe(CANONICAL.maxForce);
47+
});
48+
49+
it('repelK default (120) is within the unified ceiling (500) — not clamped', () => {
50+
expect(DEFAULT_PHYSICS_SETTINGS.repelK!).toBeLessThanOrEqual(REPEL_K_MAX);
51+
});
52+
53+
it('maxVelocity default (100) is within the unified ceiling (1000) — not clamped', () => {
54+
expect(DEFAULT_PHYSICS_SETTINGS.maxVelocity!).toBeLessThanOrEqual(MAX_VELOCITY_MAX);
55+
});
56+
57+
it('springK default (12) is within the unified ceiling (500) — not clamped', () => {
58+
expect(DEFAULT_PHYSICS_SETTINGS.springK!).toBeLessThanOrEqual(SPRING_K_MAX);
59+
});
60+
61+
it('maxForce default (150) is within the unified ceiling (5000) — not clamped', () => {
62+
expect(DEFAULT_PHYSICS_SETTINGS.maxForce!).toBeLessThanOrEqual(MAX_FORCE_MAX);
63+
});
64+
65+
it('the unified max_velocity ceiling equals the Rust backstop so the backstop never clamps healthy frames', () => {
66+
expect(MAX_VELOCITY_MAX).toBe(RUST_BACKSTOP_MAX_VELOCITY);
67+
});
68+
});

client/src/api/settings/defaults.ts

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,35 +15,38 @@ export const DEFAULT_PHYSICS_SETTINGS: Partial<PhysicsSettings> = {
1515
iterations: 50,
1616
warmupIterations: 100,
1717
coolingRate: 0.001,
18-
globalSpeed: 0.5,
19-
damping: 0.85,
18+
globalSpeed: 0.4,
19+
damping: 0.9,
2020

21-
// --- Core forces ---
22-
springK: 15.0,
23-
repelK: 1200.0,
24-
restLength: 80.0,
25-
centerGravityK: 0.05,
26-
gravity: 0.0001,
27-
maxForce: 1000.0,
21+
// --- Core forces (canonical compact profile — settles within ~400u bounds) ---
22+
springK: 12.0,
23+
repelK: 120.0,
24+
restLength: 50.0,
25+
centerGravityK: 0.2,
26+
gravity: 0.002,
27+
maxForce: 150.0,
2828
maxVelocity: 100.0,
2929

3030
// --- Repulsion & spacing ---
31-
maxRepulsionDist: 1000.0,
31+
maxRepulsionDist: 400.0,
3232
separationRadius: 2.1155233,
3333
gridCellSize: 50.0,
3434
repulsionSofteningEpsilon: 0.0001,
3535

3636
// --- Bounds ---
37-
enableBounds: false,
38-
boundsSize: 2000.0,
37+
// Soft-cube containment sized to the ~400-unit graph envelope. Client fallback
38+
// only — the backend PhysicsSettings::default() is the single source of truth
39+
// and is hydrated over these on connect.
40+
enableBounds: true,
41+
boundsSize: 400.0,
3942
boundaryDamping: 0.95,
4043

4144
// --- Layout forces (FA2 / dual-graph) ---
4245
linLogMode: true,
4346
scalingRatio: 10.0,
4447
adaptiveSpeed: true,
4548
ssspAlpha: 1.5,
46-
graphSeparationX: 1000.0,
49+
graphSeparationX: 0.0,
4750
axisCompressionZ: 0.9,
4851

4952
// --- Per-population spring strength (independent KG/ontology/agent layout control) ---
@@ -140,6 +143,11 @@ export const DEFAULT_CLUSTER_HULLS = {
140143
// ADR-031 D6: opt-in fabricated spatial-grid hulls. Default OFF — when server
141144
// clusters are absent, show an empty state, never a fabricated grid.
142145
spatialFallback: false,
146+
// ADR-031 D6: opt-in Louvain-community hulls. Default OFF — community_id is
147+
// real but optimised for modularity not spatial locality, so its hulls overlap
148+
// into a blob. The honest default community signal is per-node colour
149+
// (nodes.colorScheme: 'community'); enable this only when you want the volumes.
150+
communityFallback: false,
143151
};
144152

145153
// ADR-031 D6: ship qualityGates defaults so correct server analytics render by
@@ -197,7 +205,10 @@ export const DEFAULT_INTERACTION_SETTINGS = {
197205

198206
export const DEFAULT_NODES_SETTINGS = {
199207
baseColor: '#4a6fa5',
200-
colorScheme: 'type' as 'type' | 'domain' | 'base',
208+
// ADR-031 D6: default to 'community' so the live Louvain partition renders out
209+
// of the box. Nodes the server left unclustered (community_id 0) fall through
210+
// to per-type colouring in computeColor, so the default is never all-grey.
211+
colorScheme: 'community' as 'type' | 'domain' | 'base' | 'community' | 'cluster' | 'centrality' | 'sssp',
201212
sizeScheme: 'hybrid' as 'degree' | 'fileSize' | 'hybrid',
202213
perNodeGlow: true,
203214
metalness: 0.1,

client/src/features/analytics/store/nodeAnalyticsStore.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,17 +176,29 @@ class NodeAnalyticsStore {
176176
showCentrality?: boolean;
177177
showSSSP?: boolean;
178178
};
179-
visualisation?: { clusterHulls?: { enabled?: boolean } };
179+
visualisation?: {
180+
clusterHulls?: { enabled?: boolean };
181+
graphs?: { logseq?: { nodes?: { colorScheme?: string } } };
182+
};
180183
}
181184
| undefined;
182185
const qg = s?.qualityGates;
186+
// ADR-031 D6: an analytic colour scheme consumes the analytics buffer even
187+
// when every quality gate is off, so ingestion must stay live for it.
188+
const colorScheme = s?.visualisation?.graphs?.logseq?.nodes?.colorScheme;
189+
const analyticColorScheme =
190+
colorScheme === 'community' ||
191+
colorScheme === 'cluster' ||
192+
colorScheme === 'centrality' ||
193+
colorScheme === 'sssp';
183194
return Boolean(
184195
qg?.showClusters ||
185196
qg?.showAnomalies ||
186197
qg?.showCommunities ||
187198
qg?.showCentrality ||
188199
qg?.showSSSP ||
189-
s?.visualisation?.clusterHulls?.enabled,
200+
s?.visualisation?.clusterHulls?.enabled ||
201+
analyticColorScheme,
190202
);
191203
}
192204
}

client/src/features/graph/components/ClusterHulls.tsx

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
nodeAnalyticsStore,
77
ANALYTICS_STRIDE,
88
ANALYTICS_CLUSTER_OFFSET,
9+
ANALYTICS_COMMUNITY_OFFSET,
910
} from '../../analytics/store/nodeAnalyticsStore';
1011

1112
// ============================================================================
@@ -81,6 +82,15 @@ function getDomainHullColor(domain: string): string {
8182
return DOMAIN_COLORS[domain] ?? DEFAULT_COLOR;
8283
}
8384

85+
// ADR-031 D6: Louvain community hull colour. Matches GemNodes' community node
86+
// hue (multiply-by-83 mod 360, HSL 0.65/0.5) so a hull and its member nodes
87+
// read as the same region.
88+
const _communityHullColor = new THREE.Color();
89+
function getCommunityHullColor(communityId: number): string {
90+
const hue = ((communityId * 83) % 360) / 360;
91+
return `#${_communityHullColor.setHSL(hue, 0.65, 0.5).getHexString()}`;
92+
}
93+
8494
interface ClusterPoint {
8595
id: string;
8696
x: number;
@@ -264,11 +274,17 @@ export const ClusterHulls: React.FC<ClusterHullsProps> = ({
264274
const analytics = analyticsRef.current;
265275
const positions = nodePositionsRef.current;
266276

267-
// Detect whether any real cluster_id is present (stride ANALYTICS_STRIDE).
277+
// Detect which server signals are present (stride ANALYTICS_STRIDE).
278+
// cluster_id (DBSCAN/k-means) is the preferred grouping; community_id
279+
// (Louvain) is the fallback when no DBSCAN run has populated cluster_id.
268280
let hasClusterId = false;
281+
let hasCommunityId = false;
269282
if (analytics) {
270283
for (const idx of nodeIdToIndexMap.values()) {
271-
if (analytics[idx * ANALYTICS_STRIDE + ANALYTICS_CLUSTER_OFFSET] > 0) { hasClusterId = true; break; }
284+
const a = idx * ANALYTICS_STRIDE;
285+
if (analytics[a + ANALYTICS_CLUSTER_OFFSET] > 0) hasClusterId = true;
286+
if (analytics[a + ANALYTICS_COMMUNITY_OFFSET] > 0) hasCommunityId = true;
287+
if (hasClusterId) break; // cluster_id wins outright; stop early
272288
}
273289
}
274290

@@ -293,6 +309,30 @@ export const ClusterHulls: React.FC<ClusterHullsProps> = ({
293309
return { map, colorByKey };
294310
}
295311

312+
// ADR-031 D6: no DBSCAN cluster_id, but the live graph carries Louvain
313+
// community_id. Community_id is real server structure, but Louvain optimises
314+
// graph modularity, not spatial locality — a community's members scatter
315+
// across the disc, so its convex hull blankets the whole graph and the
316+
// hulls overlap into an uninspectable blob. So community hulls are an opt-in
317+
// tier (default off), same as the fabricated spatial fallback; the honest
318+
// default community signal is per-node colour (colorScheme: 'community').
319+
// Cluster hulls (DBSCAN, spatially compact) remain default-on above.
320+
const communityFallback = settings?.visualisation?.clusterHulls?.communityFallback === true;
321+
if (hasCommunityId && communityFallback) {
322+
for (let ni = 0; ni < nodes.length; ni++) {
323+
const node = nodes[ni];
324+
const nodeIndex = nodeIdToIndexMap.get(node.id);
325+
if (nodeIndex === undefined) continue;
326+
const communityId = analytics![nodeIndex * ANALYTICS_STRIDE + ANALYTICS_COMMUNITY_OFFSET];
327+
if (!(communityId > 0)) continue;
328+
const key = `community-${communityId}`;
329+
let arr = map.get(key);
330+
if (!arr) { arr = []; map.set(key, arr); colorByKey.set(key, getCommunityHullColor(communityId)); }
331+
arr.push(node.id);
332+
}
333+
return { map, colorByKey };
334+
}
335+
296336
// No server clusters. The JS spatial-grid heuristic fabricates clusters and
297337
// masks missing server data, so it is opt-in (default off). When off, return
298338
// an empty map → the layer shows nothing.
@@ -313,7 +353,7 @@ export const ClusterHulls: React.FC<ClusterHullsProps> = ({
313353
// Spatial clusters carry no semantic key; hullEntries colours them by rank.
314354
return { map: buildSpatialClusters(pts, SPATIAL_TARGET_CELLS), colorByKey };
315355
// eslint-disable-next-line react-hooks/exhaustive-deps
316-
}, [nodes, nodeIdToIndexMap, nodePositionsRef, tick, settings?.visualisation?.clusterHulls?.spatialFallback]);
356+
}, [nodes, nodeIdToIndexMap, nodePositionsRef, tick, settings?.visualisation?.clusterHulls?.spatialFallback, settings?.visualisation?.clusterHulls?.communityFallback]);
317357

318358
// ---- Build hull geometries from current positions ----
319359
const hullEntries = useMemo(() => {

0 commit comments

Comments
 (0)