Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@ add_executable(AlgoTests
tests/algo/test_score_combiner.cpp
tests/algo/test_betweenness_centrality.cpp
tests/algo/test_algo_merge.cpp
tests/algo/test_percentile_rank.cpp
tests/algo/test_complexity_scorer.cpp
)
target_include_directories(AlgoTests PRIVATE src)
target_link_libraries(AlgoTests PRIVATE algo GTest::gtest_main)
Expand Down
21 changes: 21 additions & 0 deletions src/algo/AlgoConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,25 @@ struct AlgoConfig {

int bc_k_min = 50;
uint64_t bc_seed = 42;

// ── Complexity score (file-level heatmap) ──────────────────────────────
// complexity_score = w_cc*pct_cc + w_nesting*pct_nesting + w_loc*pct_loc
// + w_inbound*pct_inbound + w_outbound*pct_outbound
// Each pct_* is a percentile rank in [0,1] across all scored files.
double complexity_w_cc = 0.30;
double complexity_w_nesting = 0.20;
double complexity_w_loc = 0.15;
double complexity_w_inbound = 0.20;
double complexity_w_outbound = 0.15;

// cc_score = cc_max_blend*max_cc + cc_avg_blend*avg_cc (pre-blend, before percentile-rank)
double complexity_cc_max_blend = 0.80;
double complexity_cc_avg_blend = 0.20;

// nesting_score = nesting_max_blend*max_depth + nesting_avg_blend*avg_depth
double complexity_nesting_max_blend = 0.80;
double complexity_nesting_avg_blend = 0.20;

// false(0): is_generated=1 files are excluded from the scoring population.
bool complexity_include_generated = false;
};
23 changes: 23 additions & 0 deletions src/algo/AlgoDbWriter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,18 @@ AlgoConfig AlgoDbWriter::loadConfig(const char* dbPath)
cfg.bc_k_min = get_int (db, "bc_k_min", cfg.bc_k_min);
cfg.bc_seed = get_uint64(db, "bc_seed", cfg.bc_seed);

cfg.complexity_w_cc = get_double(db, "complexity_w_cc", cfg.complexity_w_cc);
cfg.complexity_w_nesting = get_double(db, "complexity_w_nesting", cfg.complexity_w_nesting);
cfg.complexity_w_loc = get_double(db, "complexity_w_loc", cfg.complexity_w_loc);
cfg.complexity_w_inbound = get_double(db, "complexity_w_inbound", cfg.complexity_w_inbound);
cfg.complexity_w_outbound = get_double(db, "complexity_w_outbound", cfg.complexity_w_outbound);
cfg.complexity_cc_max_blend = get_double(db, "complexity_cc_max_blend", cfg.complexity_cc_max_blend);
cfg.complexity_cc_avg_blend = get_double(db, "complexity_cc_avg_blend", cfg.complexity_cc_avg_blend);
cfg.complexity_nesting_max_blend = get_double(db, "complexity_nesting_max_blend", cfg.complexity_nesting_max_blend);
cfg.complexity_nesting_avg_blend = get_double(db, "complexity_nesting_avg_blend", cfg.complexity_nesting_avg_blend);
cfg.complexity_include_generated = get_int(db, "complexity_include_generated",
cfg.complexity_include_generated ? 1 : 0) != 0;

sqlite3_close(db);
return cfg;
}
Expand Down Expand Up @@ -204,6 +216,17 @@ int AlgoDbWriter::updateConfig(sqlite3* db, const AlgoConfig& cfg)
i("bc_p2_fixed_k", cfg.bc_p2_fixed_k);
i("bc_k_min", cfg.bc_k_min);
u("bc_seed", cfg.bc_seed);

