mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
feat(atlas): the handout — a shareable codebase document from the graph
GET /api/handout renders a self-contained, print-friendly HTML
document that explains the project to a newcomer: the region map
(cohesion as words, hubs), boundary spanners with a why-you-care
line, the main entry-to-effect journeys, and the churn x complexity
table. Linked from the Overview header ('Handout').
Honesty properties, tested: every graph-derived string is
HTML-escaped; no host paths leak into the artifact; caps are
declared inline ('20 of 33 regions shown', '10 of 60 journeys');
a project with no readable git history says so instead of skipping
silently; every fact is deterministic - the header states that no
model wrote any of it.
Exposing the same document as an MCP resource is recorded as a
follow-up (touches the MCP protocol surface; wants its own review).
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
1 parent
d8f95946c2
commit
e44ef0a917
7 files changed
+378
-3
No files matched your search
@@ -390,6 +390,7 @@ UI_SRCS = \
|
||||
src/ui/layout_regions.c \
|
||||
src/ui/atlas_api.c \
|
||||
src/ui/atlas_flows.c \
|
||||
src/ui/atlas_handout.c \
|
||||
src/ui/atlas_metrics.c \
|
||||
src/ui/httpd.c \
|
||||
src/ui/embedded_stub.c
|
||||
|
||||
@@ -252,6 +252,18 @@ export function OverviewTab({
|
||||
return (
|
||||
<ScrollArea className="h-full">
|
||||
<div className="max-w-[1200px] mx-auto px-6 py-6">
|
||||
<div className="flex justify-end mb-2">
|
||||
<a
|
||||
href={`/api/handout?${new URLSearchParams({ project })}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-[12px] text-foreground/45 hover:text-primary transition-colors"
|
||||
title="A self-contained, shareable document explaining this codebase — generated from the graph, print-friendly"
|
||||
>
|
||||
Handout ↗
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* The four slots: is anything wrong? */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-5">
|
||||
<Slot
|
||||
|
||||
@@ -55,6 +55,8 @@ char *cbm_atlas_file_history_json(cbm_store_t *store, const char *project, const
|
||||
* Reachability between two callables over CALLS ("calls") or DATA_FLOWS
|
||||
* ("data"): shortest path within a bounded depth, honest about the bound.
|
||||
* Endpoints resolve by node id (>=0) or qualified name. */
|
||||
char *cbm_atlas_handout_html(cbm_store_t *store, const char *project);
|
||||
|
||||
char *cbm_atlas_blast_json(cbm_store_t *store, const char *project, const char *files_csv);
|
||||
|
||||
char *cbm_atlas_bridges_json(cbm_store_t *store, const char *project);
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
/* atlas_handout.c — the codebase handout: a self-contained HTML document
|
||||
* that explains a project to a newcomer, generated purely from the graph.
|
||||
*
|
||||
* Every fact is deterministic (regions, spanners, flows, churn×complexity);
|
||||
* no model-written prose. Sections that would only say "none" for this
|
||||
* project are omitted, caps are declared inline, and no host paths leak
|
||||
* into the artifact (it is meant to be shared). */
|
||||
|
||||
#include "foundation/constants.h"
|
||||
#include "ui/atlas.h"
|
||||
#include "ui/layout3d.h"
|
||||
|
||||
#include <yyjson/yyjson.h>
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
/* ── Growable output buffer ─────────────────────────────────────── */
|
||||
|
||||
typedef struct {
|
||||
char *data;
|
||||
size_t len;
|
||||
size_t cap;
|
||||
bool oom;
|
||||
} hd_buf_t;
|
||||
|
||||
static void hd_put(hd_buf_t *b, const char *s) {
|
||||
if (b->oom || !s)
|
||||
return;
|
||||
size_t n = strlen(s);
|
||||
if (b->len + n + 1 > b->cap) {
|
||||
size_t nc = b->cap ? b->cap * 2 : 16384;
|
||||
while (nc < b->len + n + 1)
|
||||
nc *= 2;
|
||||
char *grown = realloc(b->data, nc);
|
||||
if (!grown) {
|
||||
b->oom = true;
|
||||
return;
|
||||
}
|
||||
b->data = grown;
|
||||
b->cap = nc;
|
||||
}
|
||||
memcpy(b->data + b->len, s, n);
|
||||
b->len += n;
|
||||
b->data[b->len] = '\0';
|
||||
}
|
||||
|
||||
static void hd_putf(hd_buf_t *b, const char *fmt, ...) {
|
||||
char tmp[CBM_SZ_2K];
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(tmp, sizeof(tmp), fmt, ap);
|
||||
va_end(ap);
|
||||
hd_put(b, tmp);
|
||||
}
|
||||
|
||||
/* HTML-escape into the buffer — every graph-derived string passes here. */
|
||||
static void hd_esc(hd_buf_t *b, const char *s) {
|
||||
if (!s)
|
||||
return;
|
||||
for (const char *c = s; *c; c++) {
|
||||
switch (*c) {
|
||||
case '&':
|
||||
hd_put(b, "&");
|
||||
break;
|
||||
case '<':
|
||||
hd_put(b, "<");
|
||||
break;
|
||||
case '>':
|
||||
hd_put(b, ">");
|
||||
break;
|
||||
case '"':
|
||||
hd_put(b, """);
|
||||
break;
|
||||
default: {
|
||||
char one[2] = {*c, 0};
|
||||
hd_put(b, one);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static const char *hd_cohesion_word(double cohesion) {
|
||||
if (cohesion >= 0.7)
|
||||
return "tightly connected";
|
||||
if (cohesion >= 0.4)
|
||||
return "moderately connected";
|
||||
return "loosely connected";
|
||||
}
|
||||
|
||||
/* ── The document ───────────────────────────────────────────────── */
|
||||
|
||||
char *cbm_atlas_handout_html(cbm_store_t *store, const char *project) {
|
||||
if (!store || !project)
|
||||
return NULL;
|
||||
|
||||
char *regions_json = cbm_layout_regions_json(store, project);
|
||||
char *metrics_json = cbm_atlas_metrics_json(store, project);
|
||||
char *flows_json = cbm_atlas_flows_json(store, project);
|
||||
char *bridges_json = cbm_atlas_bridges_json(store, project);
|
||||
|
||||
yyjson_doc *regions_doc =
|
||||
regions_json ? yyjson_read(regions_json, strlen(regions_json), 0) : NULL;
|
||||
yyjson_doc *metrics_doc =
|
||||
metrics_json ? yyjson_read(metrics_json, strlen(metrics_json), 0) : NULL;
|
||||
yyjson_doc *flows_doc = flows_json ? yyjson_read(flows_json, strlen(flows_json), 0) : NULL;
|
||||
yyjson_doc *bridges_doc =
|
||||
bridges_json ? yyjson_read(bridges_json, strlen(bridges_json), 0) : NULL;
|
||||
|
||||
hd_buf_t b = {0};
|
||||
hd_put(&b, "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n"
|
||||
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n");
|
||||
hd_put(&b, "<title>");
|
||||
hd_esc(&b, project);
|
||||
hd_put(&b, " — codebase handout</title>\n<style>\n"
|
||||
"body{margin:0;background:#FBFCFD;color:#1A2128;font:15px/1.55 -apple-system,"
|
||||
"'Segoe UI',Roboto,Helvetica,Arial,sans-serif;}\n"
|
||||
"main{max-width:860px;margin:0 auto;padding:40px 24px 80px;}\n"
|
||||
"h1{font-size:26px;line-height:1.15;margin:0 0 6px;}\n"
|
||||
"h2{font-size:18px;margin:36px 0 10px;border-bottom:1px solid #D7DEE4;"
|
||||
"padding-bottom:5px;}\n"
|
||||
"p{margin:0 0 12px;}\n"
|
||||
".muted{color:#5C6870;font-size:13px;}\n"
|
||||
".accent{color:#1E7FB8;}\n"
|
||||
"table{border-collapse:collapse;width:100%;font-size:13.5px;margin:10px 0 4px;}\n"
|
||||
"th{font-size:11.5px;text-transform:uppercase;letter-spacing:.06em;"
|
||||
"color:#5C6870;text-align:left;padding:6px 10px;border-bottom:1px solid #C9D1D8;}\n"
|
||||
"td{padding:5px 10px;border-bottom:1px solid #E4E9EE;vertical-align:top;}\n"
|
||||
"td.num{text-align:right;font-variant-numeric:tabular-nums;"
|
||||
"font-family:ui-monospace,Menlo,Consolas,monospace;font-size:12.5px;}\n"
|
||||
"code{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:.9em;"
|
||||
"background:#EEF2F5;padding:1px 4px;border-radius:3px;}\n"
|
||||
".why{color:#5C6870;font-size:13px;margin-top:2px;}\n"
|
||||
"</style>\n</head>\n<body>\n<main>\n");
|
||||
|
||||
/* Header */
|
||||
hd_put(&b, "<h1>");
|
||||
hd_esc(&b, project);
|
||||
hd_put(&b, "</h1>\n<p class=\"muted\">A handout for newcomers, generated from the code "
|
||||
"graph. Every number below is computed from indexed structure — no summaries "
|
||||
"were written by a model. Verify anything with the "
|
||||
"<code>codebase-memory-mcp</code> tools.</p>\n");
|
||||
|
||||
long long total_nodes = 0, unmapped = 0;
|
||||
yyjson_val *regions_arr = NULL;
|
||||
if (regions_doc) {
|
||||
yyjson_val *root = yyjson_doc_get_root(regions_doc);
|
||||
total_nodes = yyjson_get_int(yyjson_obj_get(root, "total_nodes"));
|
||||
unmapped = yyjson_get_int(yyjson_obj_get(root, "unmapped_nodes"));
|
||||
regions_arr = yyjson_obj_get(root, "regions");
|
||||
}
|
||||
if (regions_arr) {
|
||||
hd_putf(&b, "<p class=\"muted\">%lld symbols in %zu regions", total_nodes,
|
||||
yyjson_arr_size(regions_arr));
|
||||
if (unmapped > 0)
|
||||
hd_putf(&b, " (%lld outside any region)", unmapped);
|
||||
hd_put(&b, ".</p>\n");
|
||||
}
|
||||
|
||||
/* The map */
|
||||
if (regions_arr && yyjson_arr_size(regions_arr) > 0) {
|
||||
hd_put(&b, "<h2>The map — regions of related code</h2>\n"
|
||||
"<p class=\"why\">Regions are call communities: code that talks to itself "
|
||||
"far more than to the rest. Names come from the dominant folder; the hub "
|
||||
"is the most-connected symbol inside.</p>\n<table>\n<tr><th>Region</th>"
|
||||
"<th>Hub</th><th>Symbols</th><th>Files</th><th>Cohesion</th></tr>\n");
|
||||
size_t shown = 0;
|
||||
for (size_t i = 0; i < yyjson_arr_size(regions_arr) && shown < 20; i++, shown++) {
|
||||
yyjson_val *rv = yyjson_arr_get(regions_arr, i);
|
||||
hd_put(&b, "<tr><td>");
|
||||
hd_esc(&b, yyjson_get_str(yyjson_obj_get(rv, "name")));
|
||||
hd_put(&b, "</td><td><code>");
|
||||
hd_esc(&b, yyjson_get_str(yyjson_obj_get(rv, "hub")));
|
||||
hd_put(&b, "</code></td>");
|
||||
hd_putf(&b, "<td class=\"num\">%lld</td><td class=\"num\">%lld</td>",
|
||||
(long long)yyjson_get_int(yyjson_obj_get(rv, "members")),
|
||||
(long long)yyjson_get_int(yyjson_obj_get(rv, "files")));
|
||||
double cohesion = yyjson_get_real(yyjson_obj_get(rv, "cohesion"));
|
||||
hd_putf(&b, "<td>%s (%.2f)</td></tr>\n", hd_cohesion_word(cohesion), cohesion);
|
||||
}
|
||||
hd_put(&b, "</table>\n");
|
||||
if (yyjson_arr_size(regions_arr) > shown)
|
||||
hd_putf(&b, "<p class=\"muted\">%zu of %zu regions shown — the rest are smaller.</p>\n",
|
||||
shown, yyjson_arr_size(regions_arr));
|
||||
}
|
||||
|
||||
/* Boundary spanners */
|
||||
yyjson_val *bridges_arr =
|
||||
bridges_doc ? yyjson_obj_get(yyjson_doc_get_root(bridges_doc), "bridges") : NULL;
|
||||
if (bridges_arr && yyjson_arr_size(bridges_arr) > 0) {
|
||||
hd_put(&b, "<h2>Boundary spanners — where regions meet</h2>\n"
|
||||
"<p class=\"why\">These call into the most distinct regions. Changes here "
|
||||
"ripple furthest; mention them when a task spans areas.</p>\n<table>\n"
|
||||
"<tr><th>Symbol</th><th>File</th><th>Regions reached</th>"
|
||||
"<th>Cross calls</th></tr>\n");
|
||||
for (size_t i = 0; i < yyjson_arr_size(bridges_arr); i++) {
|
||||
yyjson_val *rv = yyjson_arr_get(bridges_arr, i);
|
||||
hd_put(&b, "<tr><td><code>");
|
||||
hd_esc(&b, yyjson_get_str(yyjson_obj_get(rv, "name")));
|
||||
hd_put(&b, "</code></td><td>");
|
||||
hd_esc(&b, yyjson_get_str(yyjson_obj_get(rv, "file_path")));
|
||||
hd_putf(&b, "</td><td class=\"num\">%lld</td><td class=\"num\">%lld</td></tr>\n",
|
||||
(long long)yyjson_get_int(yyjson_obj_get(rv, "regions")),
|
||||
(long long)yyjson_get_int(yyjson_obj_get(rv, "cross_calls")));
|
||||
}
|
||||
hd_put(&b, "</table>\n");
|
||||
}
|
||||
|
||||
/* Main journeys */
|
||||
if (flows_doc) {
|
||||
yyjson_val *froot = yyjson_doc_get_root(flows_doc);
|
||||
yyjson_val *flows_arr = yyjson_obj_get(froot, "flows");
|
||||
if (flows_arr && yyjson_arr_size(flows_arr) > 0) {
|
||||
hd_put(&b, "<h2>Main journeys — entry to effect</h2>\n"
|
||||
"<p class=\"why\">Ranked walks from an entry point to a terminal "
|
||||
"effect over resolved calls. Read one before touching the code it "
|
||||
"crosses.</p>\n<table>\n<tr><th>Journey</th><th>Steps</th>"
|
||||
"<th>Scope</th></tr>\n");
|
||||
size_t shown = 0;
|
||||
for (size_t i = 0; i < yyjson_arr_size(flows_arr) && shown < 10; i++, shown++) {
|
||||
yyjson_val *fv = yyjson_arr_get(flows_arr, i);
|
||||
hd_put(&b, "<tr><td><code>");
|
||||
hd_esc(&b, yyjson_get_str(yyjson_obj_get(fv, "label")));
|
||||
hd_putf(&b, "</code></td><td class=\"num\">%lld</td><td>%s</td></tr>\n",
|
||||
(long long)yyjson_get_int(yyjson_obj_get(fv, "steps")),
|
||||
yyjson_get_bool(yyjson_obj_get(fv, "cross_region")) ? "crosses regions"
|
||||
: "one region");
|
||||
}
|
||||
hd_put(&b, "</table>\n");
|
||||
long long callable_total = yyjson_get_int(yyjson_obj_get(froot, "callable_total"));
|
||||
hd_putf(&b,
|
||||
"<p class=\"muted\">%zu of %zu detected journeys shown, walked from %lld "
|
||||
"callables.</p>\n",
|
||||
shown, yyjson_arr_size(flows_arr), callable_total);
|
||||
}
|
||||
}
|
||||
|
||||
/* Risk — churn × complexity */
|
||||
if (metrics_doc) {
|
||||
yyjson_val *mroot = yyjson_doc_get_root(metrics_doc);
|
||||
bool churn_ok = yyjson_get_bool(yyjson_obj_get(mroot, "churn_available"));
|
||||
yyjson_val *risky = yyjson_obj_get(mroot, "top_churn_complex");
|
||||
if (churn_ok && risky && yyjson_arr_size(risky) > 0) {
|
||||
hd_put(&b, "<h2>Where change concentrates — churn × complexity</h2>\n"
|
||||
"<p class=\"why\">Files that are both complicated and edited often: "
|
||||
"the head of this list is where refactoring pays first.</p>\n"
|
||||
"<table>\n<tr><th>File</th><th>Complexity</th><th>Commits (1y)</th>"
|
||||
"</tr>\n");
|
||||
size_t shown = 0;
|
||||
for (size_t i = 0; i < yyjson_arr_size(risky) && shown < 10; i++, shown++) {
|
||||
yyjson_val *rv = yyjson_arr_get(risky, i);
|
||||
hd_put(&b, "<tr><td>");
|
||||
hd_esc(&b, yyjson_get_str(yyjson_obj_get(rv, "file")));
|
||||
hd_putf(&b, "</td><td class=\"num\">%lld</td><td class=\"num\">%lld</td></tr>\n",
|
||||
(long long)yyjson_get_int(yyjson_obj_get(rv, "value")),
|
||||
(long long)yyjson_get_int(yyjson_obj_get(rv, "commits")));
|
||||
}
|
||||
hd_put(&b, "</table>\n");
|
||||
} else if (!churn_ok) {
|
||||
hd_put(&b, "<h2>Where change concentrates</h2>\n<p class=\"muted\">No readable "
|
||||
"git history at the indexed root, so churn cannot be computed — this "
|
||||
"section would be guesswork and is omitted.</p>\n");
|
||||
}
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
{
|
||||
char stamp[64] = {0};
|
||||
time_t now = time(NULL);
|
||||
struct tm tm_utc;
|
||||
#ifdef _WIN32
|
||||
gmtime_s(&tm_utc, &now);
|
||||
#else
|
||||
gmtime_r(&now, &tm_utc);
|
||||
#endif
|
||||
strftime(stamp, sizeof(stamp), "%Y-%m-%d %H:%M UTC", &tm_utc);
|
||||
hd_putf(&b,
|
||||
"<p class=\"muted\" style=\"margin-top:40px\">Generated %s by CBM Atlas from "
|
||||
"the indexed graph. Regenerate after re-indexing; do not edit by hand.</p>\n",
|
||||
stamp);
|
||||
}
|
||||
hd_put(&b, "</main>\n</body>\n</html>\n");
|
||||
|
||||
if (regions_doc)
|
||||
yyjson_doc_free(regions_doc);
|
||||
if (metrics_doc)
|
||||
yyjson_doc_free(metrics_doc);
|
||||
if (flows_doc)
|
||||
yyjson_doc_free(flows_doc);
|
||||
if (bridges_doc)
|
||||
yyjson_doc_free(bridges_doc);
|
||||
free(regions_json);
|
||||
free(metrics_json);
|
||||
free(flows_json);
|
||||
free(bridges_json);
|
||||
|
||||
if (b.oom) {
|
||||
free(b.data);
|
||||
return NULL;
|
||||
}
|
||||
return b.data;
|
||||
}
|
||||
@@ -77,6 +77,7 @@
|
||||
* which makes these statics safe. */
|
||||
static char g_cors[256]; /* CORS headers only */
|
||||
static char g_cors_json[512]; /* CORS + Content-Type: application/json */
|
||||
static char g_cors_html[512]; /* CORS + Content-Type: text/html */
|
||||
|
||||
static bool origin_is_same_server(const char *origin, int port) {
|
||||
char expected[128];
|
||||
@@ -113,6 +114,8 @@ static void update_cors(const cbm_http_req_t *req, int port) {
|
||||
"Access-Control-Allow-Headers: Content-Type\r\n");
|
||||
}
|
||||
snprintf(g_cors_json, sizeof(g_cors_json), "%sContent-Type: application/json\r\n", g_cors);
|
||||
snprintf(g_cors_html, sizeof(g_cors_html), "%sContent-Type: text/html; charset=utf-8\r\n",
|
||||
g_cors);
|
||||
}
|
||||
|
||||
static const char *detect_ui_lang(const char *accept_language) {
|
||||
@@ -1848,6 +1851,22 @@ static void handle_atlas_trace(cbm_http_conn_t *c, const cbm_http_req_t *req) {
|
||||
atlas_reply_json(c, json, 500, "{\"error\":\"trace failed\"}");
|
||||
}
|
||||
|
||||
/* GET /api/handout?project=X — the self-contained newcomer document. */
|
||||
static void handle_atlas_handout(cbm_http_conn_t *c, const cbm_http_req_t *req) {
|
||||
char project[256] = {0};
|
||||
cbm_store_t *store = atlas_open_project(c, req, project, sizeof(project));
|
||||
if (!store)
|
||||
return;
|
||||
char *html = cbm_atlas_handout_html(store, project);
|
||||
cbm_store_close(store);
|
||||
if (!html) {
|
||||
cbm_http_replyf(c, 500, g_cors_json, "{\"error\":\"handout generation failed\"}");
|
||||
return;
|
||||
}
|
||||
cbm_http_replyf(c, 200, g_cors_html, "%s", html);
|
||||
free(html);
|
||||
}
|
||||
|
||||
/* GET /api/blast?project=X&files=a,b,c — bucket a file list by region. */
|
||||
static void handle_atlas_blast(cbm_http_conn_t *c, const cbm_http_req_t *req) {
|
||||
char project[256] = {0};
|
||||
@@ -2214,6 +2233,12 @@ static void dispatch_request(cbm_http_server_t *srv, cbm_http_conn_t *c,
|
||||
return;
|
||||
}
|
||||
|
||||
/* GET /api/handout → CBM Atlas newcomer document */
|
||||
if (is_get && cbm_http_path_match(req->path, "/api/handout*")) {
|
||||
handle_atlas_handout(c, req);
|
||||
return;
|
||||
}
|
||||
|
||||
/* GET /api/blast → CBM Atlas region bucket for a file list */
|
||||
if (is_get && cbm_http_path_match(req->path, "/api/blast*")) {
|
||||
handle_atlas_blast(c, req);
|
||||
|
||||
+3
-3
@@ -1191,9 +1191,9 @@ TEST(ui_server_atlas_routes_are_wired) {
|
||||
* falls through to the static handler's 404. This pins the /api/flow
|
||||
* (singular) route, which once used a "?*" pattern that never matched
|
||||
* because req->path carries no query string. */
|
||||
static const char *const routes[] = {"/api/tree", "/api/symbol", "/api/flows",
|
||||
"/api/flow", "/api/metrics", "/api/trace",
|
||||
"/api/scent", "/api/bridges", "/api/blast"};
|
||||
static const char *const routes[] = {
|
||||
"/api/tree", "/api/symbol", "/api/flows", "/api/flow", "/api/metrics",
|
||||
"/api/trace", "/api/scent", "/api/bridges", "/api/blast", "/api/handout"};
|
||||
char req[256];
|
||||
char resp[4096];
|
||||
for (size_t i = 0; i < sizeof(routes) / sizeof(routes[0]); i++) {
|
||||
|
||||
@@ -1690,6 +1690,33 @@ TEST(atlas_blast_buckets_files_by_region) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(atlas_handout_is_selfcontained_and_escaped) {
|
||||
cbm_layout_regions_cache_clear();
|
||||
cbm_atlas_flows_cache_clear();
|
||||
cbm_atlas_metrics_cache_clear();
|
||||
cbm_store_t *store = atlas_fixture(NULL);
|
||||
ASSERT_NOT_NULL(store);
|
||||
|
||||
char *html = cbm_atlas_handout_html(store, "regions-test");
|
||||
ASSERT_NOT_NULL(html);
|
||||
ASSERT_NOT_NULL(strstr(html, "<!doctype html>"));
|
||||
ASSERT_NOT_NULL(strstr(html, "regions-test"));
|
||||
/* The map renders the fixture regions. */
|
||||
ASSERT_NOT_NULL(strstr(html, "src/alpha"));
|
||||
/* No host paths leak into the shareable artifact. */
|
||||
ASSERT_NULL(strstr(html, "/tmp/"));
|
||||
ASSERT_NULL(strstr(html, "/Users/"));
|
||||
/* Honest churn: fixture root has no git history → the section says so. */
|
||||
ASSERT_NOT_NULL(strstr(html, "No readable git history"));
|
||||
free(html);
|
||||
|
||||
cbm_store_close(store);
|
||||
cbm_atlas_metrics_cache_clear();
|
||||
cbm_atlas_flows_cache_clear();
|
||||
cbm_layout_regions_cache_clear();
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Suite ────────────────────────────────────────────────────── */
|
||||
|
||||
SUITE(ui) {
|
||||
@@ -1733,4 +1760,5 @@ SUITE(ui) {
|
||||
RUN_TEST(atlas_bridges_rank_by_region_reach);
|
||||
RUN_TEST(atlas_symbol_overflow_tail_and_dataflow_presence);
|
||||
RUN_TEST(atlas_blast_buckets_files_by_region);
|
||||
RUN_TEST(atlas_handout_is_selfcontained_and_escaped);
|
||||
}
|
||||
Reference in new issue
Block a user