mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
feat(ui): region level of detail for the galaxy (level=regions, scope=region:N)
The galaxy loaded 'the first max_nodes nodes' of a project, which above ~10k nodes meant freezing the tab (#498 family) or truncating the graph. Give it a real coarsest level instead: one body per REGION. Regions are sets of files. Files are assigned by majority vote of their callables' Leiden communities over the CALLS graph (seeded, deterministic); files no kept community explains fall back to folder groups (first two path components), and a final 'misc' bucket catches the rest. Every node inherits its file's region, all edge types aggregate into region-to-region weights, and each region carries its name's provenance ('call community: 202 files, 38% under tests/db_functions'), cohesion, member/file counts and its top callables by degree. GET /api/layout?project=X&level=regions -> region bodies + weighted arcs GET /api/layout?project=X&scope=region:N -> full detail, that region only Positions come from a golden-angle sphere seed refined by the shared anchored optimizer (extracted to layout_internal.h alongside the new cbm_layout_from_nodes core, so the region scope reuses the exact layout path). Results are cached per (project, indexed_at): on a 48k-node index the region scene computes in ~1.6s cold, <1ms cached; a 4.8k-node region scope lays out in ~0.5s. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
1 parent
dab11a7d31
commit
499eeba51b
7 files changed
+1455
-29
No files matched your search
@@ -387,6 +387,7 @@ UI_SRCS = \
|
||||
src/ui/config.c \
|
||||
src/ui/http_server.c \
|
||||
src/ui/layout3d.c \
|
||||
src/ui/layout_regions.c \
|
||||
src/ui/httpd.c \
|
||||
src/ui/embedded_stub.c
|
||||
|
||||
|
||||
@@ -1485,6 +1485,57 @@ static void handle_layout(cbm_http_conn_t *c, const cbm_http_req_t *req) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* level=regions (CBM Atlas): the coarsest level of detail — one body per
|
||||
* region instead of one per node. Regions do not mix with the missed
|
||||
* skeleton or cross-repo satellites; the client asks for those at finer
|
||||
* levels. */
|
||||
char level_str[32] = {0};
|
||||
if (cbm_http_query_param(req->query, "level", level_str, (int)sizeof(level_str)) &&
|
||||
strcmp(level_str, "regions") == 0 && !missed_graph) {
|
||||
char *regions_json = cbm_layout_regions_json(store, scoped_project);
|
||||
cbm_store_close(store);
|
||||
if (!regions_json) {
|
||||
cbm_http_replyf(c, 500, g_cors_json,
|
||||
"{\"error\":\"region computation failed\"}");
|
||||
return;
|
||||
}
|
||||
cbm_http_replyf(c, 200, g_cors_json, "%s", regions_json);
|
||||
free(regions_json);
|
||||
return;
|
||||
}
|
||||
|
||||
/* scope=region:<id> — full-detail layout of one region's files only. */
|
||||
char scope_str[64] = {0};
|
||||
int scope_region = -1;
|
||||
if (cbm_http_query_param(req->query, "scope", scope_str, (int)sizeof(scope_str)) &&
|
||||
strncmp(scope_str, "region:", 7) == 0) {
|
||||
scope_region = atoi(scope_str + 7);
|
||||
if (scope_region < 0) {
|
||||
cbm_store_close(store);
|
||||
cbm_http_replyf(c, 400, g_cors_json, "{\"error\":\"invalid scope\"}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (scope_region >= 0 && !missed_graph) {
|
||||
cbm_layout_result_t *scoped =
|
||||
cbm_layout_compute_region(store, scoped_project, scope_region, max_nodes);
|
||||
cbm_store_close(store);
|
||||
if (!scoped) {
|
||||
cbm_http_replyf(c, 404, g_cors_json, "{\"error\":\"unknown region\"}");
|
||||
return;
|
||||
}
|
||||
char *scoped_json = cbm_layout_to_json(scoped);
|
||||
cbm_layout_free(scoped);
|
||||
if (!scoped_json) {
|
||||
cbm_http_replyf(c, 500, g_cors_json,
|
||||
"{\"error\":\"JSON serialization failed\"}");
|
||||
return;
|
||||
}
|
||||
cbm_http_replyf(c, 200, g_cors_json, "%s", scoped_json);
|
||||
free(scoped_json);
|
||||
return;
|
||||
}
|
||||
|
||||
cbm_layout_result_t *layout =
|
||||
cbm_layout_compute(store, scoped_project, CBM_LAYOUT_OVERVIEW, NULL, 0, max_nodes);
|
||||
|
||||
|
||||
+46
-29
@@ -12,6 +12,7 @@
|
||||
*/
|
||||
#include "foundation/constants.h"
|
||||
#include "ui/layout3d.h"
|
||||
#include "ui/layout_internal.h"
|
||||
#include "foundation/log.h"
|
||||
|
||||
#include <yyjson/yyjson.h>
|
||||
@@ -157,7 +158,7 @@ static float size_for_label(const char *label) {
|
||||
return 4.0f;
|
||||
}
|
||||
|
||||
static uint32_t fnv1a(const char *s) {
|
||||
uint32_t cbm_layout_fnv1a(const char *s) {
|
||||
uint32_t h = 2166136261u;
|
||||
if (!s)
|
||||
return h;
|
||||
@@ -168,7 +169,7 @@ static uint32_t fnv1a(const char *s) {
|
||||
return h;
|
||||
}
|
||||
|
||||
static float rand_float(uint32_t *seed) {
|
||||
float cbm_layout_rand_float(uint32_t *seed) {
|
||||
*seed = (*seed) * 1103515245u + 12345u;
|
||||
return (float)((*seed >> 16) & 0x7FFF) / 32768.0f - 0.5f;
|
||||
}
|
||||
@@ -314,18 +315,11 @@ static void octree_repulse(octree_node_t *n, float px, float py, float pz, float
|
||||
octree_repulse(n->children[i], px, py, pz, mm, si, kr, fx, fy, fz);
|
||||
}
|
||||
|
||||
/* ── Body with anchor ─────────────────────────────────────────── */
|
||||
|
||||
typedef struct {
|
||||
float x, y, z;
|
||||
float ax, ay, az; /* anchor position (from ring layout) */
|
||||
float fx, fy, fz;
|
||||
float mass;
|
||||
} body_t;
|
||||
/* ── Body with anchor — cbm_layout_cbm_layout_body_t in layout_internal.h ── */
|
||||
|
||||
/* ── Local optimization (gentle, anchor-preserving) ───────────── */
|
||||
|
||||
static void local_optimize(body_t *b, int n, const int *es, const int *ed, int ne) {
|
||||
void cbm_layout_local_optimize(cbm_layout_body_t *b, int n, const int *es, const int *ed, int ne) {
|
||||
/* Scale iteration effort down for very large graphs: each iteration is
|
||||
* O(n log n) octree work, and past ~100k bodies the anchor layout already
|
||||
* dominates the visible structure — fewer refinement passes keep huge
|
||||
@@ -514,7 +508,10 @@ cbm_layout_result_t *cbm_layout_compute(cbm_store_t *store, const char *project,
|
||||
(void)radius;
|
||||
(void)level;
|
||||
|
||||
/* 1. Query nodes */
|
||||
/* 1. Query nodes, then hand the structs to the fetch-agnostic core (the
|
||||
* region scope feeds cbm_layout_from_nodes its own node set). Strings in
|
||||
* the copied structs stay borrowed from search_out until the core has
|
||||
* duplicated them into the result. */
|
||||
cbm_search_params_t params;
|
||||
memset(¶ms, 0, sizeof(params));
|
||||
params.project = project;
|
||||
@@ -528,8 +525,31 @@ cbm_layout_result_t *cbm_layout_compute(cbm_store_t *store, const char *project,
|
||||
return calloc(CBM_ALLOC_ONE, sizeof(cbm_layout_result_t));
|
||||
|
||||
int n = search_out.count, total_count = search_out.total;
|
||||
if (n == 0) {
|
||||
cbm_store_search_free(&search_out);
|
||||
cbm_node_t *node_arr = NULL;
|
||||
if (n > 0) {
|
||||
node_arr = malloc((size_t)n * sizeof(cbm_node_t));
|
||||
if (!node_arr) {
|
||||
cbm_store_search_free(&search_out);
|
||||
cbm_layout_result_t *r = calloc(CBM_ALLOC_ONE, sizeof(*r));
|
||||
if (r)
|
||||
r->total_nodes = total_count;
|
||||
return r;
|
||||
}
|
||||
for (int i = 0; i < n; i++)
|
||||
node_arr[i] = search_out.results[i].node;
|
||||
}
|
||||
cbm_layout_result_t *result = cbm_layout_from_nodes(store, project, node_arr, n, total_count);
|
||||
free(node_arr);
|
||||
cbm_store_search_free(&search_out);
|
||||
return result;
|
||||
}
|
||||
|
||||
cbm_layout_result_t *cbm_layout_from_nodes(cbm_store_t *store, const char *project,
|
||||
const cbm_node_t *nodes, int n, int total_nodes) {
|
||||
int total_count = total_nodes;
|
||||
if (!store || !project || n < 0)
|
||||
return NULL;
|
||||
if (n == 0 || !nodes) {
|
||||
cbm_layout_result_t *r = calloc(CBM_ALLOC_ONE, sizeof(*r));
|
||||
if (r)
|
||||
r->total_nodes = total_count;
|
||||
@@ -539,7 +559,6 @@ cbm_layout_result_t *cbm_layout_compute(cbm_store_t *store, const char *project,
|
||||
/* 2. Build sorted node-ID → index map for O(log n) edge filtering */
|
||||
node_id_entry_t *id_map = malloc((size_t)n * sizeof(node_id_entry_t));
|
||||
if (!id_map) {
|
||||
cbm_store_search_free(&search_out);
|
||||
cbm_layout_result_t *r = calloc(CBM_ALLOC_ONE, sizeof(*r));
|
||||
if (r) {
|
||||
r->total_nodes = total_count;
|
||||
@@ -547,7 +566,7 @@ cbm_layout_result_t *cbm_layout_compute(cbm_store_t *store, const char *project,
|
||||
return r;
|
||||
}
|
||||
for (int i = 0; i < n; i++) {
|
||||
id_map[i].id = search_out.results[i].node.id;
|
||||
id_map[i].id = nodes[i].id;
|
||||
id_map[i].idx = i;
|
||||
}
|
||||
qsort(id_map, (size_t)n, sizeof(node_id_entry_t), cmp_node_id_entry);
|
||||
@@ -618,14 +637,14 @@ cbm_layout_result_t *cbm_layout_compute(cbm_store_t *store, const char *project,
|
||||
const char **lbls = malloc((size_t)n * sizeof(char *));
|
||||
if (lbls) {
|
||||
for (int i = 0; i < n; i++)
|
||||
lbls[i] = search_out.results[i].node.label;
|
||||
lbls[i] = nodes[i].label;
|
||||
if (cdepth)
|
||||
compute_call_depth(n, es, ed, mapped, lbls, cdepth);
|
||||
free(lbls);
|
||||
}
|
||||
|
||||
/* 5. Seed positions: ring by directory cluster key + z from call depth */
|
||||
body_t *bodies = calloc((size_t)n, sizeof(body_t));
|
||||
cbm_layout_body_t *bodies = calloc((size_t)n, sizeof(cbm_layout_body_t));
|
||||
cbm_layout_result_t *result = calloc(CBM_ALLOC_ONE, sizeof(*result));
|
||||
if (!result || !bodies) {
|
||||
free(bodies);
|
||||
@@ -635,7 +654,6 @@ cbm_layout_result_t *cbm_layout_compute(cbm_store_t *store, const char *project,
|
||||
free(cdepth);
|
||||
cbm_layout_free(result);
|
||||
free_edge_array(all_edges, mapped);
|
||||
cbm_store_search_free(&search_out);
|
||||
return NULL;
|
||||
}
|
||||
result->nodes = calloc((size_t)n, sizeof(cbm_layout_node_t));
|
||||
@@ -655,7 +673,7 @@ cbm_layout_result_t *cbm_layout_compute(cbm_store_t *store, const char *project,
|
||||
bool dead_degrees_valid = node_ids && in_calls && in_usage && in_call_reference && deg_dummy;
|
||||
if (dead_degrees_valid) {
|
||||
for (int i = 0; i < n; i++)
|
||||
node_ids[i] = search_out.results[i].node.id;
|
||||
node_ids[i] = nodes[i].id;
|
||||
for (int off = 0; off < n; off += DEAD_DEGREE_CHUNK) {
|
||||
int cnt = (n - off < DEAD_DEGREE_CHUNK) ? (n - off) : DEAD_DEGREE_CHUNK;
|
||||
if (cbm_store_batch_count_degrees(store, node_ids + off, cnt, "CALLS", in_calls + off,
|
||||
@@ -672,7 +690,7 @@ cbm_layout_result_t *cbm_layout_compute(cbm_store_t *store, const char *project,
|
||||
}
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
const cbm_node_t *sn = &search_out.results[i].node;
|
||||
const cbm_node_t *sn = &nodes[i];
|
||||
const char *fp = sn->file_path ? sn->file_path : "";
|
||||
|
||||
/* Cluster key = first 3 dir components */
|
||||
@@ -690,14 +708,14 @@ cbm_layout_result_t *cbm_layout_compute(cbm_store_t *store, const char *project,
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t h = fnv1a(ck);
|
||||
uint32_t h = cbm_layout_fnv1a(ck);
|
||||
float angle = ((float)(h & 0xFFFF) / 65535.0f) * 6.2832f;
|
||||
float r = 500.0f + ((float)((h >> 16) & 0xFF) / 255.0f) * 250.0f;
|
||||
|
||||
uint32_t seed = fnv1a(sn->qualified_name);
|
||||
uint32_t seed = cbm_layout_fnv1a(sn->qualified_name);
|
||||
float jitter = 40.0f;
|
||||
float px = r * cosf(angle) + rand_float(&seed) * jitter;
|
||||
float py = r * sinf(angle) + rand_float(&seed) * jitter;
|
||||
float px = r * cosf(angle) + cbm_layout_rand_float(&seed) * jitter;
|
||||
float py = r * sinf(angle) + cbm_layout_rand_float(&seed) * jitter;
|
||||
float pz = cdepth ? -(float)cdepth[i] * Z_DEPTH_SPACING : 0;
|
||||
|
||||
bodies[i].x = px;
|
||||
@@ -750,7 +768,7 @@ cbm_layout_result_t *cbm_layout_compute(cbm_store_t *store, const char *project,
|
||||
}
|
||||
|
||||
/* 6. Gentle local optimization (anchor-preserving) */
|
||||
local_optimize(bodies, n, es, ed, mapped);
|
||||
cbm_layout_local_optimize(bodies, n, es, ed, mapped);
|
||||
|
||||
/* 7. Copy positions */
|
||||
for (int i = 0; i < n; i++) {
|
||||
@@ -764,8 +782,8 @@ cbm_layout_result_t *cbm_layout_compute(cbm_store_t *store, const char *project,
|
||||
result->edges = calloc((size_t)mapped, sizeof(cbm_layout_edge_t));
|
||||
result->edge_count = mapped;
|
||||
for (int e = 0; e < mapped && result->edges; e++) {
|
||||
result->edges[e].source = search_out.results[es[e]].node.id;
|
||||
result->edges[e].target = search_out.results[ed[e]].node.id;
|
||||
result->edges[e].source = nodes[es[e]].id;
|
||||
result->edges[e].target = nodes[ed[e]].id;
|
||||
result->edges[e].type = all_edges[e].type ? strdup(all_edges[e].type) : NULL;
|
||||
}
|
||||
}
|
||||
@@ -781,7 +799,6 @@ cbm_layout_result_t *cbm_layout_compute(cbm_store_t *store, const char *project,
|
||||
free(in_call_reference);
|
||||
free(deg_dummy);
|
||||
free_edge_array(all_edges, mapped);
|
||||
cbm_store_search_free(&search_out);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -71,4 +71,21 @@ void cbm_layout_free(cbm_layout_result_t *result);
|
||||
/* Serialize layout result to JSON string. Caller must free(). */
|
||||
char *cbm_layout_to_json(const cbm_layout_result_t *result);
|
||||
|
||||
/* ── CBM Atlas region level ───────────────────────────────────── */
|
||||
|
||||
/* level=regions payload: one body per region (Leiden communities voted per
|
||||
* file, folder groups as fallback), aggregated region→region edge weights,
|
||||
* deterministic positions. Cached per (project, indexed_at). Returns a
|
||||
* heap-allocated JSON document; caller frees. NULL on error. */
|
||||
char *cbm_layout_regions_json(cbm_store_t *store, const char *project);
|
||||
|
||||
/* scope=region:<id> — full-detail layout restricted to one region's files.
|
||||
* total_nodes in the result is the REGION's node count. NULL on error or an
|
||||
* unknown region id. */
|
||||
cbm_layout_result_t *cbm_layout_compute_region(cbm_store_t *store, const char *project,
|
||||
int region_id, int max_nodes);
|
||||
|
||||
/* Drop the region cache (tests; safe at any time). */
|
||||
void cbm_layout_regions_cache_clear(void);
|
||||
|
||||
#endif /* CBM_UI_LAYOUT3D_H */
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* layout_internal.h — shared internals of the server-side layout engine.
|
||||
*
|
||||
* layout3d.c owns the anchored ForceAtlas2 optimizer and its helpers;
|
||||
* layout_regions.c (the CBM Atlas region level) reuses them on a much
|
||||
* smaller body set. Nothing here is public API — src/ui only.
|
||||
*/
|
||||
#ifndef CBM_UI_LAYOUT_INTERNAL_H
|
||||
#define CBM_UI_LAYOUT_INTERNAL_H
|
||||
|
||||
#include "store/store.h"
|
||||
#include "ui/layout3d.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* ── Body with anchor ─────────────────────────────────────────── */
|
||||
|
||||
typedef struct {
|
||||
float x, y, z;
|
||||
float ax, ay, az; /* anchor position (from the seeding pass) */
|
||||
float fx, fy, fz;
|
||||
float mass;
|
||||
} cbm_layout_body_t;
|
||||
|
||||
/* Gentle anchor-preserving optimization (Barnes-Hut repulsion + edge
|
||||
* attraction + anchor springs). `es`/`ed` are edge endpoint indices into the
|
||||
* body array; `ne` is the edge count. */
|
||||
void cbm_layout_local_optimize(cbm_layout_body_t *b, int n, const int *es, const int *ed, int ne);
|
||||
|
||||
/* FNV-1a over a NUL-terminated string (NULL-safe). */
|
||||
uint32_t cbm_layout_fnv1a(const char *s);
|
||||
|
||||
/* Deterministic [-0.5, 0.5) jitter stream. */
|
||||
float cbm_layout_rand_float(uint32_t *seed);
|
||||
|
||||
/* Layout core: seed + optimize + classify an explicit node array. Borrowed
|
||||
* `nodes` structs (strings are copied into the result). `total_nodes` is the
|
||||
* project-wide count the caller knows (may exceed n). */
|
||||
cbm_layout_result_t *cbm_layout_from_nodes(cbm_store_t *store, const char *project,
|
||||
const cbm_node_t *nodes, int n, int total_nodes);
|
||||
|
||||
#endif /* CBM_UI_LAYOUT_INTERNAL_H */
|
||||
File diff suppressed because it is too large.
Load diff
+156
@@ -11,6 +11,8 @@
|
||||
#include "ui/embedded_assets.h"
|
||||
#include "ui/layout3d.h"
|
||||
#include "store/store.h"
|
||||
|
||||
#include <yyjson/yyjson.h>
|
||||
#ifdef _WIN32
|
||||
#include "foundation/win_utf8.h"
|
||||
#endif
|
||||
@@ -859,6 +861,156 @@ TEST(layout_coincident_nodes_bounded) {
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
/* ── CBM Atlas region level ───────────────────────────────────── */
|
||||
|
||||
/* Two call communities in two folders, one cross call. The region level must
|
||||
* find both, name them by their dominant folder, aggregate the cross edge,
|
||||
* and lay them out deterministically. */
|
||||
static cbm_store_t *regions_fixture(void) {
|
||||
cbm_store_t *store = cbm_store_open_memory();
|
||||
if (!store)
|
||||
return NULL;
|
||||
cbm_store_upsert_project(store, "regions-test", "/tmp/regions-test");
|
||||
static const struct {
|
||||
const char *name;
|
||||
const char *file;
|
||||
} defs[] = {
|
||||
{"alpha_one", "src/alpha/a1.c"}, {"alpha_two", "src/alpha/a1.c"},
|
||||
{"alpha_three", "src/alpha/a2.c"}, {"beta_one", "src/beta/b1.c"},
|
||||
{"beta_two", "src/beta/b1.c"}, {"beta_three", "src/beta/b2.c"},
|
||||
};
|
||||
int64_t ids[6];
|
||||
for (int i = 0; i < 6; i++) {
|
||||
cbm_node_t node;
|
||||
memset(&node, 0, sizeof(node));
|
||||
node.project = "regions-test";
|
||||
node.label = "Function";
|
||||
node.name = defs[i].name;
|
||||
char qn[128];
|
||||
snprintf(qn, sizeof(qn), "regions-test::%s", defs[i].name);
|
||||
node.qualified_name = qn;
|
||||
node.file_path = defs[i].file;
|
||||
node.start_line = 1;
|
||||
node.end_line = 5;
|
||||
ids[i] = cbm_store_upsert_node(store, &node);
|
||||
}
|
||||
/* Dense intra-community calls, one cross call alpha_one → beta_one. */
|
||||
static const int pairs[][2] = {{0, 1}, {1, 2}, {2, 0}, {0, 2},
|
||||
{3, 4}, {4, 5}, {5, 3}, {3, 5}, {0, 3}};
|
||||
for (size_t e = 0; e < sizeof(pairs) / sizeof(pairs[0]); e++) {
|
||||
cbm_edge_t edge;
|
||||
memset(&edge, 0, sizeof(edge));
|
||||
edge.project = "regions-test";
|
||||
edge.source_id = ids[pairs[e][0]];
|
||||
edge.target_id = ids[pairs[e][1]];
|
||||
edge.type = "CALLS";
|
||||
cbm_store_insert_edge(store, &edge);
|
||||
}
|
||||
return store;
|
||||
}
|
||||
|
||||
TEST(layout_regions_two_communities) {
|
||||
cbm_layout_regions_cache_clear();
|
||||
cbm_store_t *store = regions_fixture();
|
||||
ASSERT_NOT_NULL(store);
|
||||
|
||||
char *json = cbm_layout_regions_json(store, "regions-test");
|
||||
ASSERT_NOT_NULL(json);
|
||||
|
||||
yyjson_doc *doc = yyjson_read(json, strlen(json), 0);
|
||||
ASSERT_NOT_NULL(doc);
|
||||
yyjson_val *root = yyjson_doc_get_root(doc);
|
||||
ASSERT_STR_EQ(yyjson_get_str(yyjson_obj_get(root, "level")), "regions");
|
||||
ASSERT_STR_EQ(yyjson_get_str(yyjson_obj_get(root, "method")), "leiden+folders");
|
||||
ASSERT_EQ((int)yyjson_get_int(yyjson_obj_get(root, "total_nodes")), 6);
|
||||
|
||||
yyjson_val *regions = yyjson_obj_get(root, "regions");
|
||||
ASSERT_EQ((int)yyjson_arr_size(regions), 2);
|
||||
bool saw_alpha = false, saw_beta = false;
|
||||
size_t idx, max;
|
||||
yyjson_val *rv;
|
||||
yyjson_arr_foreach(regions, idx, max, rv) {
|
||||
const char *name = yyjson_get_str(yyjson_obj_get(rv, "name"));
|
||||
ASSERT_NOT_NULL(name);
|
||||
ASSERT_EQ((int)yyjson_get_int(yyjson_obj_get(rv, "files")), 2);
|
||||
ASSERT_EQ((int)yyjson_get_int(yyjson_obj_get(rv, "members")), 3);
|
||||
ASSERT_NOT_NULL(yyjson_obj_get(rv, "why"));
|
||||
ASSERT_NOT_NULL(yyjson_obj_get(rv, "x"));
|
||||
yyjson_val *tops = yyjson_obj_get(rv, "top_nodes");
|
||||
ASSERT_GT((int)yyjson_arr_size(tops), 0);
|
||||
if (strcmp(name, "src/alpha") == 0)
|
||||
saw_alpha = true;
|
||||
if (strcmp(name, "src/beta") == 0)
|
||||
saw_beta = true;
|
||||
}
|
||||
ASSERT_TRUE(saw_alpha);
|
||||
ASSERT_TRUE(saw_beta);
|
||||
|
||||
/* Exactly one aggregated cross edge with weight 1. */
|
||||
yyjson_val *edges = yyjson_obj_get(root, "edges");
|
||||
ASSERT_EQ((int)yyjson_arr_size(edges), 1);
|
||||
yyjson_val *edge = yyjson_arr_get(edges, 0);
|
||||
ASSERT_EQ((int)yyjson_get_int(yyjson_obj_get(edge, "weight")), 1);
|
||||
|
||||
/* Deterministic: a second computation (fresh cache) is byte-identical. */
|
||||
cbm_layout_regions_cache_clear();
|
||||
char *json2 = cbm_layout_regions_json(store, "regions-test");
|
||||
ASSERT_NOT_NULL(json2);
|
||||
ASSERT_STR_EQ(json, json2);
|
||||
|
||||
free(json2);
|
||||
yyjson_doc_free(doc);
|
||||
free(json);
|
||||
cbm_store_close(store);
|
||||
cbm_layout_regions_cache_clear();
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(layout_regions_scope_restricts_nodes) {
|
||||
cbm_layout_regions_cache_clear();
|
||||
cbm_store_t *store = regions_fixture();
|
||||
ASSERT_NOT_NULL(store);
|
||||
|
||||
/* Find the region id whose name is src/alpha. */
|
||||
char *json = cbm_layout_regions_json(store, "regions-test");
|
||||
ASSERT_NOT_NULL(json);
|
||||
yyjson_doc *doc = yyjson_read(json, strlen(json), 0);
|
||||
ASSERT_NOT_NULL(doc);
|
||||
int alpha_id = -1;
|
||||
yyjson_val *regions = yyjson_obj_get(yyjson_doc_get_root(doc), "regions");
|
||||
size_t idx, max;
|
||||
yyjson_val *rv;
|
||||
yyjson_arr_foreach(regions, idx, max, rv) {
|
||||
const char *name = yyjson_get_str(yyjson_obj_get(rv, "name"));
|
||||
if (name && strcmp(name, "src/alpha") == 0)
|
||||
alpha_id = (int)yyjson_get_int(yyjson_obj_get(rv, "id"));
|
||||
}
|
||||
yyjson_doc_free(doc);
|
||||
free(json);
|
||||
ASSERT_GT(alpha_id, -1);
|
||||
|
||||
cbm_layout_result_t *r = cbm_layout_compute_region(store, "regions-test", alpha_id, 100);
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_EQ(r->node_count, 3);
|
||||
ASSERT_EQ(r->total_nodes, 3);
|
||||
for (int i = 0; i < r->node_count; i++)
|
||||
ASSERT_NOT_NULL(strstr(r->nodes[i].file_path, "src/alpha/"));
|
||||
/* Only intra-region edges: the cross call to beta is not present. */
|
||||
for (int e = 0; e < r->edge_count; e++)
|
||||
for (int i = 0; i < r->node_count; i++)
|
||||
if (r->edges[e].target == r->nodes[i].id)
|
||||
ASSERT_NOT_NULL(strstr(r->nodes[i].file_path, "src/alpha/"));
|
||||
cbm_layout_free(r);
|
||||
|
||||
/* Unknown region id → NULL, not a crash. */
|
||||
ASSERT_NULL(cbm_layout_compute_region(store, "regions-test", 99, 100));
|
||||
|
||||
cbm_store_close(store);
|
||||
cbm_layout_regions_cache_clear();
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Suite ────────────────────────────────────────────────────── */
|
||||
|
||||
SUITE(ui) {
|
||||
@@ -886,4 +1038,8 @@ SUITE(ui) {
|
||||
RUN_TEST(layout_null_inputs);
|
||||
RUN_TEST(layout_dead_code_classification);
|
||||
RUN_TEST(layout_coincident_nodes_bounded);
|
||||
|
||||
/* CBM Atlas region level */
|
||||
RUN_TEST(layout_regions_two_communities);
|
||||
RUN_TEST(layout_regions_scope_restricts_nodes);
|
||||
}
|
||||
Reference in new issue
Block a user