Skip to content

Commit dbab4d3

Browse files
DeusDatahalindromegdilla
committed
Wire up risk_labels on trace_path, add --progress CLI flag
risk_labels: adds CRITICAL/HIGH/MEDIUM/LOW classification to BFS results based on hop distance. Opt-in via risk_labels=true. --progress: human-readable indexing progress on stderr. Log sink replaces default output, maps pipeline events to [1/9]..[9/9] phases. Co-Authored-By: halindrome <halindrome@users.noreply.github.com> Co-Authored-By: gdilla <gdilla@users.noreply.github.com>
1 parent 129334a commit dbab4d3

File tree

7 files changed

+288
-12
lines changed

7 files changed

+288
-12
lines changed

Makefile.cbm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ TRACES_SRCS = src/traces/traces.c
194194
WATCHER_SRCS = src/watcher/watcher.c
195195

196196
# CLI module (new)
197-
CLI_SRCS = src/cli/cli.c
197+
CLI_SRCS = src/cli/cli.c src/cli/progress_sink.c
198198

199199
# UI module (graph visualization)
200200
UI_SRCS = \

src/cli/cli.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,7 @@ static const char skill_content[] =
405405
"| Dead code | `search_graph(max_degree=0, exclude_entry_points=true)` |\n"
406406
"| Cross-service edges | `query_graph` with Cypher |\n"
407407
"| Impact of local changes | `detect_changes()` |\n"
408+
"| Risk-classified trace | `trace_path(risk_labels=true)` |\n"
408409
"| Text search | `search_code` or Grep |\n"
409410
"\n"
410411
"## Exploration Workflow\n"

