[feature] Complexity Score Combination#32
Conversation
CREATE TABLE IF NOT EXISTS alone won't add the column to existing DBs, so initDb now checks via PRAGMA table_info and issues an ALTER TABLE ADD COLUMN for DBs created before this column existed.
Ten new weights/blends for the upcoming ComplexityScorer, following the same load/upsert pattern already used for alpha/beta/damping so they're user-adjustable via reading_sequence_config without a recompile.
📝 WalkthroughWalkthroughAdds configurable percentile-based file complexity scoring, persists scores in SQLite with schema migration support, integrates scoring into the algorithm runner, and adds normalization, scoring, database, and generated-file handling tests. ChangesComplexity scoring
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with 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.
Inline comments:
In `@src/algo/Scoring.cpp`:
- Around line 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.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 57ecbdc7-c3f0-411c-9032-ba61e1013988
📒 Files selected for processing (9)
CMakeLists.txtsrc/algo/AlgoConfig.hsrc/algo/AlgoDbWriter.cppsrc/algo/AlgoRunner.cppsrc/algo/Scoring.cppsrc/algo/Scoring.hsrc/db/db.cpptests/algo/test_complexity_scorer.cpptests/algo/test_percentile_rank.cpp
| 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); |
There was a problem hiding this comment.
🗄️ 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.
| 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.
Summary
Implements #31 — combines the per-file complexity signals from #30 (cyclomatic complexity, block nesting depth, logical LOC) with inbound/outbound link counts into a single
complexity_scoreper file, using percentile-rank normalization instead of min-max so a single outlier file doesn't compress everyone else's score toward 0.complexity_score REAL NOT NULL DEFAULT 0.0to thefiletable, with a migration path for existing DBs (CREATE TABLE IF NOT EXISTSalone won't touch already-created tables).percentile_rank_normalizealongside the existingminmax_normalizeinScoring.cpp— PageRank/BC's min-max path is untouched.ComplexityScorer(compute+computeAndWrite), following the same pure-compute/DB-IO split already used byPageRank/AlgoDbWriter.AlgoConfigkeys (5 top-level weights, 2 pre-blend pairs, 1 include-generated flag), read/written viareading_sequence_configthe same wayalpha/beta/dampingalready are — no recompile needed to retune.ComplexityScorer::computeAndWriteintoAlgoRunner::run, soTelescodeAlgo <db_path>computes it end-to-end alongside the existing reading-sequence pipeline.Design notes
rank(i) = #{values strictly less than v[i]} / (N-1)), so it only cares about relative ordering.0.8·max + 0.2·avgblend before percentile-ranking (configurable);logical_loc, inbound, and outbound feed in unblended.WHERE is_generated = 0); includable viacomplexity_include_generated. Excluded files stay at the column default (0.0) — this is unambiguous sinceis_generatedis already its own column callers can filter on.Graph.radj/Graph.adjat scoring time (same patternPageRank::computealready uses forout_deg), not persisted asfiletable columns — avoids a second source of truth that would need invalidation on incremental updates.Commits
feat(db): add complexity_score column to file table with migrationfeat(algo): add complexity scoring config keys to AlgoConfigfeat(algo): add percentile_rank_normalize helper with unit testsfeat(algo): add ComplexityScorer::compute (blend, percentile-rank, weighted sum)feat(algo): add ComplexityScorer::computeAndWrite (DB read/write + graph join)feat(algo): wire ComplexityScorer into AlgoRunner::runTest plan
Unit tests (15 new cases: percentile-rank edge cases, blend/weighted-sum correctness against a hand-computed fixture, weight auto-normalization, DB round-trip via
computeAndWrite):Schema migration (verifies existing DBs get the column added without losing data):
Config seeding (verifies all 10 new keys get seeded with correct defaults on first run):
cmake --build build --target TelescodeScanner TelescodeAlgo --config Debug ./build/Debug/TelescodeScanner.exe repos/sherlock cfg_test.db ./build/Debug/TelescodeAlgo.exe cfg_test.db sqlite3 cfg_test.db "SELECT config_key, config_value FROM reading_sequence_config WHERE config_key LIKE 'complexity_%' ORDER BY config_key;" rm cfg_test.dbEnd-to-end (real repo, sanity-checks the resulting distribution):
./build/Debug/TelescodeScanner.exe repos/sherlock e2e_test.db ./build/Debug/TelescodeAlgo.exe e2e_test.db sqlite3 e2e_test.db "SELECT file_id, complexity_score FROM file WHERE is_generated=0 ORDER BY complexity_score DESC LIMIT 10;" rm e2e_test.dbExpect the largest/most-complex file in the repo (sherlock_project/sherlock.py, 714 loc, CC 48) at the top of the list with a score near the high end (~0.95), scores spread across the range rather than clustered.
Out of scope
Color mapping / heatmap visualization (separate issue)
UI for weight adjustment (separate issue — config keys defined here are what it will read/write)
Per-function complexity score (file-level only)