d("complexity_w_cc", cfg.complexity_w_cc);
d("complexity_w_nesting", cfg.complexity_w_nesting);
d("complexity_w_loc", cfg.complexity_w_loc);
d("complexity_w_inbound", cfg.complexity_w_inbound);
d("complexity_w_outbound", cfg.complexity_w_outbound);
d("complexity_cc_max_blend", cfg.complexity_cc_max_blend);
d("complexity_cc_avg_blend", cfg.complexity_cc_avg_blend);
d("complexity_nesting_max_blend", cfg.complexity_nesting_max_blend);
d("complexity_nesting_avg_blend", cfg.complexity_nesting_avg_blend);
i("complexity_include_generated", cfg.complexity_include_generated ? 1 : 0);
if (rc != SQLITE_OK) return rc;

char ts[32];
Expand Down
11 changes: 11 additions & 0 deletions src/algo/AlgoRunner.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#include <algorithm>
#include <climits>
#include <cstdio>
#include <sqlite3.h>
#include <unordered_map>
#include <vector>
Expand Down Expand Up @@ -126,6 +127,16 @@ AlgoRunResult AlgoRunner::run(const char* dbPath, const AlgoConfig& cfg)

auto gbr = GraphBuilder::build(db);

// Complexity scoring is independent of the PageRank/BC reading-sequence
// pipeline below (writes directly to the `file` table), but needs the
// same open db handle and file_graph -- best-effort, doesn't block the
// reading-sequence result on failure.
int complexityRc = ComplexityScorer::computeAndWrite(db, gbr.file_graph, cfg);
if (complexityRc != SQLITE_OK) {
std::fprintf(stderr, "AlgoRunner: complexity scoring failed (code %d): %s\n",
complexityRc, sqlite3_errmsg(db));
}

// Build loc_hint for file-level pass (LOC descending tie-break)
const Graph& fg = gbr.file_graph;
std::vector<int> file_loc_hint(fg.size(), 0);
Expand Down
166 changes: 166 additions & 0 deletions src/algo/Scoring.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
#include "Scoring.h"

#include <sqlite3.h>

#include <algorithm>
#include <cmath>
#include <cstdio>
#include <initializer_list>
#include <limits>
#include <numeric>
#include <queue>
Expand Down Expand Up @@ -75,6 +79,168 @@ static std::vector<double> minmax_normalize(const std::vector<double>& v)
return out;
}

// ── ComplexityScorer ────────────────────────────────────────────────────────

std::vector<double> percentile_rank_normalize(const std::vector<double>& values)
{
const std::size_t N = values.size();
if (N == 0) return {};
if (N == 1) return {0.0};

std::vector<double> sorted = values;
std::sort(sorted.begin(), sorted.end());

if (sorted.front() == sorted.back())
return std::vector<double>(N, 0.5);

std::vector<double> out(N);
for (std::size_t i = 0; i < N; ++i) {
auto it = std::lower_bound(sorted.begin(), sorted.end(), values[i]);
std::size_t strictlyLower = static_cast<std::size_t>(it - sorted.begin());
out[i] = static_cast<double>(strictlyLower) / static_cast<double>(N - 1);
}
return out;
}

// Normalizes a set of weights to sum to 1.0 if they don't already (+/- 0.001),
// logging a warning. Falls back to equal weights if the sum is ~0 (can't scale).
static void normalizeWeights(std::initializer_list<double*> weights, const char* label)
{
double sum = 0.0;
for (double* w : weights) sum += *w;
if (std::fabs(sum - 1.0) <= 0.001) return;

std::fprintf(stderr,
"ComplexityScorer: %s weights sum to %.6f (expected 1.0) -- auto-normalizing\n",
label, sum);

if (std::fabs(sum) < 1e-9) {
const double eq = 1.0 / static_cast<double>(weights.size());
for (double* w : weights) *w = eq;
return;
}
for (double* w : weights) *w /= sum;
}