src/cli/progress_sink.c

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
/*
2+
* progress_sink.c — Human-readable progress for --progress CLI flag.
3+
*
4+
* Maps structured log events (msg=pass.timing, msg=pipeline.done, etc.)
5+
* to phase labels on stderr. When installed, replaces default log output.
6+
*
7+
* Thread-safe: fprintf has per-FILE* locking on POSIX.
8+
*/
9+
#include "progress_sink.h"
10+
#include "foundation/constants.h"
11+
#include "foundation/log.h"
12+
13+
#include <stdatomic.h>
14+
#include <stdio.h>
15+
#include <stdlib.h>
16+
#include <string.h>
17+
18+
enum { PERCENT = 100, NOT_SET = -1 };
19+
20+
static FILE *s_out;
21+
static atomic_int s_needs_newline;
22+
static int s_gbuf_nodes = NOT_SET;
23+
static int s_gbuf_edges = NOT_SET;
24+
25+
/* Extract value of "key=VALUE" from a structured log line. */
26+
static const char *extract_kv(const char *line, const char *key, char *buf, int buf_len) {
27+
if (!line || !key || !buf || buf_len <= 0) {
28+
return NULL;
29+
}
30+
size_t klen = strlen(key);
31+
const char *p = line;
32+
while (*p) {
33+
if ((p == line || p[-SKIP_ONE] == ' ') && strncmp(p, key, klen) == 0 && p[klen] == '=') {
34+
const char *val = p + klen + SKIP_ONE;
35+
int i = 0;
36+
while (val[i] && val[i] != ' ' && i < buf_len - SKIP_ONE) {
37+
buf[i] = val[i];
38+
i++;
39+
}
40+
buf[i] = '\0';
41+
return buf;
42+
}
43+
p++;
44+
}
45+
return NULL;
46+
}
47+
48+
void cbm_progress_sink_init(FILE *out) {
49+
s_out = out ? out : stderr;
50+
atomic_store(&s_needs_newline, 0);
51+
s_gbuf_nodes = NOT_SET;
52+
s_gbuf_edges = NOT_SET;
53+
cbm_log_set_sink(cbm_progress_sink_fn);
54+
}
55+
56+
void cbm_progress_sink_fini(void) {
57+
if (atomic_load(&s_needs_newline) && s_out) {
58+
(void)fprintf(s_out, "\n");
59+
(void)fflush(s_out);
60+
}
61+
cbm_log_set_sink(NULL);
62+
s_out = NULL;
63+
}
64+
65+
/* Phase label table: maps pass names to display labels. */
66+
typedef struct {
67+
const char *pass;
68+
const char *label;
69+
} phase_t;
70+
71+
static const phase_t phases[] = {
72+
{"parallel_extract", "[2/9] Extracting definitions"},
73+
{"registry_build", "[3/9] Building registry"},
74+
{"parallel_resolve", "[4/9] Resolving calls & edges"},
75+
{"tests", "[5/9] Detecting tests"},
76+
{"httplinks", "[6/9] Scanning HTTP links"},
77+
{"githistory_compute", "[7/9] Analyzing git history"},
78+
{"configlink", "[8/9] Linking config files"},
79+
{"dump", "[9/9] Writing database"},
80+
};
81+
82+
enum { PHASE_COUNT = sizeof(phases) / sizeof(phases[0]) };
83+
84+
/* Flush pending \r line if needed. */
85+
static void flush_carriage(void) {
86+
if (atomic_load(&s_needs_newline)) {
87+
(void)fprintf(s_out, "\n");
88+
atomic_store(&s_needs_newline, 0);
89+
}
90+
}
91+
92+
/* Handle pipeline.discover event. */
93+
static void on_discover(const char *line) {
94+
char files[CBM_SZ_32] = {0};
95+
if (extract_kv(line, "files", files, (int)sizeof(files))) {
96+
(void)fprintf(s_out, " Discovering files (%s found)\n", files);
97+
} else {
98+
(void)fprintf(s_out, " Discovering files...\n");
99+
}
100+
(void)fflush(s_out);
101+
}
102+
103+
/* Handle pipeline.route event. */
104+
static void on_route(const char *line) {
105+
char val[CBM_SZ_32] = {0};
106+
const char *path = extract_kv(line, "path", val, (int)sizeof(val));
107+
if (path && strcmp(path, "incremental") == 0) {
108+
(void)fprintf(s_out, " Starting incremental index\n");
109+
} else {
110+
(void)fprintf(s_out, " Starting full index\n");
111+
}
112+
(void)fflush(s_out);
113+
}
114+
115+
/* Handle pass.start event. */
116+
static void on_pass_start(const char *line) {
117+
char val[CBM_SZ_64] = {0};
118+
const char *pass = extract_kv(line, "pass", val, (int)sizeof(val));
119+
if (pass && strcmp(pass, "structure") == 0) {
120+
(void)fprintf(s_out, "[1/9] Building file structure\n");
121+
(void)fflush(s_out);
122+
}
123+
}
124+
125+
/* Handle pass.timing event. */
126+
static void on_pass_timing(const char *line) {
127+
char val[CBM_SZ_64] = {0};
128+
const char *pass = extract_kv(line, "pass", val, (int)sizeof(val));
129+
if (!pass) {
130+
return;
131+
}
132+
flush_carriage();
133+
for (int i = 0; i < PHASE_COUNT; i++) {
134+
if (strcmp(pass, phases[i].pass) == 0) {
135+
(void)fprintf(s_out, "%s\n", phases[i].label);
136+
(void)fflush(s_out);
137+
return;
138+
}
139+
}
140+
}
141+
142+
/* Handle gbuf.dump event — capture node/edge counts. */
143+
static void on_gbuf_dump(const char *line) {
144+
char n[CBM_SZ_32] = {0};
145+
char e[CBM_SZ_32] = {0};
146+
if (extract_kv(line, "nodes", n, (int)sizeof(n))) {
147+
s_gbuf_nodes = (int)strtol(n, NULL, CBM_DECIMAL_BASE);
148+
}
149+
if (extract_kv(line, "edges", e, (int)sizeof(e))) {
150+
s_gbuf_edges = (int)strtol(e, NULL, CBM_DECIMAL_BASE);
151+
}
152+
}
153+
154+
/* Handle pipeline.done event. */
155+
static void on_done(const char *line) {
156+
flush_carriage();
157+
char ms[CBM_SZ_32] = {0};
158+
const char *elapsed = extract_kv(line, "elapsed_ms", ms, (int)sizeof(ms));
159+
if (s_gbuf_nodes >= 0 && s_gbuf_edges >= 0 && elapsed) {
160+
(void)fprintf(s_out, "Done: %d nodes, %d edges (%s ms)\n", s_gbuf_nodes, s_gbuf_edges,
161+
elapsed);
162+
} else if (s_gbuf_nodes >= 0 && s_gbuf_edges >= 0) {
163+
(void)fprintf(s_out, "Done: %d nodes, %d edges\n", s_gbuf_nodes, s_gbuf_edges);
164+
} else {
165+
(void)fprintf(s_out, "Done.\n");
166+
}
167+
(void)fflush(s_out);
168+
}
169+
170+
/* Handle parallel.extract.progress event — in-place counter. */
171+
static void on_extract_progress(const char *line) {
172+
char done[CBM_SZ_32] = {0};
173+
char total[CBM_SZ_32] = {0};
174+
if (extract_kv(line, "done", done, (int)sizeof(done)) &&
175+
extract_kv(line, "total", total, (int)sizeof(total))) {
176+
long d = strtol(done, NULL, CBM_DECIMAL_BASE);
177+
long t = strtol(total, NULL, CBM_DECIMAL_BASE);
178+
int pct = (t > 0) ? (int)((d * PERCENT) / t) : 0;
179+
(void)fprintf(s_out, "\r Extracting: %ld/%ld files (%d%%)", d, t, pct);
180+
(void)fflush(s_out);
181+
atomic_store(&s_needs_newline, SKIP_ONE);
182+
}
183+
}
184+
185+
/* Event dispatch table. */
186+
typedef struct {
187+
const char *msg;
188+
void (*handler)(const char *line);
189+
} event_handler_t;
190+
191+
static const event_handler_t handlers[] = {
192+
{"pipeline.discover", on_discover},
193+
{"pipeline.route", on_route},
194+
{"pass.start", on_pass_start},
195+
{"pass.timing", on_pass_timing},
196+
{"gbuf.dump", on_gbuf_dump},
197+
{"pipeline.done", on_done},
198+
{"parallel.extract.progress", on_extract_progress},
199+
};
200+
201+
enum { HANDLER_COUNT = sizeof(handlers) / sizeof(handlers[0]) };
202+
203+
void cbm_progress_sink_fn(const char *line) {
204+
if (!line || !s_out) {
205+
return;
206+
}
207+
char msg[CBM_SZ_64] = {0};
208+
if (!extract_kv(line, "msg", msg, (int)sizeof(msg))) {
209+
return;
210+
}
211+
for (int i = 0; i < HANDLER_COUNT; i++) {
212+
if (strcmp(msg, handlers[i].msg) == 0) {
213+
handlers[i].handler(line);
214+
return;
215+
}
216+
}
217+
}

