Merge pull request #1883 from DeusData/feat/879-importance-distill

feat(pipeline): index-time importance scoring (weighted-degree)
This commit is contained in:
Martin Vogel
2026-08-29 11:32:15 +02:00
committed by GitHub
8 changed files with 1635 additions and 9 deletions
+556
View File
@@ -0,0 +1,556 @@
/*
* pass_importance.c — Index-time per-symbol importance score (weighted degree).
*
* Predump pass. Computes an importance score for every Function/Method/Class
* node and stores it as a numeric "importance" key inside the node's EXISTING
* properties_json blob — no new column, no schema change, and therefore no
* CBM_INDEX_FORMAT_VERSION bump. Indexes written by older builds simply lack
* the key; every consumer must tolerate its absence.
*
* Model (weighted degree, Aider-repomap shaped):
*
* importance = sqrt(num_refs) * priv * generic * distinct * test_penalty
*
* num_refs incoming CALLS + USAGE edges. 0 -> sqrt(0) = 0.
* priv 0.1 name is private (leading underscore)
* generic 0.1 the name is defined in >= 5 DISTINCT files
* distinct 10 name is snake_case or camelCase AND len >= 8
* test_penalty
* 0.1 the symbol is test scaffolding — either the target of an
* incoming TESTS edge (a production helper exercised by a
* test), or it lives in a test file per the graph's own
* canonical cbm_is_test_path() classifier (the same one
* pass_tests uses).
*
* Transitive importance (PageRank) is deliberately NOT built here: weighted
* degree is what this pass claims, and a transitive refinement has to earn
* itself against a fixed judgment set before it ships.
*
* ── Cost ─────────────────────────────────────────────────────────────
* The generic-name multiplier needs |{ file a name is defined in }|. The
* obvious shape — for each node, walk its whole same-name group and
* pairwise-compare file paths — is O(k^2) per node and therefore O(k^3) per
* same-name group of size k. That is not a theoretical concern: on a large
* Java corpus a single name group (`toString`, k≈4.6k) costs ~2 minutes, and
* a handful of such names alone multiplies total index time several-fold.
*
* This implementation is linear in the graph instead:
* - the distinct-file count is computed ONCE PER DISTINCT NAME and memoized
* (g_imp_name_counts), so a group of size k is scanned once, not k times;
* - within a group, file paths are deduplicated through a hash set rather
* than pairwise strcmp.
* Total distinct-file work is therefore Σ over distinct names of k = O(N).
* Both gbuf lookups it rests on (find_by_name, find_edges_by_target_type) are
* hash-indexed, so the per-node loop is O(1) amortised per node.
*
* g_importance_name_visits counts same-name-group member visits and is GATED
* in the complexity suite: it must stay linear (ratio ~2) under a corpus
* doubling. A regression to the per-node scan lands at ~4 and fails the gate.
*
* ── Ordering (load-bearing) ──────────────────────────────────────────
* Registered LAST among the predump passes, and last in the incremental
* post-pass sequence, so every edge type the score reads (CALLS, USAGE from
* extraction; TESTS from pass_tests) already exists. A pass ordered earlier
* would silently score everything at num_refs = 0 with no test penalty.
*/
#include "foundation/constants.h"
#include "pipeline/pipeline.h"
#include "pipeline/pipeline_internal.h"
#include "graph_buffer/graph_buffer.h"
#include "foundation/hash_table.h"
#include "foundation/log.h"
#include "foundation/compat.h"
#include "store/store.h"
#include "cbm.h"
#include "sqlite3.h"
#include <ctype.h>
#include <math.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stdbool.h>
/* Work counters — deltas are read by the complexity suite's linearity gate.
* Never reset here: callers snapshot before/after so nested runs compose. */
_Atomic uint64_t g_importance_nodes = 0;
_Atomic uint64_t g_importance_name_visits = 0;
_Atomic uint64_t g_importance_store_rows = 0;
enum {
CBM_IMPORTANCE_GENERIC_MIN_FILES = 5, /* name defined in >= N files -> generic */
CBM_IMPORTANCE_DISTINCT_MIN_LEN = 8, /* distinctive-identifier length floor */
};
static const double CBM_IMPORTANCE_PRIV_MUL = 0.1;
static const double CBM_IMPORTANCE_GENERIC_MUL = 0.1;
static const double CBM_IMPORTANCE_DISTINCT_MUL = 10.0;
static const double CBM_IMPORTANCE_TEST_MUL = 0.1;
/* The node labels that carry an importance score. File/Module and other
* non-symbol nodes are deliberately excluded. */
static const char *const CBM_IMPORTANCE_LABELS[] = {"Function", "Method", "Class"};
enum { CBM_IMPORTANCE_LABEL_COUNT = 3 };
/* Int → string for structured logging (thread-safe ring buffer). */
static const char *itoa_imp(uint64_t val) {
enum { RING = 2, MASK = 1 };
static CBM_TLS char bufs[RING][CBM_SZ_32];
static CBM_TLS int idx = 0;
int i = idx;
idx = (idx + 1) & MASK;
snprintf(bufs[i], sizeof(bufs[i]), "%llu", (unsigned long long)val);
return bufs[i];
}
static bool name_is_private(const char *name) {
return name != NULL && name[0] == '_';
}
/* Distinctive = (snake_case OR camelCase) AND length >= 8. snake_case is an
* embedded '_'; camelCase is a lower->upper "hump". A plain len>=8 lowercase
* word (no '_', no hump) is NOT distinctive. */
static bool name_is_distinctive(const char *name) {
if (!name) {
return false;
}
size_t len = strlen(name);
if (len < (size_t)CBM_IMPORTANCE_DISTINCT_MIN_LEN) {
return false;
}
if (strchr(name, '_') != NULL) {
return true; /* snake_case */
}
for (size_t i = 1; i < len; i++) {
if (islower((unsigned char)name[i - 1]) && isupper((unsigned char)name[i])) {
return true; /* camelCase hump */
}
}
return false;
}
/* ── JSON property write-back ─────────────────────────────────────────
*
* properties_json is a flat object here (same assumption the sibling passes'
* json_get_int/json_get_bool helpers make), but the value scanner below is
* written to survive nested values so a future richer blob cannot be
* truncated by an in-place overwrite. */
/* Return a pointer just past the JSON value beginning at p (first non-space
* char of the value). NULL if the value is unterminated/malformed. */
static const char *imp_value_end(const char *p) {
if (*p == '"') {
p++;
while (*p) {
if (*p == '\\' && p[1]) {
p += 2;
continue;
}
if (*p == '"') {
return p + 1;
}
p++;
}
return NULL;
}
if (*p == '{' || *p == '[') {
int depth = 0;
bool in_str = false;
while (*p) {
if (in_str) {
if (*p == '\\' && p[1]) {
p += 2;
continue;
}
if (*p == '"') {
in_str = false;
}
} else if (*p == '"') {
in_str = true;
} else if (*p == '{' || *p == '[') {
depth++;
} else if (*p == '}' || *p == ']') {
depth--;
if (depth == 0) {
return p + 1;
}
}
p++;
}
return NULL;
}
while (*p && *p != ',' && *p != '}' && *p != ']') {
p++;
}
return p;
}
/* Find `"key"` where it is genuinely in KEY position: preceded (modulo
* whitespace) by '{' or ',' and followed (modulo whitespace) by ':'. A bare
* strstr would also match the text inside a string VALUE. Returns a pointer
* to the opening quote, or NULL. */
static const char *imp_find_key(const char *json, const char *key) {
char pat[CBM_SZ_64];
int n = snprintf(pat, sizeof(pat), "\"%s\"", key);
if (n < 0 || (size_t)n >= sizeof(pat)) {
return NULL;
}
const char *p = json;
while ((p = strstr(p, pat)) != NULL) {
const char *before = p;
while (before > json && isspace((unsigned char)before[-1])) {
before--;
}
char prev = (before > json) ? before[-1] : '\0';
const char *after = p + n;
while (isspace((unsigned char)*after)) {
after++;
}
if ((prev == '{' || prev == ',') && *after == ':') {
return p;
}
p += n;
}
return NULL;
}
/* Produce a copy of `json` carrying the numeric "importance" key set to
* `score`, or NULL if `json` is not a JSON object (caller leaves it alone) or
* on allocation failure. Caller owns the result.
*
* IDEMPOTENT BY CONTRACT. Nodes reach this function already carrying an
* "importance" key on every re-scoring route: the legacy-partial incremental
* path rehydrates them via cbm_gbuf_load_from_db, and the store-level
* recompute reads them straight out of SQLite. A pure append would emit
* {"importance":1.0,...,"importance":2.0} — silent property corruption that no
* build or schema check would catch. So: overwrite an existing key in place,
* append only when the key is genuinely absent.
*
* SINGLE DEFINITION. Both scoring routes (the in-memory gbuf pass and the
* SQL-level store recompute) write through this one function, so the JSON
* shape cannot drift between them. */
char *cbm_pipeline_importance_set_prop(const char *json, double score) {
const char *old = json ? json : "{}";
size_t olen = strlen(old);
if (olen < 2 || old[olen - 1] != '}') {
return NULL; /* not a JSON object — leave untouched */
}
char val[CBM_SZ_32];
int vn = snprintf(val, sizeof(val), "%.6f", score);
if (vn < 0 || (size_t)vn >= sizeof(val)) {
return NULL;
}
const char *k = imp_find_key(old, "importance");
if (k) {
const char *colon = strchr(k, ':');
if (!colon) {
return NULL; /* imp_find_key guarantees one, but never trust a raw scan */
}
const char *v = colon + 1;
while (isspace((unsigned char)*v)) {
v++;
}
const char *vend = imp_value_end(v);
if (!vend) {
return NULL;
}
size_t head = (size_t)(v - old);
size_t tail = strlen(vend);
char *neu = malloc(head + (size_t)vn + tail + 1);
if (!neu) {
return NULL;
}
memcpy(neu, old, head);
memcpy(neu + head, val, (size_t)vn);
memcpy(neu + head + (size_t)vn, vend, tail + 1);
return neu;
}
char frag[CBM_SZ_64];
int fn = snprintf(frag, sizeof(frag), "%s\"importance\":%s}", (olen == 2) ? "" : ",", val);
if (fn < 0 || (size_t)fn >= sizeof(frag)) {
return NULL;
}
char *neu = malloc(olen - 1 + (size_t)fn + 1);
if (!neu) {
return NULL;
}
memcpy(neu, old, olen - 1); /* copy without the trailing '}' */
memcpy(neu + olen - 1, frag, (size_t)fn + 1);
return neu;
}
/* gbuf-node convenience wrapper around cbm_pipeline_importance_set_prop. */
void cbm_pipeline_importance_append_prop(cbm_gbuf_node_t *node, double score) {
if (!node) {
return;
}
char *neu = cbm_pipeline_importance_set_prop(node->properties_json, score);
if (!neu) {
return;
}
free(node->properties_json);
node->properties_json = neu;
}
/* ── The scoring rule ─────────────────────────────────────────────────
*
* SINGLE DEFINITION, deliberately. There are two routes that score nodes —
* the in-memory gbuf pass (full index, legacy-partial incremental) and the
* SQL-level recompute over the staging store (closure-delta incremental) —
* and they gather their inputs very differently. Only the GATHERING differs;
* the rule itself lives here alone, so the two routes cannot drift apart in
* what a score means. The equivalence test in tests/test_importance.c guards
* the remaining freedom: that both gatherings produce the same inputs for the
* same graph. */
double cbm_pipeline_importance_score(const cbm_importance_inputs_t *in) {
if (!in) {
return 0.0;
}
double score = sqrt((double)(in->num_refs > 0 ? in->num_refs : 0));
if (name_is_private(in->name)) {
score *= CBM_IMPORTANCE_PRIV_MUL;
}
if (in->name_distinct_files >= CBM_IMPORTANCE_GENERIC_MIN_FILES) {
score *= CBM_IMPORTANCE_GENERIC_MUL;
}
if (name_is_distinctive(in->name)) {
score *= CBM_IMPORTANCE_DISTINCT_MUL;
}
if (cbm_is_test_path(in->file_path) || in->tests_target) {
score *= CBM_IMPORTANCE_TEST_MUL;
}
return score;
}
/* ── Distinct-file counting (the linear core) ─────────────────────────── */
typedef struct {
CBMHashTable *counts; /* name -> (void *)(intptr_t)(distinct_files + 1) */
CBMHashTable *paths; /* scratch file-path set, cleared between names */
} imp_name_index_t;
/* Distinct files a name is DEFINED in. Counts distinct file paths, not
* distinct nodes — two defs of one name in the same file count once.
* Computed once per distinct name; every later node in the group is a hash
* hit. Name keys are borrowed from the gbuf nodes, which outlive the pass. */
static int imp_distinct_file_count(const cbm_gbuf_t *gb, const char *name, imp_name_index_t *ix) {
if (!name || !*name || !ix || !ix->counts) {
return 0;
}
void *cached = cbm_ht_get(ix->counts, name);
if (cached) {
return (int)((intptr_t)cached - 1);
}
const cbm_gbuf_node_t **nodes = NULL;
int count = 0;
int distinct = 0;
if (cbm_gbuf_find_by_name(gb, name, &nodes, &count) == 0 && count > 0) {
atomic_fetch_add_explicit(&g_importance_name_visits, (uint64_t)count, memory_order_relaxed);
if (ix->paths) {
cbm_ht_clear(ix->paths);
for (int i = 0; i < count; i++) {
const char *fp = nodes[i]->file_path;
if (!fp || cbm_ht_has(ix->paths, fp)) {
continue;
}
cbm_ht_set(ix->paths, fp, (void *)(intptr_t)1);
distinct++;
}
} else {
/* Scratch set unavailable (allocation failure): the generic-name
* multiplier degrades to "never generic" rather than falling back
* to a pairwise scan — a silent quadratic is worse than a missing
* 0.1 multiplier. */
distinct = 0;
}
}
cbm_ht_set(ix->counts, name, (void *)(intptr_t)(distinct + 1));
return distinct;
}
static int incoming_edge_count(const cbm_gbuf_t *gb, int64_t id, const char *type) {
const cbm_gbuf_edge_t **edges = NULL;
int ne = 0;
if (cbm_gbuf_find_edges_by_target_type(gb, id, type, &edges, &ne) != 0) {
return 0;
}
return ne;
}
void cbm_pipeline_pass_importance(cbm_pipeline_ctx_t *ctx) {
if (!ctx || !ctx->gbuf) {
return;
}
cbm_gbuf_t *gb = ctx->gbuf;
/* Modest reservations, deliberately not sized from node_count: the memo
* holds one entry per DISTINCT name, far fewer than nodes, and reserving
* buckets for every node would cost hundreds of MB of transient peak on a
* multi-million-node graph to save a handful of amortised rehashes. */
imp_name_index_t ix = {
.counts = cbm_ht_create(CBM_SZ_4K),
.paths = cbm_ht_create(CBM_SZ_256),
};
if (!ix.counts) {
cbm_ht_free(ix.paths);
cbm_log_error("pass.importance", "msg", "name_index_alloc_failed");
return;
}
uint64_t updated = 0;
for (int li = 0; li < CBM_IMPORTANCE_LABEL_COUNT; li++) {
const cbm_gbuf_node_t **nodes = NULL;
int count = 0;
if (cbm_gbuf_find_by_label(gb, CBM_IMPORTANCE_LABELS[li], &nodes, &count) != 0) {
continue;
}
for (int i = 0; i < count; i++) {
cbm_gbuf_node_t *n = (cbm_gbuf_node_t *)nodes[i];
/* Gather only — the rule itself lives in one place. */
cbm_importance_inputs_t in = {
.name = n->name,
.file_path = n->file_path,
.num_refs = incoming_edge_count(gb, n->id, "CALLS") +
incoming_edge_count(gb, n->id, "USAGE"),
.name_distinct_files = imp_distinct_file_count(gb, n->name, &ix),
.tests_target = incoming_edge_count(gb, n->id, "TESTS") > 0,
};
cbm_pipeline_importance_append_prop(n, cbm_pipeline_importance_score(&in));
updated++;
}
}
cbm_ht_free(ix.paths);
cbm_ht_free(ix.counts);
atomic_fetch_add_explicit(&g_importance_nodes, updated, memory_order_relaxed);
cbm_log_info("pass.importance", "symbols", itoa_imp(updated));
}
/* ── Store-level recompute (closure-delta incremental) ────────────────
*
* WHY THIS EXISTS. The closure-delta route never materialises the project
* graph in RAM: cbm_delta_preseed fills the delta gbuf with *proxy* nodes
* (id, label, name, qn, file_path — no edges, empty properties), and only the
* re-extracted files' symbols carry real edges. Scoring there reads a
* near-zero in-degree for a symbol referenced hundreds of times project-wide,
* and cbm_delta_patch then persists exactly those fresh rows. Loading the full
* graph to fix that would give back the point of the delta route, so the
* recompute runs where the complete graph already exists: in SQL, over the
* staging store, AFTER cbm_delta_patch has inserted the new nodes and
* re-linked the snapshotted inbound edges.
*
* WHY IT CANNOT DRIFT FROM THE IN-MEMORY PASS. The scoring rule and the JSON
* write-back are not reimplemented here. They are registered as SQLite
* user-defined functions that call cbm_pipeline_importance_score() and
* cbm_pipeline_importance_set_prop() — the same C code the gbuf pass runs.
* SQL supplies only the three aggregates (in-degree, TESTS presence, distinct
* files per name), each one indexed GROUP BY. What remains free is the
* GATHERING, and the full-vs-incremental equivalence test guards that.
*
* The whole project is rescored, not just the changed files: distinct-files-
* per-name is a global input, so editing one file can legitimately move a name
* across the generic-name threshold for every other definition of that name. A
* scoped recompute would leave those stale — and would fail the equivalence
* test, correctly. */
/* UDF: cbm_imp_score(name, file_path, num_refs, name_files, tests_target). */
static void sql_importance_score(sqlite3_context *c, int argc, sqlite3_value **argv) {
if (argc != 5) {
sqlite3_result_error(c, "cbm_imp_score arity", -1);
return;
}
cbm_importance_inputs_t in = {
.name = (const char *)sqlite3_value_text(argv[0]),
.file_path = (const char *)sqlite3_value_text(argv[1]),
.num_refs = sqlite3_value_int(argv[2]),
.name_distinct_files = sqlite3_value_int(argv[3]),
.tests_target = sqlite3_value_int(argv[4]) != 0,
};
sqlite3_result_double(c, cbm_pipeline_importance_score(&in));
}
/* UDF: cbm_imp_set(properties, score) → properties with "importance" set.
* A non-object blob yields NULL; the UPDATE's COALESCE then leaves that row
* untouched — the same tolerance the in-memory writer has. */
static void sql_importance_set(sqlite3_context *c, int argc, sqlite3_value **argv) {
if (argc != 2) {
sqlite3_result_error(c, "cbm_imp_set arity", -1);
return;
}
const char *props = (const char *)sqlite3_value_text(argv[0]);
char *neu = cbm_pipeline_importance_set_prop(props, sqlite3_value_double(argv[1]));
if (!neu) {
sqlite3_result_null(c);
return;
}
sqlite3_result_text(c, neu, -1, SQLITE_TRANSIENT);
free(neu);
}
/* The aggregate CTE is MATERIALIZED on purpose: it reads `nodes` while the
* UPDATE writes `nodes`, and materialising settles that ordering explicitly
* rather than relying on the planner to choose it. */
static const char *const kImportanceRecomputeSQL =
"WITH agg AS MATERIALIZED ("
" SELECT n.id AS id,"
" cbm_imp_score(n.name, n.file_path, COALESCE(d.refs, 0),"
" COALESCE(f.files, 0), COALESCE(d.tests, 0)) AS score"
" FROM nodes n"
" LEFT JOIN (SELECT target_id AS id,"
" SUM(CASE WHEN type IN ('CALLS','USAGE') THEN 1 ELSE 0 END) AS refs,"
" SUM(CASE WHEN type = 'TESTS' THEN 1 ELSE 0 END) AS tests"
" FROM edges WHERE project = ?1 GROUP BY target_id) d ON d.id = n.id"
" LEFT JOIN (SELECT name, COUNT(DISTINCT file_path) AS files"
" FROM nodes WHERE project = ?1 GROUP BY name) f ON f.name = n.name"
" WHERE n.project = ?1 AND n.label IN ('Function','Method','Class')"
")"
"UPDATE nodes SET properties = COALESCE(cbm_imp_set(properties, agg.score), properties)"
" FROM agg WHERE nodes.id = agg.id";
int cbm_pipeline_importance_recompute_store(cbm_store_t *store, const char *project) {
if (!store || !project) {
return -1;
}
sqlite3 *db = cbm_store_get_db(store);
if (!db) {
return -1;
}
if (sqlite3_create_function(db, "cbm_imp_score", 5, SQLITE_UTF8 | SQLITE_DETERMINISTIC, NULL,
sql_importance_score, NULL, NULL) != SQLITE_OK ||
sqlite3_create_function(db, "cbm_imp_set", 2, SQLITE_UTF8 | SQLITE_DETERMINISTIC, NULL,
sql_importance_set, NULL, NULL) != SQLITE_OK) {
cbm_log_error("importance.store_udf_failed", "err", sqlite3_errmsg(db));
return -1;
}
if (cbm_store_begin(store) != CBM_STORE_OK) {
return -1;
}
sqlite3_stmt *st = NULL;
if (sqlite3_prepare_v2(db, kImportanceRecomputeSQL, -1, &st, NULL) != SQLITE_OK) {
cbm_log_error("importance.store_prepare_failed", "err", sqlite3_errmsg(db));
cbm_store_rollback(store);
return -1;
}
sqlite3_bind_text(st, 1, project, -1, SQLITE_TRANSIENT);
int step_rc = sqlite3_step(st);
sqlite3_finalize(st);
if (step_rc != SQLITE_DONE) {
cbm_log_error("importance.store_step_failed", "err", sqlite3_errmsg(db));
cbm_store_rollback(store);
return -1;
}
int changed = sqlite3_changes(db);
if (cbm_store_commit(store) != CBM_STORE_OK) {
return -1;
}
uint64_t rows = (uint64_t)(changed > 0 ? changed : 0);
atomic_fetch_add_explicit(&g_importance_store_rows, rows, memory_order_relaxed);
cbm_log_info("pass.importance_store", "symbols", itoa_imp(rows));
return 0;
}
+18 -4
View File
@@ -946,6 +946,9 @@ static void predump_complexity(cbm_pipeline_ctx_t *ctx) {
static void predump_ensemble(cbm_pipeline_ctx_t *ctx) {
cbm_pipeline_pass_ensemble_routing(ctx);
}
static void predump_importance(cbm_pipeline_ctx_t *ctx) {
cbm_pipeline_pass_importance(ctx);
}
static void run_predump_passes(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx) {
static const struct {
@@ -953,12 +956,23 @@ static void run_predump_passes(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx) {
const char *name;
bool moderate_only; /* true = skip in fast mode */
} passes[] = {
{predump_deco, "decorator_tags", false}, {predump_cfg, "configlink", false},
{predump_route, "route_match", false}, {predump_ensemble, "ensemble_routing", false},
{predump_sim, "similarity", true}, {predump_sem, "semantic_edges", true},
{predump_deco, "decorator_tags", false},
{predump_cfg, "configlink", false},
{predump_route, "route_match", false},
{predump_ensemble, "ensemble_routing", false},
{predump_sim, "similarity", true},
{predump_sem, "semantic_edges", true},
{predump_complexity, "complexity", false},
/* Importance runs LAST: it reads CALLS/USAGE (extraction) and TESTS
* (pass_tests, which run_post_extraction runs before this loop), so
* every edge type its score depends on already exists here. */
{predump_importance, "importance", false},
};
enum { PREDUMP_PASS_COUNT = 7 };
/* Derived from the table, never hand-written. A hand-written count that
* lags a newly appended entry silently skips the LAST pass while every
* test stays green — exactly the failure this expression makes
* impossible. */
enum { PREDUMP_PASS_COUNT = (int)(sizeof(passes) / sizeof(passes[0])) };
struct timespec t;
for (int i = 0; i < PREDUMP_PASS_COUNT && !check_cancel(p); i++) {
/* "moderate_only" passes (similarity/semantic edges) run in FULL,
+47 -4
View File
@@ -1410,9 +1410,15 @@ static int run_extract_resolve(cbm_pipeline_ctx_t *ctx, cbm_file_info_t *changed
}
}
/* Run post-extraction passes (tests, decorator tags, configlink). */
/* Run post-extraction passes (tests, decorator tags, configlink).
*
* score_importance: false on the closure-delta route, whose ctx->gbuf is a
* PROXY buffer — cbm_delta_preseed fills it with id/label/name/qn/file_path
* and no project-wide edges, so an in-memory score there would read a
* near-zero in-degree and cbm_delta_patch would persist it. That route scores
* in SQL instead, after the patch; see cbm_pipeline_importance_recompute_store. */
static int run_postpasses(cbm_pipeline_ctx_t *ctx, cbm_file_info_t *changed_files, int ci,
const char *project) {
const char *project, bool score_importance) {
struct timespec t;
cbm_clock_gettime(CLOCK_MONOTONIC, &t);
@@ -1456,6 +1462,26 @@ static int run_postpasses(cbm_pipeline_ctx_t *ctx, cbm_file_info_t *changed_file
return rc < 0 ? rc : CBM_NOT_FOUND;
}
}
/* Importance last, in EVERY mode — same ordering rule as the full path: it
* reads CALLS/USAGE (re-extraction, above) and TESTS (pass_tests, first in
* this function).
*
* ONLY on the legacy-partial route, where ctx->gbuf really is the whole
* project rehydrated by cbm_gbuf_load_from_db — those nodes already carry
* an "importance" key from the previous run, and the write-back overwrites
* it in place rather than appending a duplicate. The closure-delta route
* passes score_importance = false and rescores in SQL after its patch,
* because its buffer holds proxies without project-wide edges. */
if (score_importance) {
cbm_clock_gettime(CLOCK_MONOTONIC, &t);
cbm_pipeline_pass_importance(ctx);
cbm_log_info("pass.timing", "pass", "incr_importance", "elapsed_ms",
itoa_buf((int)elapsed_ms(t)));
}
if (cbm_pipeline_check_cancel(ctx)) {
return CBM_NOT_FOUND;
}
return 0;
}
/* Publish the test-only legacy partial result through the same atomic
@@ -2144,7 +2170,7 @@ static int run_closure_delta(cbm_pipeline_t *p, const char *db_path, const char
phase_rc = cbm_pipeline_check_cancel(&ctx);
}
if (phase_rc == 0) {
phase_rc = run_postpasses(&ctx, changed_files, ci, project);
phase_rc = run_postpasses(&ctx, changed_files, ci, project, false);
}
if (ctx.return_type_table) {
for (int i = 0; i < ctx.return_type_table->count; i++) {
@@ -2169,6 +2195,23 @@ static int run_closure_delta(cbm_pipeline_t *p, const char *db_path, const char
}
cbm_log_info("delta.patch_done", "elapsed_ms", itoa_buf((int)elapsed_ms(t)));
/* Importance, in SQL, over the now-complete staging graph. This is the
* FIRST point on this route where the whole project is queryable: the
* patch has just inserted the repaired files' nodes AND re-linked the
* snapshotted inbound edges, so in-degree is finally correct. Scoring
* earlier — in run_postpasses, off the proxy buffer — would persist a
* near-zero score for any heavily-referenced symbol in a changed file.
* A failure here is a delta-route failure: `result` still holds
* FORCE_FULL_REINDEX, so the run degrades to a correct full rebuild
* rather than publishing wrong scores. */
cbm_clock_gettime(CLOCK_MONOTONIC, &t);
if (cbm_pipeline_importance_recompute_store(staging, project) != 0) {
cbm_log_error("delta.err", "phase", "importance_recompute");
goto out;
}
cbm_log_info("pass.timing", "pass", "delta_importance_store", "elapsed_ms",
itoa_buf((int)elapsed_ms(t)));
/* Coverage: previous failure rows for files not re-extracted + this
* run's fresh entries — same merge the legacy tail performs. */
cbm_file_error_t *run_errs = NULL;
@@ -2699,7 +2742,7 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil
phase_rc = cbm_pipeline_check_cancel(&ctx);
}
if (phase_rc == 0) {
phase_rc = run_postpasses(&ctx, changed_files, ci, project);
phase_rc = run_postpasses(&ctx, changed_files, ci, project, true);
}
/* Free ObjectScript tables built by pass_calls during run_extract_resolve. */
+55
View File
@@ -630,6 +630,61 @@ int cbm_pipeline_pass_semantic_edges(cbm_pipeline_ctx_t *ctx);
* cycles (recursive). Runs on the graph buffer before the dump. */
void cbm_pipeline_pass_complexity(cbm_pipeline_ctx_t *ctx);
/* Pre-dump pass: per-symbol importance score (weighted degree).
* importance = sqrt(num_refs) * priv * generic * distinct * test_penalty
* Stored as a numeric "importance" key inside the node's EXISTING
* properties_json — no schema change and no index-format bump; indexes written
* by older builds simply lack the key and consumers must tolerate its absence.
* MUST run after pass_tests and after CALLS/USAGE extraction — a pass ordered
* earlier would see zero TESTS edges and num_refs = 0 everywhere. It is
* therefore registered last in run_predump_passes and last in the incremental
* post-pass sequence. */
void cbm_pipeline_pass_importance(cbm_pipeline_ctx_t *ctx);
/* Gathered inputs for one symbol. Each scoring route fills this its own way
* (gbuf lookups, or SQL aggregates) and then calls the ONE rule below. */
typedef struct {
const char *name;
const char *file_path;
int num_refs; /* incoming CALLS + USAGE */
int name_distinct_files; /* distinct files the NAME is defined in */
bool tests_target; /* has an incoming TESTS edge */
} cbm_importance_inputs_t;
/* THE scoring rule — single definition, shared by every route, so the two
* gathering strategies cannot drift apart in what a score means. */
double cbm_pipeline_importance_score(const cbm_importance_inputs_t *in);
/* Produce a copy of `json` with the numeric "importance" key set to `score`,
* or NULL when `json` is not a JSON object or allocation fails. Caller owns
* the result. IDEMPOTENT: an existing key is overwritten in place, never
* appended twice — every re-scoring route sees nodes that already carry it.
* Single definition, shared by the in-memory and SQL writers alike. */
char *cbm_pipeline_importance_set_prop(const char *json, double score);
/* gbuf-node convenience wrapper around cbm_pipeline_importance_set_prop. */
void cbm_pipeline_importance_append_prop(cbm_gbuf_node_t *node, double score);
/* Recompute importance for an ENTIRE project directly in a store, in SQL.
* Used by the closure-delta incremental route, whose in-RAM graph is a proxy
* buffer with no project-wide edges — scoring there would persist a near-zero
* in-degree for heavily-referenced symbols in the changed files. Must be
* called on the STAGING store AFTER cbm_delta_patch has merged nodes and
* re-linked inbound edges. Returns 0 on success; the caller treats failure as
* a delta-route failure and falls back to a full rebuild. */
int cbm_pipeline_importance_recompute_store(cbm_store_t *store, const char *project);
/* Work counters for the importance pass (pass_importance.c). Deltas are read
* by the complexity suite's linearity gate; never reset by the pass itself, so
* nested/repeated runs compose. g_importance_name_visits counts same-name-group
* member visits during distinct-file counting — the quantity that goes
* superlinear if the per-distinct-name memoization is ever lost. */
extern _Atomic uint64_t g_importance_nodes;
extern _Atomic uint64_t g_importance_name_visits;
/* Rows rescored by the SQL-level store recompute. Lets a test prove the
* closure-delta route actually took that path instead of passing vacuously. */
extern _Atomic uint64_t g_importance_store_rows;
/* ── Env URL scanner (pass_envscan.c) ────────────────────────────── */
typedef struct {