std::vector<double> ComplexityScorer::compute(const std::vector<ComplexityFileMetrics>& inputs,
const AlgoConfig& cfg)
{
const std::size_t N = inputs.size();
if (N == 0) return {};

double w_cc = cfg.complexity_w_cc;
double w_nesting = cfg.complexity_w_nesting;
double w_loc = cfg.complexity_w_loc;
double w_inbound = cfg.complexity_w_inbound;
double w_outbound = cfg.complexity_w_outbound;
normalizeWeights({&w_cc, &w_nesting, &w_loc, &w_inbound, &w_outbound}, "top-level");

double cc_max_blend = cfg.complexity_cc_max_blend;
double cc_avg_blend = cfg.complexity_cc_avg_blend;
normalizeWeights({&cc_max_blend, &cc_avg_blend}, "cc pre-blend");

double nesting_max_blend = cfg.complexity_nesting_max_blend;
double nesting_avg_blend = cfg.complexity_nesting_avg_blend;
normalizeWeights({&nesting_max_blend, &nesting_avg_blend}, "nesting pre-blend");

std::vector<double> cc_raw(N), nesting_raw(N), loc_raw(N), inbound_raw(N), outbound_raw(N);
for (std::size_t i = 0; i < N; ++i) {
const ComplexityFileMetrics& m = inputs[i];
cc_raw[i] = cc_max_blend * m.max_cyclomatic_complexity
+ cc_avg_blend * m.avg_cyclomatic_complexity;
nesting_raw[i] = nesting_max_blend * m.max_block_depth
+ nesting_avg_blend * m.avg_block_depth;
loc_raw[i] = static_cast<double>(m.logical_loc);
inbound_raw[i] = static_cast<double>(m.inbound);
outbound_raw[i] = static_cast<double>(m.outbound);
}

std::vector<double> pct_cc = percentile_rank_normalize(cc_raw);
std::vector<double> pct_nesting = percentile_rank_normalize(nesting_raw);
std::vector<double> pct_loc = percentile_rank_normalize(loc_raw);
std::vector<double> pct_inbound = percentile_rank_normalize(inbound_raw);
std::vector<double> pct_outbound = percentile_rank_normalize(outbound_raw);

std::vector<double> scores(N);
for (std::size_t i = 0; i < N; ++i) {
scores[i] = w_cc * pct_cc[i]
+ w_nesting * pct_nesting[i]
+ w_loc * pct_loc[i]
+ w_inbound * pct_inbound[i]
+ w_outbound * pct_outbound[i];
}
return scores;
}