src/cli/progress_sink.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* progress_sink.h — Human-readable progress output for --progress CLI flag.
3+
*
4+
* Installs a log sink that maps structured pipeline events to phase labels.
5+
* Usage:
6+
* cbm_progress_sink_init(stderr);
7+
* // ... run pipeline ...
8+
* cbm_progress_sink_fini();
9+
*/
10+
#ifndef CBM_PROGRESS_SINK_H
11+
#define CBM_PROGRESS_SINK_H
12+
13+
#include <stdio.h>
14+
15+
void cbm_progress_sink_init(FILE *out);
16+
void cbm_progress_sink_fini(void);
17+
void cbm_progress_sink_fn(const char *line);
18+
19+
#endif

src/foundation/log.c

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,12 @@ void cbm_log(CBMLogLevel level, const char *msg, ...) {
6565
}
6666
va_end(args);
6767

68-
/* Write to stderr */
69-
(void)fprintf(stderr, "%s\n", line_buf);
70-
71-
/* Send to sink if registered */
68+
/* When a sink is registered it takes over all output (exclusive).
69+
* Otherwise write structured log to stderr. */
7270
if (g_log_sink) {
7371
g_log_sink(line_buf);
72+
} else {
73+
(void)fprintf(stderr, "%s\n", line_buf);
7474
}
7575
}
7676

@@ -83,9 +83,9 @@ void cbm_log_int(CBMLogLevel level, const char *msg, const char *key, int64_t va
8383
snprintf(line_buf, sizeof(line_buf), "level=%s msg=%s %s=%" PRId64, level_str(level),
8484
msg ? msg : "", key ? key : "?", value);
8585

86-
(void)fprintf(stderr, "%s\n", line_buf);
87-
8886
if (g_log_sink) {
8987
g_log_sink(line_buf);
88+
} else {
89+
(void)fprintf(stderr, "%s\n", line_buf);
9090
}
9191
}

src/main.c

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include "pipeline/pipeline.h"
1919
#include "store/store.h"
2020
#include "cli/cli.h"
21+
#include "cli/progress_sink.h"
2122
#include "foundation/constants.h"
2223

2324
enum {
@@ -117,16 +118,43 @@ static int watcher_index_fn(const char *project_name, const char *root_path, voi
117118

118119
static int run_cli(int argc, char **argv) {
119120
if (argc < MAIN_MIN_ARGC) {
120-
(void)fprintf(stderr, "Usage: codebase-memory-mcp cli <tool_name> [json_args]\n");
121+
(void)fprintf(stderr,
122+
"Usage: codebase-memory-mcp cli [--progress] <tool_name> [json_args]\n");
123+
return SKIP_ONE;
124+
}
125+
126+
/* Strip --progress flag from argv. */
127+
bool progress = false;
128+
for (int i = 0; i < argc; i++) {
129+
if (strcmp(argv[i], "--progress") == 0) {
130+
progress = true;
131+
for (int j = i; j < argc - SKIP_ONE; j++) {
132+
argv[j] = argv[j + SKIP_ONE];
133+
}
134+
argc--;
135+
break;
136+
}
137+
}
138+
139+
if (argc < MAIN_MIN_ARGC) {
140+
(void)fprintf(stderr,
141+
"Usage: codebase-memory-mcp cli [--progress] <tool_name> [json_args]\n");
121142
return SKIP_ONE;
122143
}
123144

124145
const char *tool_name = argv[0];
125146
const char *args_json = argc >= MAIN_CLI_ARGC ? argv[SKIP_ONE] : "{}";
126147

148+
if (progress) {
149+
cbm_progress_sink_init(stderr);
150+
}
151+
127152
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
128153
if (!srv) {
129154
(void)fprintf(stderr, "Failed to create server\n");
155+
if (progress) {
156+
cbm_progress_sink_fini();
157+
}
130158
return SKIP_ONE;
131159
}
132160

@@ -137,6 +165,9 @@ static int run_cli(int argc, char **argv) {
137165
}
138166

139167
cbm_mcp_server_free(srv);
168+
if (progress) {
169+
cbm_progress_sink_fini();
170+
}
140171
return 0;
141172
}
142173

0 commit comments

Comments
 (0)