Skip to content

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 03 May 00:46
v0.5.0
22fde6b

Added

  • assess_risk_batch MCP tool (and matching codesage risk-batch <files...> CLI subcommand). Takes a list of repo-relative file paths and returns one RiskAssessment per path in the request order, with no patch-level aggregation. Use when you have N files (e.g. from impact analysis or coupling) and want each individual score — cuts N MCP round-trips down to one. Retrospective session analysis (recommendations doc §1.7) found 230 individual assess_risk calls per 30-day window vs 13 assess_risk_diff — per-file scoring is the agent's dominant pattern, and this is the matching batch primitive. For patch-level aggregation (max/mean, rollups, summary_notes, cycles) keep using assess_risk_diff — the two answer different questions. CLI accepts stdin too: git diff --name-only | codesage risk-batch. Response includes a top-level _legend short-code map (same shape as the new field on RiskDiffAssessment below). Three integration tests cover input ordering, empty input, and the legend behaviour at threshold.
  • session_start and session_end MCP tools (and matching codesage session-start / codesage session-end CLI subcommands). session_start snapshots the current structural state of the index — file count, symbol count, full file list, all import cycles, and the top-50 highest-risk files with their scores — to .codesage/sessions/<session_id>.json. session_end reloads that snapshot, recomputes current state, and returns a SessionDiff: pass: bool (true when no new cycles were introduced AND no top-risk file regressed by ≥ 0.10), new_cycles / resolved_cycles (each as sorted member lists), risk_regressions (per-file before/after/delta for files in the snapshot's top-risk baseline that moved by ≥ 0.05), new_files / removed_files, file/symbol counts before-and-after, best-effort git_head_before / git_head_after, and summary_notes ready to paste into a PR description. CLI exits non-zero on FAIL so the workflow composes with shell pipelines and &&. Session ids are validated (alphanumerics, -, _, .; max 128 chars) so they can't escape the sessions/ directory. Re-running session_start overwrites the prior snapshot for that id (resets the baseline mid-session). Pattern borrowed from sentrux's session_start / session_end tools, reimplemented around CodeSage's existing risk + cycle infrastructure. 6 integration tests cover clean pass, new-cycle fail, resolved-cycle pass, missing-snapshot error, baseline overwrite, and id-validation rejection.

Changed

  • assess_risk_diff and assess_risk_batch now emit a top-level _legend map that aliases categorical notes to short codes when ≥3 files in a single response share the same note string. Two notes are eligible for aliasing today: "test gap: no obvious test file (sibling or co-changer)" becomes "T"; "no git history for this file (file too new, or codesage git-index hasn't been run)" becomes "NG". Templated notes ("hotspot: churn percentile 80%", "in import cycle of 4 files: …", etc.) are not aliased — they collide across files and wouldn't dedupe cleanly. Threshold reasoning: each _legend entry costs ~75-95 bytes overhead; below 3 occurrences the dedup is net loss. Patches that touch many files in similar states (refactor where every touched file lacks a co-located test, fresh module not yet in git history) see ~10% byte reduction on the response. Schema is additive and back-compatible — agents that don't read _legend see short codes inline; the existing rollup arrays (test_gap_files, etc.) still list every file by name, so semantic information isn't lost. Recommendations doc §1.5 part 2.
  • SQLite busy_timeout=5000 is now set on every database connection in init_db. A second MCP session reading mid-index waits up to 5 s for the lock instead of failing instantly with SQLITE_BUSY. Concurrent writers were already serialized by the advisory lockfile from §2.4; this closes the brief WAL-checkpoint windows that affected readers. Perf-neutral: it's a wait threshold, not a throughput knob — no-op when there's no contention. Matches repowise's hardening (recommendations doc §1.8). journal_mode=WAL and foreign_keys=ON were already set.
  • The MCP server no longer fails every tool call when a project's .codesage/config.toml is malformed. load_embedding_config now logs a warning and falls back to EmbeddingConfig::default() on read or parse error, so structural tools (assess_risk, find_coupling, find_symbol, find_references, list_dependencies, impact_analysis, export_context) keep serving the project. search will fail at vec-table lookup if the indexed model differs from the defaults — a narrower, clearer failure than a TOML parse error on every tool call. The CLI path (codesage index, codesage search, etc.) keeps its loud-fail behavior on purpose: an interactive user wants to know their config is broken. Pattern from Serena #1424 + repowise #94 (recommendations doc §1.9).
  • bench/generate-llm-corpus.py generates LLM-as-judge eval cases by sampling source files in a project, asking codex exec to invent a natural-language search query for each one, and emitting CodeSage's existing corpus YAML. Produces queries that read like an engineer's search-box phrasing rather than the git-commit subjects of the existing corpora — much harder for retrieval. Demo on ripgrep (5 cases, bench/corpora/ripgrep-llm-eval.yaml): r@5 0.40 vs 0.80 on the commit-subject corpus, median first-hit 6 vs 2. Commit-subject corpora top out near ceiling and don't differentiate retrieval improvements; LLM-judged queries expose the headroom. Sampling skips tests, vendor, generated, lockfiles, and files outside the 30-500 line band; deterministic via --seed. Verify-step (separate codex call to filter false-positive labels) and category labels (semantic / architecture / symbol) are noted in the docstring as a follow-up. Pattern from Semble's benchmarks/data.py.
  • bench/codesage-bench-runner now reports mean tokens to first relevant hit per Semble's headline metric. Counts cl100k_base tokens (via tiktoken) of every chunk an agent would have consumed in rank order before hitting an expected file. Misses are charged a fixed 32k-token penalty so a single MISS doesn't make the average unbounded. Per-case scorecard adds a tokens→hit column; the <!-- METRICS: ... --> line gains mean_tokens_to_hit=<int> when tiktoken is installed (the runner falls back gracefully without it). On the bundled corpora today: nest 267 tokens/query (5/5 hit), ripgrep 6,753 (4/5 hit, the MISS adds the 32k penalty). For comparison, Semble's published baseline of "ripgrep + read file" is 45,692 tokens.
  • search now runs a non-candidate stem scan on bare-symbol queries: if no chunk in the top-50 candidate pool matches the file whose stem is the queried symbol (snake_case or kebab_case-normalized — ModuleRef matches module_ref.py, module-ref.ts, or ModuleRef.java), we fetch that file's chunks directly and inject any whose content contains a definition of the symbol. The injected chunk enters the pipeline with score 0; downstream apply_definition_boost lifts it by 3 * max_score * 1.5 (file-stem bonus), then path-penalty / cross-encoder / saturation judge it normally. Backstop for embedding misses on small / oddly-chunked files. Default-on; opt-out with CODESAGE_STEM_SCAN=0. Validated on the nest index: ModuleRef query went from "definition not in top-5" to rank 1 (score 1.61, def chunk at module-ref.ts:20); InstanceWrapper unchanged because its definition file was already in the candidate pool. Symbol length floor at 3 chars to keep short identifiers from triggering N file scans. Pattern from Semble's _scan_non_candidates.
  • apply_definition_boost (default-on, opt-out via CODESAGE_DEFINITION_BOOST=0) now also fires on NL queries that contain embedded CamelCase / camelCase tokens with an internal capital (e.g. "how does StateManager initialize?", "feat: add MulterOptions and MulterField interfaces"). Half-strength boost (1.5× max_score additive vs 3× for bare-symbol queries), since the user is asking about the symbol but may want explanatory context too. Pure acronyms (HTTP, XML) and single-capital words (Dockerfile, Default) are intentionally excluded — the regex requires an internal capital after a lowercase run, so only PascalCase / camelCase shapes match. Validated on the nest bench: feat: add MulterOptions and MulterField interfaces for express platform configuration moved its expected file from rank 2 → 1 (the only bench query containing a qualifying embedded symbol). Pattern from Semble's boosting.py _boost_embedded_symbols.
  • search now boosts candidate chunks that contain a language-keyword definition of a queried bare symbol (fn foo, class FooBar, defmodule Phoenix.Router, etc.). Triggered only when is_symbol_query accepts the query — namespace-qualified, snake_case, CamelCase, leading-underscore, or starts-with-uppercase. NL queries (commit subjects, "how does X work" prose) are provably inert, so the bundled bench corpora (all NL git-commit subjects) showed all 10 cases identical with vs without. Manual A/B against the nest index on bare-symbol queries: ApplicationConfig definition chunk surfaced from off-top-5 to rank 1 (0.76 → 1.45); MicroservicesModule similarly (0.65 → 2.57). Boost is additive 3 * max_score, with a 1.5× multiplier when the file stem matches the symbol name (snake_case-normalized: login_controller.rs matches LoginController). typedef is intentionally not in the keyword list (its keyword TYPE NAME shape can't be expressed in the namespace-prefix regex; find_symbol covers it). 17 keywords from Rust/Python/JS/TS/Go/PHP/Elixir/Kotlin/Scala/Swift/C#/Java. Default-on; disable with CODESAGE_DEFINITION_BOOST=0. Pattern from Semble's boosting.py _boost_symbol_definitions. 13 unit tests cover the symbol-vs-NL gate, namespace stripping, definition-vs-reference matching, and the file-stem bonus.
  • search now applies file-saturation decay after the cross-encoder reranker so a single file can't monopolize the top-K. Walks results in score order; the Nth chunk from a file (N≥2) gets its score multiplied by 0.5^(N-1). Then re-sorts. Threshold and decay match Semble's ranking/penalties.py (THRESHOLD=1, DECAY=0.5). Default-on; disable with CODESAGE_FILE_SATURATION=0. Validated on bundled corpora: nest aggregate unchanged (each expected file already had only 1 chunk in top-10), ripgrep aggregate unchanged but the diversity mechanism is observable on a pre-existing MISS case where noise jumped 1→9 distinct files in the top-10 (saturation pulled 8 single-chunk files in to replace 8 redundant chunks of the same file). Zero regressions on either corpus. 5 unit tests cover the no-op (one-chunk-per-file), 2nd/3rd chunk decay arithmetic, the diversity-promotes-distinct-file case, and empty input.
  • search now applies multiplicative path-based penalties to results before reranker blending. Test/bench files (TEST_LIKE_EXCLUDE_PATTERNS), compat/_compat/legacy/_legacy directory segments, and examples/_examples directory segments get 0.3×; re-export barrels (__init__.py, package-info.java) get 0.5×; TypeScript declaration files (*.d.ts) get 0.7×. Penalties stack multiplicatively (a test in compat/ gets 0.09×). Singular example is intentionally NOT a trigger so the com.example.* Java/Kotlin namespace is unaffected. Default-on; disable with CODESAGE_PATH_PENALTY=0 if a real-world corpus regresses. Validated on the bundled nest and ripgrep eval corpora: nest median first-hit improved from rank 2 → 1 across 4 of 5 cases (the previously-#2 production code climbs over previously-#1 test/example chunks); ripgrep saw zero rank changes (no test files in top-10 candidates for those queries) and one demoted noise unit on an existing miss. No regressions on either corpus. Pattern ported from Semble's ranking/penalties.py. Companion to the test-inclusive indexing change above — without that, tests wouldn't be in the candidate pool to demote in the first place.
  • Test and bench files are now indexed structurally and semantically. Previously DEFAULT_EXCLUDE_PATTERNS dropped them at file discovery, so find_references could not see callsites in tests, find_symbol could not find test fixtures, and search had no test code to demote even when the user wanted it. The list is now split: HARD_EXCLUDE_PATTERNS (vendored deps, build outputs, lockfiles, docs, IDE state) is applied at discovery; TEST_LIKE_EXCLUDE_PATTERNS (the existing test/bench glob set, already used by the git-history layer) is no longer subtracted from DEFAULT_EXCLUDE_PATTERNS and stays available for the search ranker to demote at rank time. Index size grows ~25–100% on real codebases (measured: nest +67%, ripgrep +25%, codesage +99%) and find_references accuracy improves substantially (nest reference count +217%, 12,555 → 39,804). The git-history compile_excludes filter that subtracted TEST_LIKE from DEFAULT is now redundant and removed. The codesage init config template comment updated accordingly.
  • assess_risk score now includes import-cycle membership as a sixth signal. New fields on RiskAssessment: in_cycle: bool, cycle_size: u32, cycle_files: Vec<String> (other members of the SCC, excluding the file itself). The score blend rebalances to make room for the new term: churn 0.35 (unchanged), fix 0.20 (unchanged), dependents 0.10 (was 0.15), coupling 0.10 (was 0.15), test_gap 0.15 (unchanged), cycle 0.10 (new). Cycle term value: (cycle_size - 1) / 4 clamped to 1.0, so a 2-file cycle contributes 0.025, a 3-file 0.05, a 4-file 0.075, and 5+ file 0.10. When in_cycle is true, a new note like "in import cycle of 4 files: B.php, C.php, D.php" joins the existing decomposition notes. Reuses the iterative Tarjan SCC machinery already powering assess_risk_diff.cycles_touching_patch, so no new graph code. Donor pattern from sentrux's structural acyclicity metric, reimplemented per-file. Three new tests cover the cycle signal, the no-cycle case, and the score-lift on a quiet file in a cycle; all 34 prior risk tests pass with the rebalanced weights.

Fixed

  • codesage init now writes model = "jinaai/jina-embeddings-v2-base-code" and device = "gpu" as the default. Validated 2026-05-02 against four codebases (one PHP, one C, one Rust, one TypeScript) using LLM-judged corpora generated by bench/generate-llm-corpus.py: Jina-code-v2 wins on every aggregate on every project (−6 to −18pp miss rate, +6 to +18pp recall@10, −9 to −26% mean tokens to first hit). Indexing is ~5× slower per chunk and the DB ~2× larger (768d vs 384d), but the new GPU-fallback hard-fail check (below) prevents the silent CPU degradation that motivated the prior conservative device = "cpu" default. Users without CUDA can edit two lines back to MiniLM/cpu. The commit-subject corpora that previously drove the "MiniLM beats Jina" conclusion were vocabulary-saturated at ceiling and didn't expose Jina's lift on paraphrased natural-language queries.
  • Reranker::new now applies the same silent-CUDA-fallback hard-fail check shipped for Embedder::new last commit. Previously the reranker session would register CUDA "successfully" and quietly run on CPU when libcuda failed to bind, just like the embedder did before. Same /proc/self/maps probe, same CODESAGE_ALLOW_CPU_FALLBACK=1 bypass. The shared helper model::require_cuda_libs_mapped is now pub(crate) so both call sites use one implementation.
  • Embedder::new now hard-fails with an actionable error when device = "gpu" was requested but ORT silently fell back to CPU. Detected by inspecting /proc/self/maps after session commit (Linux only): if libcuda.so and libcudart.so aren't both mapped into the process, refuses to continue. Caught a real failure mode where a flip-all script reindexed one project on CPU for 11+ minutes despite a Successfully registered CUDAExecutionProvider log line — /proc/self/maps had zero CUDA entries. Bypass for tests / mixed-mode setups: set CODESAGE_ALLOW_CPU_FALLBACK=1. Missing-only-cuDNN-or-cuBLAS produces a WARN (per-op fallback is normal for some attention shapes); both libcuda and libcudart missing is a hard error.
  • codesage cleanup now treats only actual vec0 chunk tables as vector tables, so the active {chunk_table}_fts sidecar is preserved. When cleanup drops an orphan vec table, it drops that table's matching FTS and FTS vocab sidecars too.
  • MCP structural tools (find_symbol, find_references, list_dependencies, impact_analysis, export_context, find_coupling, assess_risk, assess_risk_batch, assess_risk_diff, recommend_tests, session_start, session_end) no longer load the embedding model or require CUDA just to open the project database. Symbol-based export_context still uses existing semantic chunks when the configured model table is already present, without constructing the embedder.
  • codesage export --symbol now avoids loading the embedding model too, matching MCP symbol export. No-embedder symbol export now selects chunk tables through exact persisted model metadata, so sanitized model-name collisions do not read chunks from the wrong model; case-only SQLite table-name collisions are rejected before they can bind a colliding model to the wrong chunks. Legacy pre-metadata table detection requires the exact sanitized model prefix followed by a numeric dimension suffix, so prefix-adjacent models do not trigger false rebuild errors. Multiple recorded tables for the same model are reported as ambiguous. Upgraded databases with populated pre-metadata chunk tables now get a clear codesage index --full rebuild instruction instead of silently dropping semantic context.
  • codesage index now fails loudly when semantic model setup fails, unless --no-semantic was explicitly requested. A GPU-configured project run by a non-CUDA binary no longer reports a successful semantic index after silently skipping embeddings.
  • Semantic incremental indexing now tracks semantic-file hashes separately from structural-file hashes and scopes them to the active chunk table. This avoids stale chunks when a previous --no-semantic structural index refreshed file hashes, when a changed file becomes unchunkable, or when switching embedding models. Opening a chunk table now also repairs a missing or empty FTS sidecar from existing vec chunks, so upgraded/pre-FTS indexes and databases affected by an older cleanup run regain BM25 coverage without requiring a full semantic reindex. Full semantic reindexing rebuilds legacy chunk tables that predate exact model metadata, and semantic removal stats now include metadata-only orphan cleanup.
  • Hybrid BM25+semantic search now applies requested path filters to BM25 candidates before RRF fusion, preventing exact literal matches outside the requested path set from leaking into filtered results.
  • Search pagination now returns an empty result page when offset is at or beyond the result count instead of falling back to the first page, and applies pagination after BM25 fusion, symbol boosts, and reranking so pages reflect the final ranked order.
  • codesage doctor now verifies the post-rewrite hook in addition to post-commit, post-merge, and post-checkout, matching the hook set installed by codesage install-hooks.