int ComplexityScorer::computeAndWrite(sqlite3* db, const Graph& file_graph, const AlgoConfig& cfg)
{
const char* selectSql = cfg.complexity_include_generated
? "SELECT file_id, max_cyclomatic_complexity, avg_cyclomatic_complexity, "
"max_block_depth, avg_block_depth, logical_loc FROM file;"
: "SELECT file_id, max_cyclomatic_complexity, avg_cyclomatic_complexity, "
"max_block_depth, avg_block_depth, logical_loc FROM file WHERE is_generated = 0;";

sqlite3_stmt* stmt = nullptr;
int rc = sqlite3_prepare_v2(db, selectSql, -1, &stmt, nullptr);
if (rc != SQLITE_OK) return rc;

std::vector<ComplexityFileMetrics> inputs;
while (sqlite3_step(stmt) == SQLITE_ROW) {
const char* fid = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
if (!fid) continue;

ComplexityFileMetrics m;
m.file_id = fid;
m.max_cyclomatic_complexity = sqlite3_column_int(stmt, 1);
m.avg_cyclomatic_complexity = sqlite3_column_double(stmt, 2);
m.max_block_depth = sqlite3_column_int(stmt, 3);
m.avg_block_depth = sqlite3_column_double(stmt, 4);
m.logical_loc = sqlite3_column_int(stmt, 5);

// Isolated files with no CALLS/INHERITS/IMPORTS edges still get a node
// (GraphBuilder pre-populates every file_id), so a missing lookup here
// means the file wasn't in the file table when the graph was built --
// fall back to 0/0 rather than dropping the file from scoring.
auto it = file_graph.id_to_node.find(m.file_id);
if (it != file_graph.id_to_node.end()) {
NodeId nid = it->second;
m.inbound = static_cast<int>(file_graph.radj[nid].size());
m.outbound = static_cast<int>(file_graph.adj[nid].size());
}

inputs.push_back(std::move(m));
}
sqlite3_finalize(stmt);
Comment on lines +188 to +213

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

SELECT loop swallows step errors, risking partial scoring.

The loop exits on any non-SQLITE_ROW return, so a mid-iteration error (e.g. SQLITE_BUSY, SQLITE_ERROR) is indistinguishable from SQLITE_DONE. Execution then proceeds to compute() and writes scores computed over a truncated population — silently corrupting the heatmap. Capture and check the final step code.

🛡️ Proposed fix to propagate read errors
     std::vector<ComplexityFileMetrics> inputs;
-    while (sqlite3_step(stmt) == SQLITE_ROW) {
+    int stepRc;
+    while ((stepRc = sqlite3_step(stmt)) == SQLITE_ROW) {
         const char* fid = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
         if (!fid) continue;
@@
         inputs.push_back(std::move(m));
     }
     sqlite3_finalize(stmt);
+    if (stepRc != SQLITE_DONE) return stepRc;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
while (sqlite3_step(stmt) == SQLITE_ROW) {
const char* fid = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
if (!fid) continue;
ComplexityFileMetrics m;
m.file_id = fid;
m.max_cyclomatic_complexity = sqlite3_column_int(stmt, 1);
m.avg_cyclomatic_complexity = sqlite3_column_double(stmt, 2);
m.max_block_depth = sqlite3_column_int(stmt, 3);
m.avg_block_depth = sqlite3_column_double(stmt, 4);
m.logical_loc = sqlite3_column_int(stmt, 5);
// Isolated files with no CALLS/INHERITS/IMPORTS edges still get a node
// (GraphBuilder pre-populates every file_id), so a missing lookup here
// means the file wasn't in the file table when the graph was built --
// fall back to 0/0 rather than dropping the file from scoring.
auto it = file_graph.id_to_node.find(m.file_id);
if (it != file_graph.id_to_node.end()) {
NodeId nid = it->second;
m.inbound = static_cast<int>(file_graph.radj[nid].size());
m.outbound = static_cast<int>(file_graph.adj[nid].size());
}
inputs.push_back(std::move(m));
}
sqlite3_finalize(stmt);
int stepRc;
while ((stepRc = sqlite3_step(stmt)) == SQLITE_ROW) {
const char* fid = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
if (!fid) continue;
ComplexityFileMetrics m;
m.file_id = fid;
m.max_cyclomatic_complexity = sqlite3_column_int(stmt, 1);
m.avg_cyclomatic_complexity = sqlite3_column_double(stmt, 2);
m.max_block_depth = sqlite3_column_int(stmt, 3);
m.avg_block_depth = sqlite3_column_double(stmt, 4);
m.logical_loc = sqlite3_column_int(stmt, 5);
// Isolated files with no CALLS/INHERITS/IMPORTS edges still get a node
// (GraphBuilder pre-populates every file_id), so a missing lookup here
// means the file wasn't in the file table when the graph was built --
// fall back to 0/0 rather than dropping the file from scoring.
auto it = file_graph.id_to_node.find(m.file_id);
if (it != file_graph.id_to_node.end()) {
NodeId nid = it->second;
m.inbound = static_cast<int>(file_graph.radj[nid].size());
m.outbound = static_cast<int>(file_graph.adj[nid].size());
}
inputs.push_back(std::move(m));
}
sqlite3_finalize(stmt);
if (stepRc != SQLITE_DONE) return stepRc;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/algo/Scoring.cpp` around lines 188 - 213, Update the SQLite SELECT loop
in the scoring function to capture each sqlite3_step result, process SQLITE_ROW
rows, and after the loop verify the final status is SQLITE_DONE. On any other
status, handle and propagate the read error before calling compute() or writing
scores, preventing partial scoring. Use the surrounding statement-loading
function and its existing error-handling conventions as the integration point.


std::vector<double> scores = compute(inputs, cfg);

rc = sqlite3_exec(db, "BEGIN;", nullptr, nullptr, nullptr);
if (rc != SQLITE_OK) return rc;

sqlite3_stmt* upd = nullptr;
rc = sqlite3_prepare_v2(db,
"UPDATE file SET complexity_score = ? WHERE file_id = ?;", -1, &upd, nullptr);
if (rc != SQLITE_OK) {
sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr);
return rc;
}

for (std::size_t i = 0; i < inputs.size(); ++i) {
sqlite3_bind_double(upd, 1, scores[i]);
sqlite3_bind_text(upd, 2, inputs[i].file_id.c_str(), -1, SQLITE_STATIC);
rc = sqlite3_step(upd);
sqlite3_reset(upd);
if (rc != SQLITE_DONE) {
sqlite3_finalize(upd);
sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr);
return rc;
}
}
sqlite3_finalize(upd);

return sqlite3_exec(db, "COMMIT;", nullptr, nullptr, nullptr);
}

std::vector<double> ScoreCombiner::combine(const std::vector<double>& pr,
const std::vector<double>& bc,
double alpha,
Expand Down
39 changes: 39 additions & 0 deletions src/algo/Scoring.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,16 @@
#include "AlgoConfig.h"
#include "Graph.h"
#include <memory>
#include <string>
#include <vector>

// Percentile-rank normalization: position-based, so a single extreme outlier
// doesn't compress the rest of the population toward 0 the way min-max does.
// rank(i) = (# values strictly less than v[i]) / (N - 1); ties share a rank.
// N==0 -> {}; N==1 -> {0.0}; zero-variance input -> all 0.5.
// Exposed (not file-local static like minmax_normalize) so it's unit-testable.
std::vector<double> percentile_rank_normalize(const std::vector<double>& values);

class PageRank {
public:
static std::vector<double> compute(const Graph& g, const AlgoConfig& cfg);
Expand All @@ -18,6 +26,37 @@ class ScoreCombiner {
double beta);
};

// Per-file inputs for ComplexityScorer, already joined with graph in/out-degree.
// (DB query + graph lookup happen in ComplexityScorer::computeAndWrite.)
struct ComplexityFileMetrics {
std::string file_id;
int max_cyclomatic_complexity = 0;
double avg_cyclomatic_complexity = 0.0;
int max_block_depth = 0;
double avg_block_depth = 0.0;
int logical_loc = 0;
int inbound = 0;
int outbound = 0;
};

class ComplexityScorer {
public:
// Pure computation: pre-blends CC/nesting (max/avg), percentile-ranks all
// five inputs (cc, nesting, logical_loc, inbound, outbound) independently,
// and combines them by configured weight. No DB access. Weight groups that
// don't sum to 1.0 (+/- 0.001) are auto-normalized with a stderr warning.
// Returns one score per file, same order as `inputs`.
static std::vector<double> compute(const std::vector<ComplexityFileMetrics>& inputs,
const AlgoConfig& cfg);

// Reads file_id + the five rollup metrics from the `file` table (excluding
// is_generated=1 rows unless cfg.complexity_include_generated), joins each
// file_id to `file_graph` for inbound/outbound degree, computes scores via
// compute(), and writes complexity_score back to `file` in one transaction.
// Returns SQLITE_OK or the sqlite error code that failed.
static int computeAndWrite(sqlite3* db, const Graph& file_graph, const AlgoConfig& cfg);
};

// ── Betweenness Centrality ──────────────────────────────────────────────────
// Pass 1(파일)/Pass 2(함수) 별로 그래프 규모에 따라 BC 계산 전략을 분기한다.

Expand Down
Loading