Skip to content

Latest commit

 

History

History
665 lines (507 loc) · 165 KB

File metadata and controls

665 lines (507 loc) · 165 KB

Fixed

  • Laravel route URIs: array-options group prefixes (Route::group(['prefix' => 'admin'], ...)) are applied, nested group prefixes compose, and a root route maps to / instead of an empty URI.

0.18.0 - 2026-07-20

Added

  • codesage index --no-features skips the feature-mapping stage.
  • codesage status --json and codesage export --json.
  • codesage doctor warns when installed git hooks invoke a codesage binary that is missing or not executable, and distinguishes a foreign (non-codesage) hook from a missing one.
  • Schema migration 0013 removes duplicate symbol, reference, and fingerprint rows and adds unique indexes that reject future duplicates.
  • codesage cleanup checks free disk space before VACUUM and fails up front with a clear message instead of mid-operation.

Changed

  • codesage index skips feature mapping when an incremental pass changed no source files or build manifests. Full indexes and codesage map always remap, and a partial mapper failure leaves the next run to remap.
  • Git hooks append to .codesage/hooks.log (truncated past 1 MiB) instead of discarding output, and run the index and git-index passes independently instead of short-circuiting.
  • Every MCP tool's outputSchema declares the optional _meta envelope (truncation, staleness); find_coupling, export_context, and feature_bundle descriptions document the structured found field.
  • Daemon shutdown waits up to 5 seconds for in-flight MCP requests to finish before exiting.
  • search applies every requested language filter to BM25-fused candidates, and near-tied results keep their ranking instead of being reshuffled by symbol boosts.
  • Feature mappers honor gitignore and exclude patterns in directory scans, cap file reads at 1 MiB, and share one test-file definition across languages.
  • Pre-commit leak patterns cover Anthropic, OpenAI, Google, Hugging Face, npm, PyPI, and GitLab token shapes.

Fixed

  • Watcher: edited .h headers track the project's C/C++ dialect in both directions; renamed or deleted files are purged from the index instead of stranded; edit bursts and filesystem-event overflows trigger one cooldown-respecting catch-up reindex instead of repeated full passes; a malformed config.toml skips the watcher with a warning instead of silently dropping exclude_patterns.
  • The daemon no longer recreates a deleted .codesage/index.db as an empty index; reads report the project as not onboarded.
  • codesage mcp waits for a slow daemon cold-start (up to 15 seconds while the child process is alive) instead of failing at 5 seconds and orphaning it.
  • .phpt files under a feature's declared test directory attach to C/C++ PHP-extension feature slices, and when the per-feature test cap fills, the slice's own language wins.
  • session_end tolerates small backwards clock steps instead of failing until wall time catches up.
  • A corrupted cached model artifact is evicted and re-downloaded once before failing closed.

Security

  • Git hooks refuse to write through a symlinked or non-regular .codesage/hooks.log.
  • Model-cache eviction deletes blob targets only inside the Hugging Face cache; an escaping symlink loses just the link.

0.17.0 - 2026-07-17

Added

  • /codesage-review adds --focus all|product and --max-verify-findings; product focus skips test-suite, bench/, and scripts/ slices while full coverage remains the default. /codesage-revalidate gains --max-verify-findings and runs new regression findings through the same adversarial verifier.

Changed

  • feature_bundle reserves up to two existing related slots for requested caller/callee chunks before backfilling tests and context, keeping the response limit unchanged.
  • /codesage-review ranks slices by maximum owned-file risk, sends slim feature-local priors and precomputed batch risk to reviewers, targets large files by hot symbols, and verifies new findings once per feature.
  • Required /codesage-review and /codesage-revalidate responses to cover a deterministic entry/changed/risk-ranked file set before findings merge.

Fixed

  • Review findings now use deterministic evidence validation, identity matching, content-based freshness, feature metadata persistence, atomic merges, and transition-only history; revalidation no longer marks an omitted open finding fixed.
  • Fixed review evidence validation for short exact two-line code blocks, full ±15-line block bounds, and unrelated returned finding IDs; evidence blocks now also match across blank source lines.
  • Review-mode merges reopen an evidence-matched fixed finding instead of silently absorbing it; revalidation drops rediscovered untargeted findings as out_of_scope instead of failing the whole feature, and no longer advances slice freshness.
  • Review freshness fingerprints bind to the run's category/severity/focus scope and dispatch-time content; coverage plans drop deleted files and include changed context/test files; findings documents persist trust_boundaries.
  • /codesage-review anchors all state paths at the project root, exempts an explicit --feature from the freshness skip, raises feature discovery to 500 slices with a truncation warning, and fails a deep-mode feature when a lens response reports an error.
  • Release automation keeps the Codex plugin, Claude plugin, and Claude marketplace versions aligned with the workspace version, and refreshes both local plugin installs before pushing.

0.16.0 - 2026-07-12

Added

  • Added a codesage-retrieval skill (loaded by both Claude Code and Codex) that routes retrieval, risk, and test-selection queries through CodeSage, packaged as a repo-local Codex marketplace that reuses the global MCP registration.

Changed

  • Rationale-comment extraction (WHY: / NOTE: / IMPORTANT: / TODO markers surfaced on find_symbol / export_context) now covers C, C++, Go, and PHP in addition to Rust and Python; PHP /** */ docblocks (marker on an interior line), # line comments, and #[...] attributes between docblock and definition are handled.

0.15.0 - 2026-07-09

Added

  • /codesage-review gains --deep (multi-lens reviewers on high-risk slices), --no-verify, --model, and --verify-model; new findings pass an adversarial verification stage (new codesage-finding-verifier agent) by default.
  • CODESAGE_HF_DOWNLOAD_TIMEOUT_SECS bounds Hugging Face artifact downloads during model load.

Changed

  • Upgrade the rmcp MCP SDK to 2.x (MCP 2025-11-25 spec model types).
  • /codesage-review / /codesage-revalidate drop findings whose quoted evidence doesn't match the cited file, compute finding_ids orchestrator-side with fuzzy prior-matching (IDs survive model/wording changes and small code motion), repair unparseable reviewer JSON before erroring a feature, and pass a last-reviewed base SHA so re-reviews focus on the diff.
  • codesage-feature-reviewer agent: explicit review procedure (trust-boundary tracing, error-path walks, self-audit), contract lookups via find_symbol outside the slice, risk-scaled read depth, lens mode, and severity calibration anchors.
  • assess_risk returns found: false for paths absent from both the structural and git-history indexes; assess_risk_diff returns empty_input: true with a note for empty file lists.
  • MCP tools cap large limits, depth, and file-path lists instead of accepting unbounded request sizes.
  • MCP session_start returns a compact summary plus snapshot_path; the full file list remains persisted in .codesage/sessions/<session_id>.json.
  • find-symbol --kind and find-references --kind reject unknown values instead of running unfiltered.

Fixed

  • The filesystem watcher retries structural/semantic reindexes and removals on a transient busy/locked database instead of dropping the edit, gives up on removals after repeated hard failures so it can still idle out, removes stale rows for empty files, and no longer deletes semantic chunks before a replacement embedding succeeds.
  • review_rehearsal includes high-risk files preserved inside assess_risk_diff.clustered_directories[].top_files.
  • list_dependencies reports found: false with a note for paths absent from the structural index.
  • find_similar MCP params accept min_jaccard as a JSON string number, rejecting non-finite values (nan/inf) that would silently zero results.
  • assess_risk_diff, assess_risk_batch, and recommend_tests MCP calls reject empty file_paths instead of returning successful empty payloads.
  • search no longer creates a table on the query path when the BM25 FTS sidecar is missing, so it works against a read-only index.
  • impact_analysis deduplicates traversal by file, qualified name, and line instead of qualified name alone.
  • search applies every requested language filter to BM25-fused rows.
  • Feature mapping and upserts no longer leave partial feature, route-handler, or trust-boundary state when a run fails mid-pass.
  • A failed vec-table drop or schema migration rolls back cleanly instead of leaving a half-migrated database.

Security

  • .codesage directories and index.db sidecars are created with private Unix permissions; MCP stale-file checks refuse indexed paths that escape the project root.
  • File discovery skips paths that cannot be made project-relative instead of storing absolute/out-of-root paths.
  • Allowlisted embedding and reranker model artifacts are pinned by Hugging Face revision and sha256 before loading tokenizer or ONNX bytes.
  • Embedding batch size is capped at 256 from config, environment, and CLI overrides.
  • Bump crossbeam-epoch to 0.9.20, clearing RUSTSEC-2026-0204 (invalid pointer dereference in fmt::Pointer).

0.14.0 - 2026-07-02

Added

  • MCP tools advertise readOnlyHint / openWorldHint: false; session_start / session_end set readOnlyHint: false.
  • --lock-wait <secs> on codesage index and git-index; installed hooks pass --lock-wait 60. Rerun codesage install-hooks to refresh existing hooks.
  • found boolean on export_context, feature_bundle, and find_coupling results.
  • Migration runner refuses to open an index carrying an unknown breaking_-prefixed migration.

Changed

  • Parser captures Rust grouped/glob/renamed use, PHP group-use, and Python relative imports as import edges; import-cycle detection, assess_risk, and trust boundaries now see them.
  • Parser captures TypeScript abstract classes, JS/TS generators and top-level var, JS/TS re-exports / extends / new, Rust trait method signatures, Go package-level var, C double-pointer returns, and Java enum constants.
  • assess_risk / codesage risk give structure-aware cycle-breaking guidance (ring: lowest-weight break edge; hub: top decoupling targets).
  • CLI --json for find-symbol / find-references / search / similar wraps results in {"results": [...]}.
  • MCP kind / language filter params are typed enums; an unknown value errors instead of returning unfiltered results.
  • The daemon reloads .codesage/config.toml per project on change without codesage daemon stop.
  • Search: BM25-fused scores rescale onto the semantic span; reranking skipped only when fusion contributed; single English words don't take the identifier rerank weight.
  • MCP budget truncation preserves assess_risk_diff.files and records dropped entries in _meta.dropped_files.
  • codesage doctor fails a non-allowlisted model or reranker, naming CODESAGE_ALLOW_ANY_MODEL.
  • codesage export --format rejects an unknown value instead of rendering markdown.
  • codesage cleanup reports failed vec-table drops and exits non-zero when any drop fails.
  • codesage watch run resolves the project root by walking up, matching watch status / stop / start.

Fixed

  • Chunk end_line no longer counts the chunk's trailing newline, fixing symbol misattribution at chunk boundaries.
  • The filesystem watcher retains and retries reindex / removal work under index-lock contention, cancels a pending removal when a file is re-created, respawns after a panic, avoids duplicate spawns, and no longer pins pooled models against idle eviction.
  • A structural-only reindex no longer leaves a deleted file's chunks, FTS rows, or feature refs in the index; remove_file purges them.
  • Non-Linux daemon startup no longer SIGTERMs a recycled pid; siblings are validated against their own socket.
  • assess_risk / review_rehearsal flag Python os.system / os.popen / os.exec / os.spawn / pty.spawn as process-exec; <system_error> no longer tags C++ as process-exec.
  • Feature mapper: Laravel prefix()->group() inner routes inherit the prefix; # in a CMake string no longer truncates targets; Rust workspace-member bins stay out of library slices; Next.js pages-router skips _app / _document / middleware; crates monorepo members don't cross-attach sibling tests.
  • codesage overview / project_overview surface a query error instead of a false zero-risk result.
  • Drift detection classifies a commit-less repository as never-indexed, not "not a git repository".

Security

  • Embedding/reranker model names from repo config are allowlisted before download or load; CODESAGE_ALLOW_ANY_MODEL=1 overrides.
  • The leak-check pre-commit hook is opt-in via codesage install-hooks --with-leak-check, not auto-installed.
  • scripts/leak-check.sh scans staged files with non-ASCII names instead of reading them as empty.
  • codesage install writes agent MCP configs atomically (temp file + rename).
  • The release workflow passes tag values through the environment, not shell interpolation.

0.13.0 - 2026-06-22

Added

  • find_similar MCP tool and codesage similar <name> CLI command: near-clone detection for functions/methods via MinHash over AST structure (identifiers and literals ignored), ranked by Jaccard similarity. Test files are excluded; tune --min-jaccard. Requires a reindex (codesage index) to populate fingerprints (schema migration 0012_symbol_fingerprints).
  • Rust find_references now captures method calls (obj.method()) and impl Trait for Type edges (trait as inheritance, type as type_hint).
  • PHP find_references now emits type_hint references for parameter, constructor-promoted-property, and return type hints; the same signal feeds assess_risk trust-boundary derivation.
  • PHP find_symbol now resolves enum cases (PHP 8.1 case Hearts;) as constants.

Changed

  • codesage init writes a committed .codesage/.gitignore (instead of a per-clone .git/info/exclude entry) so index/session/review state stays out of version control for every teammate, including plugin-onboarded ones.
  • search caps the KNN + reranker candidate pool on deep pagination, so a large offset no longer balloons cross-encoder work (pages past the cap return fewer results).

Fixed

  • impact_analysis / assess_risk reverse-dependency resolution is now import-aware: a reference to an unqualified name is attributed only to the definition the caller imports, not to every same-named definition. Deflates inflated dependent_files blast radius for common names (framework lifecycle methods, shared utilities, trait-method implementations).
  • Import-aware resolution now also keeps same-file callers: a call to a name defined both locally and elsewhere resolves to the local definition (which has no import edge) instead of being dropped, so same-file callers are no longer lost from impact_analysis / find_references.
  • find_similar fingerprinting no longer recurses per AST depth; a pathologically deep source file can't overflow an indexer worker stack and abort codesage index.
  • PHP find_references no longer double-counts in-class trait uses (use SomeTrait;) as both an import and a trait_use.
  • session_start / project_overview top-risk scoring no longer runs a reverse-dependency BFS over every file; on large repos it scores the highest-churn candidates, bounding session-start latency.
  • impact_analysis bounds per-level fan-out, so a symbol referenced by hundreds of files can no longer make traversal unbounded.
  • Feature mapper: PSR-4 namespaces mapping to multiple directories now register every directory; Rust workspace glob members (libs/*) outside crates/ are mapped instead of dropped; php-src #elif/#else branches under #if 0 are no longer blanked (their symbols were lost).
  • FileCategory::classify recognizes ./-prefixed relative paths, so test/config files passed with a leading ./ are categorized correctly.
  • MCP tool schemas no longer advertise schemars' non-standard numeric format values (uint32, uint, float, …), so strict clients (e.g. opencode) stop logging "unknown format" warnings; unsigned params keep their minimum: 0 constraint.

0.12.0 - 2026-06-13

Added

  • The MCP daemon evicts pooled embedder/reranker models after they sit unused (default 15 min, CODESAGE_MODEL_IDLE_SECS, 0 disables), freeing their GPU VRAM and (via malloc_trim) some host memory while keeping the daemon and its client connections alive; the next query reloads them. The daemon now also spawns with MALLOC_ARENA_MAX=2 unless overridden.

Fixed

  • The MCP daemon now reaps stale-version daemons sharing its runtime dir on startup, so a rebuilt or upgraded binary no longer leaves the old daemon pinning a second copy of the embedder + reranker in memory until the idle backstop fires.
  • Structural/metadata commands (status, risk, find-symbol, …) no longer preload the ORT + CUDA stack at startup; only search/index/export and the watch/daemon/mcp --direct servers do. Avoids a CUDA-dlopen abort (exit 134) under restricted sandboxes and the wasted python probe on every structural command.

0.11.0 - 2026-06-12

Added

  • project_overview MCP tool and codesage overview CLI command: one-call project orientation (languages, structural+semantic freshness, feature summary by kind, top-risk files, trust-boundary clusters, per-language test conventions, sample entrypoints, suggested next calls).
  • review_rehearsal MCP tool and codesage rehearse CLI command: predict severity-ranked review objections for a patch (missing tests, high-risk/blast-radius/fix-prone/hotspot files, import cycles, trust-boundary expansion, feature-test gaps, and scope-spread when a patch touches ≥4 unrelated feature areas) from assess_risk_diff + recommend_tests + drift + feature mapping. High-risk objections list the file's hottest symbols. CLI falls back to working-tree changes vs HEAD when no files are given.

Changed

  • impact_analysis gains opt-in include_forward (forward dependencies), include_siblings (same-file symbols), limit, and summary_only controls; the result is now an object with results plus the requested extras (codesage impact --forward --siblings --limit --summary-only). Reading .results is unchanged.
  • find_references now populates each reference's from_symbol with the enclosing caller symbol (null at file scope), and impact_analysis walks the call graph at symbol precision via that field instead of re-deriving callers from line numbers. Requires a reindex (codesage index --full) to backfill existing projects.
  • Live filesystem watcher: the daemon auto-starts a per-project watcher on first tool call that reindexes (structural + semantic) on edit, debounced (default 1s, REINDEX_DEBOUNCE), reusing the daemon's pooled embedder. Honors .gitignore and [index].exclude_patterns and skips ignored top-level trees (target/, .git/, node_modules/) from the watch set. On by default; disable with [index] watch = false or CODESAGE_WATCH=0. Self-exits after idle (CODESAGE_WATCH_IDLE_SECS, default 1800; 0 disables).
  • codesage watch run|status|stop|start [project] to manually control the live watcher (run is a foreground instance with its own embedder).

Fixed

  • CUDA verification now fails when neither cuDNN nor cuBLAS is loaded after GPU session creation.
  • recommend_tests now finds primary sibling tests from the structural file index, independent of git-history rows.
  • CMake target_sources feature mapping no longer records later PUBLIC / INTERFACE scope keywords as owned files.
  • CMake add_executable / add_library feature mapping no longer records option keywords (WIN32, MACOSX_BUNDLE, EXCLUDE_FROM_ALL) as owned files.
  • The live watcher now applies nested .gitignore files consistently with indexing discovery.

0.10.0 - 2026-06-11

Added

  • Add codesage index --batch-size <N> to override embedding batch size.
  • Add target, acceleration, and embedding-device details to codesage --version.
  • Add CoreML embedding and reranker support on Apple Silicon via device = "coreml".
  • Add codesage index --verbose for per-phase indexing progress.
  • Add stale-file metadata to MCP responses via _meta.stale_files.
  • Add tree-sitter query validation to codesage doctor.
  • Add codesage install / codesage uninstall support for Codex and opencode.
  • Add default project injection with codesage mcp --project <root>.
  • Add optional line numbers to export_context and feature_bundle.
  • Add repo-size-based token budgets for export_context and feature_bundle.
  • Add Laravel route-handler references for impact_analysis and find_references.

Changed

  • Default Apple embedding batch size is now 10.
  • macOS builds now link ONNX Runtime with CoreML support at compile time.
  • The MCP daemon now exits after 30 minutes without clients; set CODESAGE_DAEMON_IDLE_TIMEOUT_SECS=0 to keep it alive.
  • Semantic indexing now writes changed files in bounded batches and reports structural and semantic failures separately.
  • MCP tool handlers now run off the async runtime worker threads.

Fixed

  • codesage cleanup no longer treats every vec table as orphaned when the active semantic table is missing.
  • codesage cleanup --dry-run no longer loads the embedder.
  • codesage mcp --project, codesage install, and codesage install-hooks reject non-UTF-8 paths instead of writing lossy values.
  • codesage index now prunes excluded directories, skips unreadable or deleted files, and fails when trust-boundary backfill fails.
  • Chunk merging and overlap now respect chunk_size.
  • impact_analysis rejects ambiguous bare symbols; callee export prefers imported symbols over homonyms.
  • Parser extraction now handles PHP multi-namespace files, Java nested types, and nested functions with correct qualified names and kinds.
  • Feature mapping now drops excluded files before trust-boundary aggregation, deduplicates entry files, includes Rust library files under src/, and ignores disabled PHP extension macros.
  • list_features tag filters now escape SQL LIKE metacharacters.
  • serde_json imports now map to the serialization trust boundary.
  • list_dependencies now reports reverse file importers.
  • ReferenceKind JSON now uses snake_case names and still accepts the legacy spellings.
  • --version handling no longer intercepts subcommand arguments or panics on non-UTF-8 argv.
  • codesage init rejects a non-directory .codesage path.
  • CODESAGE_NVIDIA_LIBS can point directly at CUDA library directories.
  • Unknown device values are rejected instead of falling back to CPU.
  • Search ranking no longer promotes weak matches with negative KNN scores.
  • Reranker score extraction handles rank-1 logits and malformed tensor lengths without panicking.
  • Custom embedding models with already-pooled ONNX output now fail with an actionable error.
  • MCP tool panics now return errors instead of hanging clients.
  • MCP semantic tools now fail on malformed project config instead of creating a default semantic table.
  • The MCP daemon survives transient accept() errors.
  • codesage daemon status and codesage daemon stop can find daemons from previous builds.
  • codesage daemon status and codesage daemon stop use a stable /tmp/codesage-$UID fallback across TMPDIR changes.
  • session-start and session-end validate snapshot paths and loaded session IDs.
  • session-end flushes its report before exiting nonzero.
  • codesage map now takes the project index lock.
  • Generated git hooks shell-quote the codesage binary path.

Security

  • codesage daemon status and codesage daemon stop validate runtime dirs and pid identity before trusting pid files.
  • codesage daemon refuses a symlinked or foreign-owned /tmp/codesage-$UID runtime dir.
  • Non-Linux codesage daemon status / stop no longer trust a stale pid unless the daemon socket is reachable.
  • list_features --since and MCP since reject option-like git refs.

0.9.0 - 2026-06-01

Added

  • Django URL route feature slices. New django-route / FeatureKind::Route seed source. The Python mapper now scans .py files that import from django.urls / django.conf.urls and declare a urlpatterns = [...] list for path(), re_path(), and legacy url() entries, emitting one route feature per distinct normalized URL. path() converter prefixes are stripped (<int:year><year>); re_path() / url() regexes are normalized (drop ^/$ anchors, rewrite (?P<name>…) named groups to <name>, unescape \/ and \.). The view symbol is resolved onto entry_symbol where possible (views.homehome, ArticleList.as_view()ArticleList). Tags are [python, framework:django, route]. Consistent with the existing Flask / FastAPI conservatism: include(...) mounts are NOT expanded (the child URLConf contributes its own routes without the parent prefix) and non-literal route patterns are skipped. Ports clawpatch's Django route mapping (python.ts djangoRouteSeeds). Closes the most common Python web framework gap, since Flask + FastAPI shipped in 0.7.x. Four regression tests cover path() routes with converter stripping, re_path() regex normalization, class-based as_view() symbols with include() skipped, and the django-import gate.
  • auth-sensitive tag on shape-flagged route slices. Route seeds whose shape suggests a privileged or state-changing surface now carry an auth-sensitive tag: the HTTP method is anything other than GET/HEAD, or a path segment is exactly admin / auth / login / token (segment-exact and case-insensitive, so /authors does not match). Applied across all route mappers — Flask, FastAPI, Django (path-shape only, no verb at the URL layer), and the Node Express/Fastify/Hono server routes. Surfaces a route-shape heuristic for humans and agents reviewing a slice; filter with list_features --tag auth-sensitive. Ported from clawpatch's per-route trust-boundary heuristic, but deliberately demoted to a free-form tag: CodeSage's trust-boundary model stays single-sourced from parsed imports/references (trust_boundary_rules.rs), so a file never shows a boundary it didn't earn from a real include/call. route_is_auth_sensitive lives in the shared mapper module with three unit tests (method, path-segment, empty-method path-only).
  • CUDA support: .cu / .cuh indexing, target detection, and concurrency boundary. .cu / .cuh files are now recognized by language detection (mapped to C++, parsed with the tree-sitter-cpp grammar) — so they enter the structural index and show up in search / find_symbol / find_references, where previously they were skipped entirely. They're deliberately kept out of the C++ .h-dialect signal, so a C project carrying a stray .cu file doesn't have its .h headers re-routed to C++. In the C/C++ mapper, CMake cuda_add_executable / cuda_add_library (legacy FindCUDA macros) are parsed alongside add_executable / add_library, and any target — however declared — that pulls in a .cu / .cuh source is tagged cuda; standalone .cu files with a top-level main() get the tag too. Filter with list_features --tag cuda. The security signal is reference-derived, not seed-set: CUDA headers (cuda.h, cuda_runtime.h, cuda_runtime_api.h, device_launch_parameters.h) are added to the C trust-boundary rules as a concurrency boundary (inherited by C++), so any indexed file that includes them — now including .cu/.cuh themselves — surfaces concurrency in assess_risk through the normal per-file derivation. Ports clawpatch's CUDA mapper support (c-cpp.ts, commit 35497e9). Eight regression tests: four in the mapper (cuda_add_executable, plain add_executable with a .cu source, standalone .cu main, non-CUDA negative), two in language detection (.cu/.cuh → C++, and the no-.h-reroute guard), and one each for the CUDA-header→concurrency derivation and the boundary-rule table.
  • --since <ref> on features-list (CLI) and since on list_features (MCP). Restricts the feature list to slices whose entry, owned, or context files changed since a git ref, via git diff --name-only --relative <ref>...HEAD (three-dot merge-base form — "what HEAD changed since it branched from "). Lets an agent scope the feature surface to a diff (e.g. review only the slices a branch touched). The entry file counts because for many slices (Rust crates, route handlers, C main() binaries) the entrypoint is itself the source file, recorded only with the Entry role; test-role siblings are excluded so an unchanged slice doesn't surface on a neighbouring test edit. Unknown refs error out rather than silently returning nothing. When combined with a result limit, the diff intersection runs before the cap so the limit isn't consumed by features the filter would have dropped. New codesage_graph::changed_files_since helper wraps the git call, mirroring clawpatch's changedFilesSince.
  • Qualified-name boost on search, opt-in via CODESAGE_QUALIFIED_NAME_BOOST=1. New apply_qualified_name_boost stage runs after symbol annotation and applies a ×2.0 multiplicative boost to chunks whose annotated symbol qualified-name matches a query-derived identifier token. Different from the existing additive apply_symbol_boost (which matches tokens against raw chunk content) in two ways: this one matches against the chunk's overlapping symbols' qualified-names, and the boost is multiplicative rather than additive. An anti-trigger filter (39 tokens covering Rust trait methods, common accessors, common verbs that double as method names) suppresses the boost when the matching segment is the qualified-name's leaf (e.g. query default matching Mode::default doesn't fire). Validated across three rounds: a 15-case smoke (1 win / 0 losses after the anti-trigger filter) and a 138-query / 7-repo semble subset (net wash on miss rate and recall@10, modest mean-first-hit improvement, strong corpus-dependent split — wins on heavily-typed framework code like monolog and laravel-framework, losses on lighter library code where NL query tokens false-positive on unrelated qualified-names). Default-off because the lift isn't reliable enough across corpora to be everyone's default; opt-in for users whose corpus profile aligns with the wins. Full validation reports at notes/20260526-search-lift-ab-report-round2.md and notes/20260527-search-lift-ab-report-round3.md. Ports half of code-review-graph commit c04af36; the companion change (identifier word-split injected into embedded text at index time, requiring a re-embed pass) is deferred to a future session with GPU time budgeted.

Fixed

  • CMake test-target classification respects target-name precedence. is_cmake_test_executable was using pure OR logic (target name matches test pattern OR any source matches test path), so a regular binary like add_executable(app src/main.c src/test_mode.c) flipped to cmake-test / TestSuite because the helper source test_mode.c matched the test-path heuristic — even though the target name "app" is clearly not a test. Now target-name match wins outright; source-pattern match alone (with a neutral target name) only flips the classification when every compilable source matches the test heuristic. Header-only target lists fall through to the regular binary path. Mirrors clawpatch commit de82d0a and the §2.13 audit memo. Four regression tests cover the test-helper-doesn't-flip case, name-takes-precedence, all-sources-test-shaped, and the header-only no-compilable-sources guard.
  • Automake $(srcdir) source-directory variable expansion. bin_PROGRAMS / lib_LTLIBRARIES source lists containing $(srcdir)/foo.c or ${srcdir}/bar.cpp previously fell through to prefix_dir verbatim, producing broken paths like subdir/$(srcdir)/foo.c in entry_path and owned_files. New expand_automake_vars helper strips $(srcdir)/ and ${srcdir}/ to the empty prefix (the value the makefile's own directory implicitly contributes, which the existing prefix_dir(makefile_dir, ...) pipeline already adds). $(top_srcdir) and $(top_builddir) references are left verbatim — handling them correctly would require subtracting the makefile-dir prefix that gets re-added downstream, work not worth doing until a real user case demands it. User-defined macros ($(SOURCES)/foo.c) also stay verbatim. Mirrors clawpatch commit 39a2545. Five new regression tests cover the strip, the top-srcdir leave-alone, unknown-vars passthrough, plain-path passthrough, and a Makefile.am-in-subdir integration test that verifies $(srcdir)/thing.c lands at subdir/thing.c end-to-end.
  • CMake ${PROJECT_NAME} target-name resolution. add_executable(${PROJECT_NAME} src/main.c) / add_library(${PROJECT_NAME} ...) were previously skipped because the literal ${PROJECT_NAME} failed is_valid_target_name (which rejects any token containing $). New cmake_project_name helper pre-scans the CMakeLists.txt body for project(NAME ...) calls (last call wins per CMake semantics) and resolve_cmake_target_name substitutes ${PROJECT_NAME} and ${CMAKE_PROJECT_NAME} with the captured name before the validity check. When no project() call exists, the unresolved variable still fails validity and the target is dropped — the existing behavior, just no longer false-dropping the common case. Applied to add_executable, add_library, and target_sources so late-bound sources attach to the resolved target. Mirrors clawpatch commit 8550604. Four new regression tests cover last-project-wins, absent-project-stays-unresolved, canonical+CMake-namespaced variants, and an integration test that exercises the full mapper pipeline on project(myapp) + add_executable(${PROJECT_NAME} src/main.c).
  • MCP daemon runtime-dir fallback treats empty env vars as unset. The wiki-documented WSL2 workaround for the set-but-unusable /run/user/$UID trap injects XDG_RUNTIME_DIR="" in the client's MCP env so the shim falls through to /tmp/codesage-$UID. The fallback in default_runtime_dir was reading both CODESAGE_DAEMON_RUNTIME_DIR and XDG_RUNTIME_DIR via std::env::var_os, which returns Some("") for set-but-empty vars rather than None. The empty string then collapsed to a relative codesage/ runtime dir that the daemon tried to create next to whatever the shim's cwd happened to be at spawn — silently littering arbitrary directories with codesage/mcp-<version>-<hash>.{sock,pid,lock,log} when cwd was writable, and surfacing Failed to connect for directories where it wasn't. Both env reads now treat an empty value as unset so the documented workaround behaves the way the wiki promised; the /tmp/codesage-$UID fallback also kicks in for any client that explicitly blanks the var. Regression test in crates/cli/src/daemon.rs asserts the resolved path is absolute and rooted under std::env::temp_dir() when both vars are empty.
  • Reranker model loading matches the embedder's path. Reranker::new and Embedder::new now share one load_onnx_session loader (an internal dedup), which closes two reranker-only gaps: the reranker now fetches the optional onnx/model.onnx_data external-weights sidecar — so a >2 GB cross-encoder no longer fails at session commit — and, when device = "gpu" is configured on a binary built without the cuda feature, it fails fast before downloading the model instead of after. No config or API change.

0.8.1 - 2026-05-20

Added

  • React component feature slices. New react-component / FeatureKind::Library seed source, one feature per .tsx / .jsx file under conventional component dirs (src/pages, src/components, top-level components/) of every package that declares a React dependency. Capped at 100 components per package. Stories / .storybook/ / fixtures / testdata/ / .d.ts / .test.tsx / .spec.tsx files are excluded. Files already owned by a react-router-route, next-app-route, next-app-page, or next-pages-route seed are skipped so a route declaration file doesn't double-emit. Gives find_feature("src/components/Button.tsx") an answer (previously returned nothing for unrouted components) and feature_bundle a single-component target for review. Ports clawpatch react.ts componentSeeds (commit af0ad0e). Component name is derived from the basename stem; tags are [language, "react", "react-component"]. Five new regression tests cover: per-file emission across both src/pages and src/components; stories / tests / .d.ts / stories / fixtures exclusion; route-owned file deduplication via the shared exclusion set; the no-React-dependency gate; and the 100-per-package cap enforced at the file walker.

Changed

  • CMake test-executable classification. add_executable(target …) where the target name matches (^|[_-])tests?$ (case-insensitive) OR any source path matches the conventional C/C++ test pattern now emits a cmake-test / FeatureKind::TestSuite seed instead of cmake-bin / CliCommand. So add_executable(foo_tests test_main.cpp) lands in recommend_tests (and stays out of list_features --kind cli-command) rather than being recorded as a shippable binary. Ported from clawpatch commit f21b76c. Test-path detection uses a self-contained is_c_or_cpp_test_path helper mirroring upstream's isCOrCppTestPath: under tests/ / test/ / __tests__/, basename prefix test_ / test-, stem suffix _tests / -test (and plural), or FooTest.cpp / BarTests.cc. Four new regression tests cover name-based classification, source-path-only classification, the non-test negative case, and the helper's full pattern set.
  • Diagnostic on upstream C/C++ fix-storm (~25 commits since 2026-05-16): audited the rest of clawpatch's C/C++ mapper churn against our Rust port. The headline rewrite (cmakeCommandArgs paren-depth scanner, clawpatch 162a6fe) is now ported (see the Fixed section below). Two categories remain not ported because they don't apply: (1) regex anchoring fixes (06a7f4a, 5eea752). Our Rust port already uses (?m)^\s* line anchors and is not vulnerable to the mid-line false-match the TS regex hit. (2) catastrophic-backtracking fixes (496232f). The Rust regex crate uses NFA-based matching with linear time guarantees and isn't subject to the backtracking pathology these commits address in the JS engine. Remaining upstream commits (deferred sources, Makefile.in comments, bracket comments, header-only drop, build-tree exclusion, c-main test-path skip) were already in our 0.8.0 port of clawpatch PR #26 and didn't need re-porting.
  • Flask / FastAPI route detection matured to upstream parity. Both mappers (existing since 0.7.x) now handle four cases that previously silently dropped routes or attached the wrong metadata: (1) @app.api_route('/x', methods=[…]) variadic decorators on FastAPI; (2) multi-line decorators whose argument list wraps across newlines, via paren-depth tracking instead of the prior single-line regex; (3) methods=(…) tuple and methods={…} set literals as alternatives to the previously-only-supported methods=[…] list; (4) stacked decorators between the route decorator and the handler def (e.g. @app.get('/admin') / @auth_required / def admin_panel():). The handler function name now lands on the seed's entry_symbol, so find_feature and feature_bundle return the actual handler in addition to the route; previously the field was None for every Python route. api_route without an explicit methods= kwarg is dropped rather than guessed at, since the method set is undeterminable. The regex-driven matcher is replaced with a line-based scanner. Eight new regression tests cover api_route expansion, multi-line decorators, tuple/set method containers, function-name capture for both frameworks, stacked decorators, and the api_route-without-methods= drop. include_router(prefix=…) mount prefixes are still NOT expanded. Upstream clawpatch's commit message claims this work, but the implementation doesn't.

Fixed

  • CMake parser respects quoted args and bracket comments around ). The previous regex used [^)]* to capture the source list, which terminated at the first ) it saw, including ) characters that legitimately live inside "..." quoted args (e.g., a vendored source named "src/foo(v1).cpp") or inside message("add_executable(...)") / message([[add_library(...)]]) documentation calls. Those bugs produced two failure modes: (a) truncated source lists where everything after the first stray ) was silently lost, leaving owned_files with a half-token like "src/foo(v1; (b) phantom features when command-like text inside a string literal got picked up as a real declaration. Replaced the three add_executable / add_library / target_sources regexes with cmake_command_args, an explicit walker that tracks paren depth and skips over quoted strings ("...", including \X escapes) and bracket arguments ([[...]], [=[...]=]) so a ) inside a string is treated as ordinary content. Word-splitting moved into cmake_split_args, which preserves quoted tokens as single words (so "src/main file.cpp" survives as one source path instead of splitting on the space) and applies CMake's ; list-split rule only to unquoted words. Ports clawpatch commit 162a6fe. Six new regression tests cover string-embedded commands, bracket-arg-embedded commands, quoted paths with spaces, quoted paths containing ), library-type-keyword stripping (SHARED / STATIC etc. no longer leak as phantom sources), and direct unit tests for the walker primitives. The existing 23 C-mapper tests still pass unchanged.
  • install-hooks and doctor no longer refuse repos where core.hooksPath is set to a path that already resolves to the default <git_common>/hooks. The redundant setting (seen on PHP-extension repos that share a config template) is treated as if unset instead of erroring out.

0.8.0 - 2026-05-17

Added

  • Java as a supported language. .java files now parse class, interface, enum, record, method, constructor, field, and @interface (annotation type) symbols plus method-call, new expression, inheritance, import, and annotation-usage references (@Override, @Test, @Component, including @pkg.Foo scoped forms). Multi-declarator fields (String x, y, z;) emit one symbol per declarator. Bumps the supported-language count from eight to nine.

  • Python rationale extraction. find_symbol and export_context now populate Symbol.rationale[] for Python definitions whose attached # comment or leading docstring begins with one of the recognized markers (WHY:, NOTE:, IMPORTANT:, FIXME:, HACK:, XXX:, TODO:). Covers two attachment shapes: contiguous # line comments immediately above a def / class (one-line gap allowed, breaks on the first non-comment node) and the first (string) child of the body when it's a triple-quoted """...""" / '''...''' docstring. Decorated defs are handled by anchoring the prev-sibling walk at the decorated_definition wrapper so # TODO: above @app.route(...) / @property / @dataclass / @lru_cache etc. still attaches; the walker also climbs from a class body's first method into the class header so comments tree-sitter parks at class_definition level still attach. Markers inside string literals (e.g., s = "# TODO not a comment") do not trigger because the extractor reads tree-sitter comment nodes, not raw source.

  • Shared MCP daemon for codesage mcp. The configured MCP entrypoint now starts as a stdio shim and proxies MCP JSON-RPC over a per-user Unix socket to one long-lived CodeSage daemon. Main sessions and subagents therefore share the same project cache plus embedding/reranker model pools instead of each codesage mcp process loading its own copy. The socket lives under $CODESAGE_DAEMON_RUNTIME_DIR, $XDG_RUNTIME_DIR/codesage, or /tmp/codesage-$UID with private permissions; the socket name includes the running binary's version and executable metadata so a freshly rebuilt binary does not attach to an older daemon. codesage mcp --direct preserves the previous in-process stdio server path for debugging, and codesage daemon runs the foreground daemon explicitly. HTTP/SSE remains intentionally out of scope.

  • Daemon management subcommands. codesage daemon status prints the running daemon's pid, socket path, reachability, and log path (exit 1 when not running). codesage daemon stop sends SIGTERM and waits up to 10 s for clean shutdown. Both commands operate against the same per-binary daemon key the shim uses, so they apply to the right daemon without manual pid wrangling.

Changed

  • MCP daemon hardening (review pass). Concurrency, lifecycle, and operational fixes after the daemon's initial review. Concurrency: search / export_context no longer hold the embedder + reranker locks across SQL retrieval. Embedding runs under a tight lock, reranking acquires its own lock only when invoked, so concurrent agents on the same project interleave SQL work instead of serializing. Cold model loads serialize per model key (per-key Mutex<Option<...>>) so two concurrent first-callers can't both construct separate ORT / CUDA sessions. Startup races: daemon startup uses a bounded retry loop with PID-based lock liveness, so a dying spawner no longer strands subsequent shims, and the misleading "another starter still holds" error is replaced with explicit "did not become ready (lock holder alive) — see " or transparent recovery when the holder is genuinely dead. wait_for_socket polls the spawned daemon's PID; if it exits before binding (model load failure, etc.) the shim fails fast instead of spinning the full 5 s timeout. Security: socket bind tightens the process umask to 0o077 so the socket is born with restricted permissions; on accept, the daemon verifies peer-credentials via SO_PEERCRED and refuses connections from foreign UIDs (defense in depth for the $CODESAGE_DAEMON_RUNTIME_DIR override). Lifecycle: SIGTERM / SIGINT trigger graceful shutdown that removes socket + pid files; serve_client enforces a 1-hour per-connection ceiling so a deadlocked tool call can't pin an MCP session indefinitely; daemon log rotates at 4 MiB and keeps three generations (.log, .log.1, .log.2). Shim: proxy_stdio selects on first-direction-to-complete instead of waiting for both, so when the daemon closes its write half the shim exits immediately instead of blocking on stdin forever (pre-fix MCP-session hang). Daemon key derivation uses a stable FNV-1a hash instead of DefaultHasher; on non-Unix, --runtime-dir is now rejected with an explicit error.
  • PHP extension API feature slices. The PHP mapper now scans native extension implementation files for PHP_METHOD(Class, method) and PHP_FUNCTION(name) definitions and emits one php-ext-method / php-ext-function library feature per public API entrypoint. This applies to both repo-root Composer/PIE extensions (config.m4 / config.w32 + php_<name>.h) and php-src-style ext/<name>/ directories. Macro examples inside C/C++ comments and strings are stripped before matching so commented-out methods do not become feature records. API slices keep PHP language semantics so .phpt tests under tests/ or ext/<name>/tests attach through the existing nearby-test logic, while the coarser extension/module slices remain available for whole-extension navigation.
  • Node.js server route mapping (express-route, fastify-route, hono-route seed sources). Ports clawpatch PR #47's deterministic route detection for Express, Fastify, and Hono servers. Two-pass scan per source file: first identify route receivers from local variable declarations matching framework constructors (const app = express(), const router = Router(), const router = express.Router(), const fastify = Fastify(…), const app = new Hono(…)); then match <receiver>.(get|post|put|patch|delete|options|head|all)('/path', handler) calls against those receivers and emit one route feature per match. The walker pre-strips // / /* */ comments and ` ` template-literal bodies so doc-comment examples and \app.get(…)`templates don't false-match. Constructor scan additionally blanks regular"…"/'…'string contents so"const app = express()"written inside a string doesn't fabricate a receiver. Gated on the rootpackage.jsondeclaringexpress, fastify, hono, or @hono/*in deps/devDeps, so the per-file scan cost stays off non-server JS repos.entry_routeencodes"METHOD path"(matching the existing laravel-route shape) so two methods on the same path don't collapse to a single feature_id. Conservative on purpose: cross-file mount prefixes (Expressapp.use('/api', router), Fastify register, Hono route`) are NOT resolved; emitting the inferred path would mislead more than it'd inform. Non-literal route paths are skipped. Eight regression tests cover Express direct + Router patterns, Fastify, Hono, comment/string/template false-match avoidance, receiver-must-be-framework-constructor, no-server-dep gate, and test-file exclusion. Smoke-tested on a 3-framework fixture: 6 declared routes → 6 features emitted with correct method+path encoding.
  • Python web route mapping (flask-route, fastapi-route seed sources) and setup.cfg console-script support (setup-cfg-script). Ports clawpatch PRs #11 (Flask), #15 (FastAPI), and #28 (setup.cfg). Flask: scans .py files that import flask for @<receiver>.route('/path', methods=[…]) decorators where receiver is a local variable initialized from Flask(…) or Blueprint(…). Defaults to GET when methods= is absent. Emits one route feature per (method, path), so methods=["GET","POST"] produces two seeds with entry_route "GET /users" and "POST /users" rather than collapsing to a single feature_id. FastAPI: same pattern against @<receiver>.METHOD('/path') for app = FastAPI() and router = APIRouter(). Both walkers require an import flask / from flask import … (or fastapi equivalent) so a string literal that happens to mention the framework doesn't false-trigger. Known limitations matching clawpatch: blueprint url_prefix and include_router(prefix=…) mount prefixes are NOT expanded; non-literal paths/methods are intentionally skipped. setup.cfg [options.entry_points] console_scripts = name = module:fn (INI-style multi-line value) is now parsed into the same shape as pyproject.toml [project.scripts]. Module path resolves through resolve_script_module_path so entry_path lands on the real .py file (e.g. acme/cli.py) rather than the manifest. Seven new regression tests cover Flask GET-default, methods-kwarg expansion, Blueprint receivers, the no-flask-import gate, FastAPI METHOD/APIRouter recognition, and setup.cfg console-scripts with module resolution. Smoke-tested on a 3-file fixture: 2 Flask + 2 FastAPI + 1 setup.cfg → 5 expected features emitted alongside the python-project seed.
  • Laravel slice expansion: eight new application-layer surfaces ported from clawpatch PR #5, complementing the existing controller / form-request / Artisan-command coverage. New seed sources: laravel-project (composer.json + artisan + bootstrap/app.php as a single Service anchor with framework:laravel tag, plus phpunit.xml / .env.example / config/app.php / config/database.php / routes/{web,api,console}.php as context_files), laravel-job (per-class under app/Jobs/**, FeatureKind::Job, tagged job + async), laravel-service (app/Services/**, FeatureKind::Service), laravel-model (app/Models/**, tagged eloquent + database), laravel-migration (grouped Config slice over database/migrations/**, owning up to 50 files because migrations rarely make sense one-at-a-time), laravel-seeder (same grouped shape for database/seeders/**), laravel-test-suite (one TestSuite per existing tests/{Unit,Feature,Integration,Browser} directory, owning up to 50 files each, test_command = "composer test"), and composer-script (composer.json scripts entries restricted to an allowlist of setup, dev, test, typecheck, lint, format, analyse, analyze, deploy* to keep list_features from filling with one-off pipeline scripts; the test script lands as FeatureKind::TestSuite with the composer-test command attached). Smoke-tested on a real 1670-file Laravel application: 326 newly-created features atop the existing 182 (208 services, 94 models, 19 jobs, 2 composer scripts, plus one each of project / migration / seeder). Six new regression tests cover composer-script allowlisting, the project seed shape, per-class job/service/model emission, migration grouping, per-directory test suites, and the non-Laravel-project gate.
  • JS monorepo Next.js routes + nested-frontend discovery + Turbo test_command (three related improvements to the JavaScript / TypeScript mapper ported from clawpatch PRs #18 (monorepo Next), #4 (nested frontend roots), and #37 (Turbo task graph)). (1) Per-package Next.js routes. next_app_routes / next_pages_routes previously only scanned the repo root's app/ and pages/, so a monorepo with apps/web/app/... or apps/marketing/pages/... emitted zero Next route features. Both now run relative to each discovered workspace package root and emit per-route seeds with the workspace tag and a summary that names the owning package. (2) Nested frontend root discovery. When the repo root has no app/ or pages/ AND no workspace package covers them, the mapper additionally probes the conventional dirs frontend/, client/, web/, ui/ for a nested Next/Vite app one level down. Lets a Rust-or-Go-backed repo with a React frontend in frontend/ get its routes mapped without declaring an npm workspace. (3) Turbo-aware test_command rewrite. When turbo.json exists at the repo root AND declares a test task (in either Turbo 1.x pipeline or 2.x tasks shape), every workspace package's test_command flips from the bare <pm> --dir <pkg> test shape to turbo run test --filter=<package-name>, so agents get dependency-aware orchestration with cached output reuse instead of running the bare test runner. Repo-root seeds keep their plain command (root-level tests don't filter to a package). Four new regression tests cover per-package Next route emission, nested-frontend discovery when the root has no app/, the Turbo rewrite when a test task is declared, and the negative case where turbo.json exists but only declares build. Smoke-tested on a synthetic monorepo: 3 packages + 3 routes (2 from apps/web app-router, 1 from apps/marketing pages-router) all mapped, all three packages got turbo run test --filter=@m/<pkg>.

Fixed

  • C/C++ CMake mapper hardening. Eleven gaps surfaced by porting clawpatch PR #26's test fixtures: (1) uppercase keywords (ADD_EXECUTABLE, ADD_LIBRARY) now match; the regex is case-insensitive. (2) Numeric-prefix target names (7zip) and dotted names (foo.bar) are accepted; the name regex was previously anchored to [A-Za-z_] start with no . allowed. (3) Late-bound target_sources(name PRIVATE|PUBLIC|INTERFACE src…) declarations are now parsed and merged into the matching add_executable / add_library target; previously a target declared with add_executable(name) and sources attached separately became an empty seed. (4) CMake bracket comments (#[[ … ]] and #[=[ … ]=] with any equals count) are stripped before regex matching, so commented-out targets no longer leak through as real features. (5) Sources containing variable substitutions (${APP_SOURCES}) cause the target to be skipped rather than emitted with a phantom owned-file entry. (6) Absolute-path sources (/src/main.cpp) are similarly skipped. (7) Header-only executables (sources all .h/.hpp/.hh/.hxx) are dropped, since these can't link as binaries. (8) Targets whose owned_files is empty after exclude filtering (e.g., a vendored INTERFACE library whose only file lives under vendor/) are dropped rather than emitted as phantom seeds. (9) INTERFACE libraries with only header sources still surface so reviewers can see the API. (10) **/CMakeFiles/** added to HARD_EXCLUDE_PATTERNS (joining the existing **/cmake-build-*/**) so the CMake build-tree state directory stays out of every index. (11) The c-main walker now skips files under tests/, test/, __tests__/, Tests/ or with _test/Test stem suffixes, so googletest/catch2/custom harness main() files no longer surface as CLI features when the project hasn't configured [index].exclude_patterns. Smoke-tested on libharu (3 features incl. one cmake-bin from a literal target name) and lexbor (81 features, 0 test-path c-main leakage, 0 panics on variable-heavy CMake).
  • Laravel route parser hardening: four longstanding gaps that left real routes mis-attributed or under-prefixed, ported from clawpatch PR #5's route extractor. (1) prefix() fluent chain expansion. Route::prefix('admin')->get('/users', …) now resolves to /admin/users; nested chains like Route::prefix('a')->prefix('b')->get('/c', …) resolve to /a/b/c. (2) routes/api.php implicit /api prefix. Laravel's default route service provider auto-prefixes API routes; the mapper now applies the same default so the recorded URIs match what the framework actually serves. (3) use-import resolution. Route::get('/x', [UserController::class, 'show']) paired with use App\Http\Controllers\UserController; (or an as Alias rename) now bridges the route back to the controller file via the fully-qualified class name, instead of failing to match because the route stored only the short class name. (4) Route::controller(X::class)->group(fn () => { … }) body expansion. Inner routes inside a controller-group closure now surface as individual route features attributed to the outer controller, with any outer/inner prefix() chains applied. A consumed-spans tracker prevents the same registration from emitting twice (once from the group pass, once from the top-level scan). Internal: parse_laravel_routes collapsed from two regexes-plus-by-key-map to one route regex + one controller-group regex; new helpers parse_php_use_imports, resolve_imported_class, fluent_route_prefixes, file_default_route_prefixes, route_uri_with_prefixes. Six new regression tests cover each pattern, plus alias-form imports. Smoke-tested on a real 1670-file Laravel application: 86 routes now carry the previously-missing /api/ prefix, 89 routes got new feature_ids (URI changed because prefix expansion or controller-group bridging filled in).
  • Non-ASCII route paths preserved through JS strip pass. The strip_js_impl helper that pre-strips comments / strings / template literals before the Express/Fastify/Hono route regex iterated bytes and emitted b as char, mangling every non-ASCII UTF-8 byte into its Latin-1 codepoint (é C3 A9é C3 83 C2 A9). Any internationalized URL (/café, /привет, /ürün) was stored corrupted in entry_route, and because that field feeds the feature_id hash, find_feature("/café") from an agent never matched. Rewritten to walk by char instead of bytes, so non-ASCII runs round-trip verbatim. Two new regression tests cover Latin and Cyrillic route paths.
  • Parser worker no longer panics on 1-byte string captures. extract_references previously did to_name[1..to_name.len() - 1] with no length guard, panicking with slice index starts at 1 but ends at 0 whenever tree-sitter emitted a (string) node containing just one quote character (possible on malformed/truncated C/C++/JS/TS source). The panic propagated out of the rayon-parallel worker and aborted the whole index run. Extracted into strip_surrounding_quotes with len >= 2 guard plus six unit tests covering balanced quotes, single bare quotes, empty input, and mismatched quotes.
  • Workspace-only React/server deps now trigger route mapping. The React Router and Express / Fastify / Hono route walkers gated only on the root package.json's deps. A monorepo declaring react in apps/web/package.json (root has no React dep) missed every React Router slice; same for Express in apps/api/package.json. New any_package_has_dep helper ORs the root package with every discovered workspace package. react_router_routes also now scans src/+app/ relative to each discovered package root, so the regex actually has files to walk in monorepos. Two new regression tests cover workspace-only React and Express deps.
  • React <Route> regex now matches any prop order, including nested JSX. Previous regex required path= before element= AND broke on element={<UsersPage />} because the nested /> ended the [^>]* capture early. Replaced with extract_route_tag_bodies, a {}-depth-aware byte scanner that captures the full attribute body, followed by independent path= / element= regex passes. Matches <Route element={<X />} path="/users" /> (element-first) and <Route path="/users" element={<X />} /> symmetrically.
  • list_features tag filter now matches substrings as documented. The SQL bound %"{tag}"% (literal quote anchors around the tag), so tag="framework" missed compound tags like framework:react-router despite the docstring promising "tag substring" semantics. Changed to %{tag}%. New crates/storage/tests/features_test.rs integration test covers substring matching across compound tags, full-tag exact match, and the non-matching-substring negative case.
  • Post-commit / post-merge / post-checkout / post-rewrite git hooks no longer race their two passes. The generated hook body launched codesage index and codesage git-index --incremental in parallel with &. Both take the same project lock and exit 0 on contention, and the hook redirects stdout/stderr to /dev/null, so whichever lost the race silently skipped after every commit with no visibility. Now sequenced inside one background subshell: ( cd "$root" && bin index && bin git-index --incremental ) >/dev/null 2>&1 &. Existing installations re-run codesage install-hooks to pick up the new body. Hook-body generation extracted into generate_post_commit_hook_body with two regression tests asserting && chaining and a single top-level &.
  • bench/concurrency-audit.py backups now snapshot the full database. backup_db copied index.db without checkpointing; restore_db deleted the live -wal/-shm siblings and overwrote index.db with the un-checkpointed snapshot, silently dropping any committed-but-not-yet-checkpointed transactions. Common after a fresh codesage index because WAL checkpoints are lazy. Now runs PRAGMA wal_checkpoint(TRUNCATE) before shutil.copy2, so the .db snapshot is a self-contained image of the database.
  • bench/concurrency-audit.py aborts when the checkpoint is contested. PRAGMA wal_checkpoint(TRUNCATE) returns (busy, log, checkpointed); a busy=1 row means WAL frames remain on disk and the .db snapshot is stale. The earlier fix issued the pragma without reading the result, so when the per-user codesage daemon held a read transaction on the project's index.db (the common developer setup) the audit silently regressed to the pre-fix data-loss state. The script now reads the row and exits with a "stop the daemon and re-run" message when busy ≠ 0.
  • bench/agent-tool-selection-harness.py --append-system-prompt-file works. The harness was passing --append-system-prompt-file <path> to claude -p, which is not a real flag. claude accepts only --append-system-prompt <prompt>. Every task in a run using the option silently exited with rc!=0 and an empty tool-call list, invalidating the §2.3 prompt-steering measurement. The flag now reads the file and inlines its contents; subprocess stderr is also echoed when claude exits non-zero so a future flag rename doesn't nuke another run unobserved.
  • bench/extract-eval-cases.py no longer leaks sibling-directory paths into the corpus. normalize_path did a raw startswith(project_root) followed by a length-based slice, so files under sibling directories whose name extends the project root's path (<root>-test/, <root>_ref/...) were accepted past the prefix check and emitted with malformed relative paths starting in - or _. Rewritten with Path.resolve() + is_relative_to so the comparison is on path segments, not character offsets.
  • bench/generate-llm-corpus.py survives a hung codex exec call. A single 180s subprocess.TimeoutExpired used to propagate out of the per-file loop and discard every successful codex response collected so far. Now caught with the same "log and skip" treatment as rc!=0, so a flaky case costs one wasted invocation instead of the whole run. Missing codex binary surfaces an explicit error instead of an opaque FileNotFoundError.
  • Reranker score extraction respects the output tensor's shape. score_batch was reading the first batch_size entries of the flat logits tensor, which is correct only when num_labels == 1 (the ms-marco-MiniLM-L6-v2 default). For any reranker with num_labels > 1 (binary-classifier or NLI-style cross-encoders configured via [embedding].reranker in .codesage/config.toml) every score after the first came from the wrong row, silently scrambling the rerank order. Now extracts the positive-relevance column at the correct stride and softmax-normalizes for multi-class heads; one-time tracing::info! of the detected shape lands in the daemon log. Three new unit tests cover the single-label, binary-classifier, and three-class shapes.
  • CMake comment stripper preserves UTF-8 paths. strip_cmake_comments walked the source byte-by-byte and emitted byte as char, mangling every multibyte sequence into its Latin-1 codepoint (so add_executable(app src/café.c) produced a feature whose entry_path could not be matched back to the real file). Now copies non-comment runs as full Unicode scalars; the bracket-close marker scan still uses byte offsets because ], =, #, [ are pure ASCII and can never collide with a UTF-8 continuation byte. Same bug class as the JS strip-pass fix that landed in this Unreleased section.
  • Laravel route parser ignores commented-out Route::* calls. parse_laravel_routes ran its route regexes directly against routes/{web,api,console,channels}.php's raw text, so // Route::get(...), # Route::post(...), and /* Route::delete(...) */ all matched and emitted phantom route features. Routes are now passed through strip_php_comments_preserving_strings first, a new helper that blanks ///#//* */ comments while keeping string literals (route paths are string literals) and byte offsets intact so the cross-pass consumed_spans dedupe still works.
  • impact_analysis on .java targets resolves as a file. looks_like_file_target was missed when Java was added to the supported-language set, so a bare target like UserService.java got classified as a symbol, hit symbol lookup, found nothing, and returned an empty result silently. .java is now in the file-extension allow-list.
  • FileCategory::classify no longer flags Latest.java / Manifests.java / Latest.php as tests. The Java and PHPUnit arms lowercased the path and then matched test.java, tests.java, and test.php without a separator, so any source file whose name happens to end in those substrings got dropped from impact_analysis with source_only=true. Tightened to require the standard uppercase-T convention (*Test.java, *Tests.java, *Test.php) on the original path.
  • codesage daemon startup test no longer races. The integration test's wait loop in crates/cli/tests/mcp_daemon.rs exited as soon as the .sock file appeared, but the daemon writes the .pid file a few statements later. A read_dir scan landing in that window left pid_file = None and expect(...) panicked spuriously on busy CI runners. The loop now waits for both files.
  • codesage mcp stdio shim no longer preloads the CUDA/cuDNN stack. main() unconditionally ran init_for_main() before clap parsed argv, so every invocation (including the per-agent stdio shim) dlopen'd libonnxruntime.so plus the full NVIDIA dylib set (libcublasLt, libcudnn_*, libcufft, libnvrtc, libcurand, etc.) before falling through to a tokio::io::copy loop between stdin and the daemon's Unix socket. Per-shim RSS measured at ~238 MB / ~4.3 GB virtual on the previous build despite the shim never constructing an Embedder or Reranker. The constraint behind the eager call (std::env::set_var is unsafe under Rust 2024 and must run before any thread spawns) only applies when an env var actually needs to be set, which the shim never does. main() now skips init_for_main() when argv resolves to mcp without --direct; all other commands (including codesage daemon, codesage mcp --direct, and every model-loading subcommand) continue to preload eagerly. Measured shim RSS after the change: 9.2 MB (26× drop), with zero CUDA/cuDNN libraries mapped into the process. Five regression tests lock in the argv detector's positive and negative cases.

0.7.5 - 2026-05-16

Added

  • Subagent-driven review workflow in the codesage-tools plugin. Four new slash commands plus one subagent definition: /codesage-review (orchestrator — fans out feature-slice reviews in parallel via the new codesage-feature-reviewer subagent, persists findings to .codesage/findings/<feature_id>.json with run records under .codesage/reviews/), /codesage-triage (pure local state edit — mark a finding open / false-positive / wont-fix / fixed with an audit-trail history), /codesage-revalidate (re-runs the subagent against the slice to confirm a finding is fixed or still applies; auto-flips openfixed when the defect no longer surfaces), /codesage-report (deterministic Markdown render of the findings JSON; no LLM call). Subagent runs read-only (autoApprove: read), consumes the existing codesage MCP surface (feature_bundle, assess_risk, find_references, find_coupling), inherits the host session's model, and returns JSON findings the orchestrator parses out of a fenced code block. Architectural choice: codesage's core remains read-only; the review workflow lives in the plugin and writes findings to gitignored JSON files that other tooling can consume. Plugin version bumped 0.1.0 → 0.2.0.

0.7.0 - 2026-05-16

Added

  • assess_risk (and assess_risk_batch / assess_risk_diff by extension) now returns an optional top_symbols field on each RiskAssessment: up to five symbols inside the file ranked by ln(1 + line_count) + ref_count + (in_cycle ? 1.0 : 0.0), each carrying a one-line why ("hot: 142 lines, 38 refs, in 7-file cycle"). Closes the agent's natural follow-up to a high score — "which symbols inside the file drive it?" — without a second tool call, so PR descriptions can quote the contributing symbols by name. Empty (and omitted from JSON) when the file has no indexed symbols. CLI codesage risk <file> renders a Top symbols: block when present. Adopted from repowise's hotspot drill-down via the 2026-05-16 reference-tool sweep.

  • Every MCP tool now advertises an outputSchema (MCP spec field). Tool definitions in crates/cli/src/mcp.rs pass output_schema = schema_for_type::<T>() to the #[tool(...)] macro; protocol return types (Symbol, Reference, SearchResult, DependencyEntry, ImpactEntry, ContextBundle, CouplingReport, RiskAssessment, RiskDiffAssessment, RiskBatchAssessment, TestRecommendations, SessionSnapshot, SessionDiff, and their nested types) derive schemars::JsonSchema. Schema derives respect existing #[serde(rename_all = "lowercase")] attributes automatically. Three explicit wrapper structs (FindSymbolResults, FindReferencesResults, SearchResults, ImpactAnalysisResults) describe the {"results": [...]} envelope that render_with_kind produces for the four tools whose graph functions return bare arrays — agents see the wrapped shape, so the schema must too. A new every_tool_advertises_an_output_schema test asserts every router tool carries a valid object-shaped schema; catches the regression where a tool ships without one and agents have to guess the response shape.

  • Laravel facade rules added to the PHP trust-boundary table. Real Laravel apps abstract auth/storage/queue/env behind facades rather than calling getenv / exec / openssl_* directly, so the original rule set under-counted boundaries on framework code. New rules cover Illuminate\Support\Facades\{Hash, Crypt, Config, Session, Auth, Gate, Storage, File, Process, Artisan, Queue, Bus, Http, Mail, Notification, Broadcast, DB, Schema, Cache, Redis, Request, Input, Route}, Illuminate\{Auth, Encryption, Hashing, Mail, Notifications, Filesystem, Foundation\Http\FormRequest, Console\Command, Database\Eloquent, Http\UploadedFile}, Symfony\Component\{Mailer, Security, HttpFoundation\File}, plus the bare env() / config() helpers. Measured on a 1670-file Laravel app: total file_trust_boundaries rows grew 977 → 1296 (+33%); the previously-zero secrets, process-exec, and auth categories now register real signal (0 → 76, 0 → 13, 0 → 21 respectively). One concrete catch: a 5-boundary file (filesystem + secrets + database + user-input + auth) that scored 0.92 — DocumentStorageService.php, a hotspot + fix-heavy file with 29 dependents in a 142-file import cycle — now flags every facet the security-review note line cares about.

  • feature_bundle MCP tool + matching codesage feature-bundle <id> CLI. Returns the same ContextBundle shape as export_context but anchored on a feature's already-curated file list instead of semantic search results: primary[] carries chunks from owned + entry files, related[] carries tests then context files, symbol_definitions[] includes the entry symbol's definition (preferring matches in the entry file when the entry symbol name is ambiguous — main resolves to the Rust crate's main, not a Python module's __main__). Supports --include-callers / --include-callees to expand the entry symbol's callers/callees into related[]. Use after list_features / find_feature to get all the code an agent needs to review a slice in one MCP call instead of fan-out Read calls per file. Empty bundle with not found marker in target_description when the feature_id doesn't exist; empty primary/related with valid metadata when the feature is mapped but its files haven't been semantically indexed yet.

  • Feature mapping. New behavior-keyed slice surface: each feature bundles an entrypoint + owned files + context files + tests + aggregated trust boundaries + tags. Per-language mappers run deterministically (no LLM) over PHP, C, C++, Rust, Python, JavaScript, TypeScript, and Go. Sources include Cargo workspace members + src/main.rs + src/bin/*.rs + integration tests, Composer bins + PSR-4 autoload roots, php-src ext/<name>/config.{m4,w32} extension slices, Laravel routes/{web,api,console,channels}.php route registrations, autotools bin_PROGRAMS/lib_LTLIBRARIES and CMake add_executable/add_library declarations, tree-sitter main() detection in C/C++ source, pyproject.toml [project.scripts] + setup.py entry_points + Python if __name__ == "__main__": modules, package.json bin/scripts + Next.js app-router and pages-router files, and Go cmd/<name>/main.go + root main.go. Feature ids are stable blake3-derived (feat_<16-hex>) across re-runs as long as the entrypoint and kind don't change. Nearby tests get attached by language convention (sibling *_test.go, *.test.ts, tests/*.rs, tests/test_*.py, tests/*Test.php, etc.). Schema migration 0009_feature_tables adds three new tables (features, feature_files, feature_trust_boundaries); codesage index runs the mapper between structural and semantic indexing on every full or incremental pass. Stale features (whose seed disappeared) are garbage-collected on every map. New CLI: codesage map, codesage features-list [--kind --lang --tag --limit --json], codesage feature-show <id>, codesage feature-for <file>. New MCP tools: list_features and find_feature, both with outputSchema.

  • Trust-boundary signal feeds assess_risk. New crates/features crate derives per-file boundary tags (network, filesystem, process-exec, secrets, database, user-input, external-api, serialization, auth, concurrency) from the file's already-extracted imports / includes / calls / type-hints by matching against per-language rule tables for all 8 supported languages (Rust, PHP, C, C++, Python, JavaScript, TypeScript, Go). Rules cover the major HTTP clients, filesystem APIs, process-exec primitives, crypto/env access, database drivers, CLI/arg parsers, untrusted-deserializer libraries, and concurrency primitives in each ecosystem. Derivation runs inline during codesage index — no extra parser pass, no DB round-trip; in-memory refs feed straight into the new file_trust_boundaries table. RiskAssessment gains a trust_boundaries: Vec<TrustBoundary> field and contributes a 0.10 * min(count/5, 1.0) term to the score (capped at 5 distinct boundaries so legitimately broad infra glue files don't get pinned at the top). A note line fires when ≥3 boundaries are crossed: "crosses N trust boundaries (X, Y, Z) — security review recommended" — paste-ready for PR descriptions. New CLI: codesage trust-boundaries <file> (debugging surface; --json for machine-readable output). Schema migration 0008_file_trust_boundaries adds the table to existing indexes idempotently; the next codesage index after upgrade backfills boundaries file-by-file.

  • Laravel application-layer mapping (laravel-controller, laravel-request, laravel-artisan-command seed sources). Ports the controller / form-request / Artisan-command coverage from clawpatch PR #5. The PHP mapper now walks app/Http/Controllers/**/*.php, app/Http/Requests/**/*.php, and app/Console/Commands/**/*.php and emits one feature per class. Controllers are bridged back to their routes via a per-route table parsed from routes/{web,api,console,channels}.php — the regex captures Route::<verb>('/path', [Controller::class, 'action']) shape and matches by FQCN (preferring namespace X; class Y resolved fully) or by short name when the route used a use-imported class. Artisan commands extract their $signature field so entry_command resolves to the real CLI name (users:sync) rather than the class name. Smoke-tested on a 1670-file Laravel app: feature count 39 → 182 (+86 controllers, +32 form requests, +8 Artisan commands); controller→route bridging hit on 37/86 controllers (the rest use closures or group-prefix patterns this regex doesn't follow yet).

  • React Router route mapping (react-router-route seed source). Ports the <Route path element> detection from clawpatch PR #4. Scans src/**/*.{ts,tsx,js,jsx} and app/**/*.{ts,tsx,js,jsx} for <Route path="..." element={<Component/>}> declarations, emits one feature per route keyed by path. Only runs when the root package.json declares a react dependency to keep the walk cost off non-React repos. Skips test files and framework components (<Outlet>, <Navigate>, <Fragment>, <Suspense>). Fills a real gap: prior to this, CodeSage only mapped Next.js routes; plain React-Router SPAs (Vite + RR, CRA + RR) produced zero route features.

  • PHP extensions distributed at the repo root now map to features (php-ext-root umbrella + php-ext-module per-module). The php-src extension mapper handled the ext/<name>/config.m4 layout but not the Composer/PIE convention where a single extension lives at the repo root (fastchart, php_clickhouse, mdparser, etc.). Detection requires config.m4/config.w32 AND a php_<name>.h header at the root so generic autotools projects (and the PHP interpreter itself) don't false-positive. One umbrella seed anchors at the build config with the public header + main <name>.c|.cpp|.cc as owned files. Per-module seeds fire for each <name>_<part>.{c,cpp,cc} at the root with the sibling <name>_<part>.h attached when present — fastchart's 37 per-chart-type files (fastchart_pie.c, fastchart_bar.c, …) become 37 reviewable slices instead of one opaque blob. Generated <name>_arginfo.h/<name>_arginfo.c are filtered out. Smoke-tested on fastchart (0 → 38 features) and php_clickhouse (0 → 1 umbrella; no per-module split since the C++ lives in a single clickhouse.cpp).

  • Python project-level seed, pytest test-suite seeds, and test-command inference in the Python mapper. Previously the mapper only emitted cli-command seeds for [project.scripts] / setup.py entry_points / if __name__ == "__main__": files — a project with no script entry produced no features at all. Three new surfaces: (1) python-project library seed off any of pyproject.toml / setup.py / setup.cfg / requirements.txt, so find_feature("src/foo.py") resolves to the project itself; (2) pyproject-poetry-script cli-command seeds for [tool.poetry.scripts] (separate from [project.scripts]); (3) python-test-suite seeds — one per top-level suite root (a project with tests/api/, tests/unit/, and tests/integration/ produces ONE tests seed, not three), capped at 200 files, filtered against __fixtures__/fixtures/testdata dirs and generated stubs (*_pb2.py, *.gen.py). The test command is inferred from the lockfile or pyproject [tool.X] section in priority order uv → poetry → pdm → hatch → bare pytest, then plumbed onto each test entry's command field — agents get a runnable string (uv run pytest, poetry run pytest, etc.) instead of guessing. The inferred command also lands on the python-project seed's summary so it's visible in list_features --kind=library. Eleven new regression tests cover the project-seed manifest fallback chain, poetry-scripts section, suite-root bucketing, every supported test driver, fixture/generated filtering, and the no-tests-no-seed case. Ported from clawpatch's python.ts minus source-group partitioning (browse-only) and package-name-derived trust boundaries (would conflict with codesage's per-file derivation).

  • Go package-level feature mapping, replacing the previous shallow cmd/*/main.go + repo-root main.go surface. The mapper now walks the module's .go files filesystem-only (no go list toolchain dependency, keeping the mapper hermetic), buckets files by directory, parses each directory's package <name> declaration, and emits one feature per package: cli-command (go-cmd source) for cmd/<name>/ mains, go-root-package for repo-root mains, go-internal-package for anything under internal/, and go-package library otherwise. Per-package *_test.go files attach to the feature with a scoped go test ./<dir>/... command (not the unscoped go test ./... literal — that conveys no more than the default). Generated files (*.pb.go, *_gen.go, *_generated.go, *.sql.go, *_sqlc.go, plus first-2 KB header scan for Code generated ... DO NOT EDIT) get segregated from owned_files into context_files with reason "generated go file" so agents skip editing them. Same-repo imports (parsed from both single-line and block-form import statements) attach the imported package's owned files as context with reason "imported package <import_path>", capped at 24 refs per seed. vendor/, testdata/, dot/underscore-prefixed dirs, and any descendant of a nested go.mod are skipped. Ten new regression tests cover the cmd/library/internal split, scoped test command, generated-file detection (both suffix and header forms), vendor/testdata skip, nested go.mod skip, same-repo import resolution, and the 24-ref cap. Ported from clawpatch's go.ts minus three anti-patterns: go list (toolchain dep), hardcoded ["user-input","filesystem","process-exec","network"] boundaries on every binary (would override codesage's per-file derivation), and the unscoped test-command literal.

  • Node/TypeScript workspace decomposition in the feature mapper. The JS mapper previously parsed only the root package.json; in a monorepo (Turborepo, Nx, pnpm, yarn workspaces), every workspace package was invisible to find_feature and feature_bundle. The mapper now reads package.json workspaces (array form and {packages: [...]} object form), parses pnpm-workspace.yaml (hand-rolled YAML, no new dep), supports glob expansion (*, **, ?) with ! negation, and falls back to conventional prefixes (packages/, apps/, extensions/, plugins/) when no explicit declaration exists. Each discovered workspace emits a node-package library seed (so find_feature("packages/api/src/auth.ts") resolves to the package manifest + per-package testCommand), plus per-package package-json-bin seeds. The package manager is detected from the lockfile / workspace file (pnpm-lock.yaml → pnpm, yarn.lock → yarn, bun.lockb → bun, else npm) and the test command is formatted accordingly (pnpm --dir packages/api test, yarn --cwd ..., bun --cwd ..., npm --prefix ...). Common scripts (start/build/test/...) stay root-only to keep list_features from filling with N copies of the same script seed. Bin source-back resolution maps dist/foo.jssrc/foo.ts when the source exists, so agents edit the source rather than the build artifact. Eight regression tests cover the array/object workspace forms, pnpm-workspace.yaml, fallback prefixes, glob negation, source-back, and the workspace-vs-root common-script gate. Ported from clawpatch's node.ts workspace branch; source-group partitioning is intentionally not ported (browse-only, dilutes list_features).

  • Directory-saturation signal in the search pipeline. Existing file_saturation (already shipped, default-on) penalizes multiple chunks of the same file in top-10. The the semble-corpus benchmark surfaced the orthogonal failure mode: many chunks from the same directory, different files flooding top-10 — laravel-framework's Queue/Connectors/*Connector.php returning 10 distinct sibling files in the top-10 while pushing the conceptual target QueueManager.php (a different dir) off the page. New apply_directory_saturation runs after apply_file_saturation: per-parent-dir count, threshold 2, decay 0.75^excess per excess chunk. The two signals stack — a chunk from an oversaturated dir whose file is itself oversaturated gets hit twice. Default-on; opt-out via CODESAGE_DIR_SATURATION=0. Three regression tests including a reproduction of the laravel-framework failure mode.

  • FeatureRecord.test_command field. New optional free-form shell command on the public protocol surface (pnpm --dir packages/api test, go test ./pkg/util/..., uv run pytest). Distinct from entry_command, which is documented as the argv[0]-shape token and contributes to the feature-id hash — test commands change as a project's package manager / lockfile / runner evolves, and putting them in entry_command destabilized feature IDs across sessions. Schema migration 0011_features_test_command adds a nullable test_command column to the features table; idempotent on existing indexes. The mapper orchestrator now copies FeatureSeed.test_command straight through. CLI feature-show renders it on its own line. New regression test feature_id_stable_when_test_command_changes asserts the invariant by swapping a project's lockfile (npm → pnpm) and verifying the node-package feature_id is unchanged while test_command flips correctly.

Changed

  • Rebalanced assess_risk weights to make room for the new trust-boundary term: churn 0.35 → 0.32, fix ratio 0.20 → 0.18, dep pressure 0.10 → 0.09, coupled pressure 0.10 → 0.09, test gap 0.15 → 0.13, cycle 0.10 → 0.09, trust boundary +0.10 (new). All seven weights sum to 1.00 so the maximum possible score remains ≤ 1.0. The relative shape of existing signals is preserved; absolute numbers on the same file may shift by up to ±0.12 depending on how many trust boundaries it crosses. The assess_risk_diff summary-notes threshold for "max risk score" dropped from 0.60 to 0.50 to track this deflation (a 0.50 max under new weights signals roughly the same calibrated concern as 0.60 did under prior weights). Existing indexes pick up real trust_boundaries values on the next codesage index pass; until then, the field reports an empty Vec and the term contributes 0, so the score stays defined.

  • README + AGENTS.md now describe the 0.7.0 surface: feature mapping, trust-boundary derivation, list_features / find_feature / feature_bundle MCP tools, and the matching map / features-list / feature-show / feature-for / feature-bundle / trust-boundaries CLI commands. The capability table gains three rows for feature-slice mapping, curated feature bundles, and trust-boundary derivation; new recipes show the codesage mapfeatures-listfeature-forfeature-bundle flow.

  • Tightened MCP tool descriptions on list_dependencies, impact_analysis, and export_context so an agent reading the description-only listing can disambiguate adjacent tools without falling back to trial-and-error: list_dependencies is now explicit about single-hop scope and points at impact_analysis (multi-hop) and find_references (per-symbol) as the right neighbors; impact_analysis mirrors the bridge in reverse; export_context notes that feature_bundle is the right call when the anchor is an already-mapped feature, not a free-form query. Pattern adopted from Serena's two-commit description audit this sweep (5b9600bc "Make tool descriptions more amenable to tool search mechanisms" + 1767a259 ReplaceSymbolBodyTool docstring fix).

  • impact_analysis description tightened again to name reverse direction explicitly (callers/importers of the target, transitively), call out that the result is empty for leaf files nothing imports, and clarify that same-file symbols and the target's own forward dependencies are NOT included (point at list_dependencies for the latter). Surfaced by the head-to-head benchmark against code-review-graph: their tool is bidirectional and 4× wider in recall, and our existing description didn't say we made the opposite product choice. An agent reading the old description would reasonably assume bidirectional and be confused by leaf-file empties.

  • Adaptive rerank-weight by query type. RERANK_WEIGHT was a fixed 0.5 blend between semantic and cross-encoder scores. semble's adaptive-α signal (varies the BM25/dense blend per query type) ports to codesage's pipeline as varying the rerank/semantic blend. Short-identifier queries (single token, identifier-shaped: FooBar, parse_config, Middleware) now use weight 0.35 (trust semantic + symbol-boost more, cross-encoder less — bare identifiers are already promoted well by symbol-boost and definition-boost). Natural-language queries (≥3 alphabetic words: "queue connection resolution and connectors") use 0.6 (trust the cross-encoder more — semantic-only retrieval over-clusters on sibling files; cross-encoder untangles them). Mixed/fallback queries keep 0.5. Default-on; opt-out via CODESAGE_ADAPTIVE_RERANK=0. Motivated by the the semble-corpus benchmark finding that laravel-framework's 5-word natural-language queries were over-trusting semantic similarity and over-clustering on sibling files (e.g. Queue/Connectors/*Connector.php flooding top-10 instead of surfacing QueueManager.php).

  • Query-aware test-file demote. Previously test-like paths got a fixed 0.3x path_penalty regardless of query intent. Two problems with that: production-intent queries like "request and response interceptors" still got 9 test files flooding top-10 because 0.3 * strong_similarity beat 1.0 * weaker_similarity for the actual target; and legitimate "find the test for X" queries surfaced the production file above its own test. New behavior: queries containing test-intent tokens (test, tests, testing, spec, specs, fixture, fixtures, phpt) lift the test-like demote entirely; non-test queries get a harder 0.15x demote (= 0.3 × 0.5). Default-on; opt-out via CODESAGE_TEST_QUERY_AWARE=0. Measured on the semble corpus: axios NDCG@10 0.741 → 0.791 (recall 0.900 → 0.950), flask 0.823 → 0.906 (recall 0.905 → 1.000), laravel-framework 0.723 → 0.773 (recall 0.900 → 0.950). Net +0.17 NDCG summed across 7 spot-checked repos; tiny -0.012 swing on vitest (recall unchanged).

  • Directory-saturation defaults retuned to threshold=2, decay=0.75 (was threshold=3, decay=0.85). A/B harness across laravel-framework + redux + flask: laravel NDCG@10 0.773 → 0.779, flask 0.906 → 0.909, redux unchanged at 0.957, no recall regressions. The original 0.85 decay was a guess; the follow-up corpus measurement (laravel still surfaced QueueManager.php only at rank 5) suggested room for a steeper decay. Both parameters are now env-overridable for per-project tuning: CODESAGE_DIR_SATURATION_THRESHOLD (positive integer, default 2) and CODESAGE_DIR_SATURATION_DECAY (float in (0.0, 1.0], default 0.75; 1.0 effectively disables the signal).

  • README headline updated to the semble-corpus number (recall@10 = 0.932, NDCG@10 = 0.788 on 602 queries across 8 supported languages; 0.448 / 0.379 on the full 1,251-query corpus with the parser-coverage gap accounted for honestly). The pre-existing git-mined-corpus benchmark stays as-is.

Fixed

  • Feature mapper bypassed .gitignore. crates/features/src/mappers/shared.rs::walk_files was a hand-rolled walker that ignored gitignore entirely and only matched a small hardcoded directory-name list. codesage map against this repo was indexing .worktrees/<branch>/... as separate features even though git check-ignore reports the directory as ignored. Rewrote to delegate to ignore::WalkBuilder (same gitignore-aware walker the structural indexer uses) plus added .worktrees / worktrees to the belt-and-suspenders hard-exclude list. Two regression tests cover the .gitignore-honored walk and the explicit should_skip predicate.

  • feature_bundle returned imports instead of the entrypoint body. The bundle picked the file's first chunk for entry files; on a Rust binary with 400+ lines of use statements before fn main(), an agent reviewing the feature got the import preamble, not the function body. feature_bundle now looks up the entry symbol's line_start in the symbols table and selects the chunk that covers that line, falling back to first-chunk when the symbol can't be located. Regression test seeds an entry file where main sits at line 101 with imports at 1-50.

  • Trust-boundary upgrade-path gap. The 0.7.0 indexer derives boundaries inline only for parsed files, so an incremental run after upgrade left existing entries empty. The first pass used a global boundary_count == 0 row-count check; review caught that a partially populated state (some files got inline derivation, others didn't) would never trigger backfill because the row count is nonzero. Switched to a per-file marker: schema migration 0010_files_boundaries_derived_at adds a boundaries_derived_at column to files, replace_file_trust_boundaries stamps it on every write (including when the derived set is empty, so a rule-clean file is distinguishable from a never-derived one), and cmd_index queries files_pending_boundary_derivation() to target the actual stragglers. Verified end-to-end on a partial-state simulation: clearing one file's boundaries + its marker, while keeping 156 boundary rows on other files, ran a targeted backfill that restored exactly the missing file. Re-run on a fully-marked DB is a clean no-op.

  • Ghost features from files the structural indexer never sees. The feature mapper crate didn't read the project's [index].exclude_patterns, so mappers walked the filesystem with only .gitignore + a small hardcoded skip list. On a Python repo with test runners, that produced 36/85 features pointing at test_*.py paths absent from the files table. First pass shipped a Python FileCategory::Test skip; the broader fix now plumbs [index].exclude_patterns through a new MapperContext carried into every FeatureMapper::map call. The orchestrator compiles the patterns into a GlobSet once and threads it through the seed-collection walk plus a per-mapper output filter, so every mapper honors the same file-filter contract as the structural indexer. New regression test (map_features_respects_index_exclude_patterns) covers the contract end-to-end. Verified on the same Python repo: 49 features, zero ghost paths.

  • Mapper errors could delete valid features. The mapper orchestrator warned-and-continued on per-mapper errors, then garbage-collected anything not in the partial seed set. A transient PHP-mapper failure on corrupted composer.json could wipe every previously-mapped PHP feature. Now: any mapper error sets an any_mapper_errored flag, the orchestrator skips the stale-feature GC pass (trading small amounts of stale debt for never nuking valid rows), and codesage index propagates the error instead of silently succeeding with eprintln!.

  • C/C++ mapper double-emitted features for build targets that also define main(). A binary declared in CMakeLists.txt (or Makefile.am) AND containing int main() produced two features — one cmake-bin/autotools-bin, one c-main — because the already_seeded_paths set was constructed empty and the dedup key included source. Populate the seen-set from build-target seeds before main()-detection runs, and dedup on (entry_path, kind) only so build-target seeds win over generic main()-detection seeds. Two regression tests cover the CMake and autotools paths.

  • php-src extension mapper used wrong entry on Windows-only extensions. The detector accepted either config.m4 or config.w32 but always recorded entry_path = .../config.m4. Windows-only extensions ended up with a feature whose entry file didn't exist. Pick whichever config actually exists (prefer config.m4 when both are present to keep IDs stable for the dominant POSIX case). Regression test covers the config.w32-only case.

  • Ghost features from build-system targets and per-bin Rust crates that the structural indexer never sees. The plumbed [index].exclude_patterns plumbing applied at the orchestrator level but the per-mapper seeds.retain filter was missing in crates/features/src/mappers/rust.rs and crates/features/src/mappers/c.rs: src/bin/<name>.rs matching an exclude still produced a cargo-bin feature, and a CMake add_executable(... excluded.c) still emitted a feature whose entry_path was filtered out of the structural index. Added a MapperContext::allowed(rel) helper applied to every seed emission, every owned-file source from CMake / autotools, every Rust binary / library / integration-test candidate, and every Python script entry. Orchestrator now also retains per-file refs (entry/owned/context/test) by ctx.allowed as a safety net so no FeatureFileRef references a path the structural indexer ignored. Four new regression tests (rust_bin_under_exclude_is_dropped, rust_integration_test_under_exclude_is_dropped, cmake_target_under_exclude_is_dropped, cmake_owned_files_under_exclude_are_dropped).

  • Python console-script entry_path pointed at the manifest, not the resolved module. [project.scripts] acme = "acme.cli:main" produced a feature whose only file was pyproject.toml; codesage feature-for acme/cli.py returned nothing. Added resolve_script_module_path which probes <module>.py, <module>/__init__.py, src/<module>.py, and src/<module>/__init__.py for the dotted module target. When one of those exists (and isn't excluded), it becomes entry_path; the manifest falls back as entry_path only when no candidate resolves (preserves prior behavior on unparseable layouts). Four new regression tests cover the flat layout, src/ layout, package __init__.py, and the exclude-fallback path. Same fix applies to setup.py entry_points.

  • Workspace and project routing was a no-op. node-package and python-project seeds only persisted the manifest in feature_files, while storage::features_for_file does an exact feature_files.path = ?1 lookup. So feature-for packages/api/src/auth.ts and feature-for src/acme/foo.py returned empty — the routing advertised when the mapper landed didn't actually work. Fixed by walking the workspace/project source roots (packages/<name>/, src/, lib/, or the repo root when no source layout exists), filtering to .py / .ts / .tsx / .js / .jsx / .mts / .cts / .mjs / .cjs, excluding tests / specs / .d.ts / fixture dirs / generated stubs, and attaching as owned_files with reason "workspace source" or "project source". Capped at 2,000 files per seed to keep the feature_files table bounded on monolithic repos. Two regression tests cover the routing contract directly.

  • Go's per-package file classifier ignored project excludes. collect_package_files used raw fs::read_dir and skipped the ctx.excludes check that discover_packages had applied. If an excluded file sorted alphabetically before an included sibling, it became entry_path; the post-map seeds.retain(|s| !ctx.excluded(&s.entry_path)) then dropped the entire package, losing every other file in the dir. Threaded MapperContext into collect_package_files (and collect_import_context) and filter each candidate via ctx.allowed.

  • JS fallback workspace prefixes were always added. packages/, apps/, extensions/, plugins/ got picked up even when the root package.json declared explicit workspaces. A repo with workspaces: ["apps/*"] plus an incidental packages/stray/package.json produced features for both. Now the fallback only enumerates when no explicit declaration produced any include patterns.

  • Go's generated-file header sniff read the whole file. is_generated_go_file called fs::read_to_string then truncated to 2 KB — a multi-MB go-bindata output without a recognized suffix allocated the full file just to inspect 2 KB. Replaced with File::open + take(2_000) + UTF-8 valid_up_to truncation. Earlier draft used String::truncate(2_000) which panics on a UTF-8 char boundary (BOM or multibyte comment near the cap); that's fixed by the same valid_up_to walk-back.

  • Python pytest cap didn't actually cap. pytest_test_suites documented a 200-file cap but the inner break only exited the per-scan-dir loop — a project with 200 tests in tests/ plus more under src/ exceeded the cap. Added a labeled break 'outer so the cap is global.

  • Manifest files were marked as test files. python-project and node-package workspace seeds populated tests: vec![SeedTest { path: manifest, command: ... }] to surface the inferred test command. The orchestrator inserts every seed.tests[] row as role: Test in the feature-file table, so pyproject.toml / package.json showed up as test files in feature_bundle output. The test command now surfaces via entry_command only; tests[] is empty for nearby-test discovery to populate.

  • JS workspace discovery ignored .gitignore. The packages/* enumeration step used raw fs::read_dir. A gitignored packages/internal-only/ would emit a node-package seed pointing at files the structural indexer had skipped. Switched to ignore::WalkBuilder with max_depth(1).

  • Python test-driver detection had false positives. A [tool.uv] / [tool.poetry] / [tool.pdm] section was treated as evidence the project used that runner, but projects routinely declare uv/poetry/pdm metadata for dev-deps while running plain pytest. The section probe is gone; lockfile (uv.lock, poetry.lock, pdm.lock) is the only signal. Hatch falls back to bare pytest.

  • Go same-repo import context cap is now env-overridable. IMPORT_CONTEXT_CAP was hardcoded 24. Now configurable via CODESAGE_GO_IMPORT_CONTEXT_CAP (positive integer, default 24), matching the precedent set for the directory-saturation tuning.

  • JS bin source-back also handles lib/. dist/foo.jssrc/foo.ts already worked; older Babel / TS-library packages publish via lib/foo.js. The same source-back resolution now applies there too.

  • Internal cleanups in the mapper port. Factored pyproject_scripts + pyproject_poetry_scripts into a shared pyproject_scripts_section helper (both share name = "module:fn" syntax; only header / source-tag / title differ). Dropped an unused roots_seen BTreeSet in python.rs. Moved package_display_name's unreachable root-fallback into debug_assert!.

  • Intermittent SIGABRT at process exit ("corrupted double-linked list" via glibc heap-corruption diagnostic). Surfaced in the the semble-corpus benchmark at ~1.1% rate (2 SIGABRT in 177 codesage search runs). The query results were always correct — the abort happened during Drop-glue at exit, after stdout had been flushed. Root cause is the interaction of ORT Session destructors with sqlite-vec's extension destructors at process teardown — a known class of double-free race in destructor chains that share allocator state across FFI boundaries. Mitigated by refactoring main() to explicitly std::process::exit(code) after stdio flush, skipping Drop glue. Safe for codesage's CLI commands: all writes commit explicitly via execute_batch (SQLite WAL is on disk before main returns), all stdio is flushed, the OS reclaims process memory on exit. The MCP server path loops indefinitely and never reaches the exit; signal-terminated MCP processes get the same skip via OS-level cleanup. Documented inline in crates/cli/src/main.rs::main so the next dev sees the rationale.

  • codesage-onboard plugin script gains --refresh-hint and writes a 0.7.0-aware .claude/CLAUDE.md template. The hint now teaches list_features / find_feature / feature_bundle / trust-boundaries by the question shape they answer alongside the prior tools, mentions feature mapping + trust boundaries in the operations section, and lists the new map / features-list / feature-show / feature-for / feature-bundle / trust-boundaries CLI commands. Without --refresh-hint, existing hint files are preserved (the prior behavior). A version sentinel comment is embedded so future upgrade sweeps can detect a stale hint without parsing the body.

  • New scripts/refresh-onboarded-repos.sh automates the post-upgrade flow across every onboarded repo on the machine: re-runs codesage index (applies migrations 0008/0009/0010, populates feature tables, backfills file_trust_boundaries), re-installs git hooks (idempotent; protects against current_exe path moves), and re-runs codesage-onboard --refresh-hint to update the per-repo .claude/CLAUDE.md. Skips ~/ai/bench-repos/* and ~/ai/codesage_ref/* per the bench-repos study-only rule. --dry-run to preview, --no-index to refresh hints only.

0.6.0 - 2026-05-09

Changed

  • Raised MAX_SEQ_LENGTH from 256 to 512 tokens and DEFAULT_CHUNK_SIZE from 1000 to 1500 chars (DEFAULT_MIN_CHUNK_SIZE 250 → 350, DEFAULT_CHUNK_OVERLAP 150 → 200). The previous pair caused silent embed-time truncation on dense chunks: char-1000 chunks averaged 250–330 tokens for code, so the long tail of the distribution had its right edge dropped before pooling. Raising both together closes the truncation gap and lets each chunk carry more cross-cutting context. Measured on 30 ground-truth cases across webpack + prometheus + home-assistant-core: aggregate miss rate 6.7% → 3.3%, mean recall@10 0.89 → 0.93, recall@5 0.86 → 0.89; webpack alone went miss% 20% → 0% and r@10 0.60 → 0.83. Existing indexes continue to work but get the truncation benefit only after a full re-index (codesage index --full); incremental hooks pick it up file-by-file as edits land. Indexing wall-clock rises moderately (per-file token count drops, per-token compute rises with longer sequences). Full A/B at bench/history/cap512-1500-2026-05-04.md. Validates the "raise the cap, not shrink the chunk" thesis from the prior null-result attempt at this same fix (token-200 chunking, reverted).
  • discover_files_with_excludes now uses WalkParallel with worker-local SHA-256 hashing and an mpsc drain. The previous serial walk hashed every file in series and was the dominant indexer bottleneck on monorepos. Measured on a 2867-file Laravel checkout (no semantic): wall time ≈ 1.5 s with ~6.5× core utilization (9.8 s user / 1.5 s wall), versus the prior serial walk's near-1× ratio. Behavior is preserved: errors still bubble (first error wins via a shared slot, walk quits early), .h files still get C/C++ project-aware classification, and the result is sorted by path so callers that depend on stable ordering keep working.

Added

  • Symbol.rationale[] field on find_symbol and export_context MCP responses, populated for Rust definitions whose attached doc-comments start with WHY:, NOTE:, IMPORTANT:, FIXME:, HACK:, XXX:, or TODO:. Each entry carries the marker kind, the comment body with the marker stripped, and the comment's source-line span. The field is omitted from JSON when empty, so existing consumers see no diff for symbols without rationale comments. Marker-based filtering keeps the table small and high-signal: plain "describes what this function does" doc-comments are not stored — agents recover those from the symbol name and signature anyway. Two limitations the field's doc-comment also calls out: rationale rots when code changes (we extract whatever the source says at index time, can't verify it's still accurate); and rationale is a property of definitions only, not callsites or references. Currently extracted for Rust; other languages return empty rationale[] until per-language extractors land. Schema migration 0007_symbols_rationale adds a rationale column to the symbols table; existing indexes pick up real values on the next file-touch + reindex (no global re-index required).
  • [embedding].pooling config field ("mean" or "cls") overrides the model-name heuristic that previously decided pooling strategy from a substring match on bge-. The heuristic is silent and wrong for any non-bge- CLS model (intfloat/e5-*, etc.) and any bge- mean-pooling variant — both produce semantically wrong vectors with no error message. Existing configs keep working: when the field is omitted, the heuristic still applies. Set explicitly when picking a model the heuristic doesn't know about.

Changed

  • Tightened protocol types: SearchResult.language is now a Language enum (was a free-form String) and SymbolSummary.kind is a SymbolKind enum (was String). JSON serialization is unchanged (both serialize as the same lowercase strings), but Rust consumers that build SearchResult / SymbolSummary literals or match on the fields directly will need to swap string literals for the enum values. Closes the silent-typo class of bug where a hand-written "functon" would compile.
  • Added PartialEq / Eq derives to Symbol, Reference, and FileInfo, and Hash to FileCategory. Lets consumers dedup, assert against, and key hash maps on these types — previously blocked by missing trait derives.
  • codesage git-index now passes --since=@<2-years-ago> to git log, capping the history walk to the window where decay weight stays meaningfully above zero. With τ=180d half-life, commits older than 2 years contribute ≤ 0.017 per commit (≤ 0.0025 by 3 years, ~zero by 5 years), so a longer walk just inflates first-onboard time without moving churn or coupling scores. Trims walk by ~7× on long-history projects (php-src: 146k → ~10k commits, ~90s → ~10s on a fresh --full). Slow-coupling pairs that co-change once every 8-12 months still accumulate count >= 3 within the window. Constant HISTORY_WINDOW_DAYS = 730 in crates/graph/src/git_history/indexer.rs.
  • codesage install-hooks now wraps the background indexer invocations in nice -n 19 and (Linux-only, gated on command -v ionice) ionice -c 3 so a hook-triggered indexer can't soak foreground CPU or block disk I/O. Existing installs need to re-run codesage install-hooks to pick up the niced template; new installs get it automatically.

Fixed

  • Symbol and reference column numbers (col_start, col_end, col) are now UTF-8 character columns instead of tree-sitter's default byte columns. Tree-sitter's Point.column is bytes from the start of the line, so a single CJK ideograph or emoji ahead of a node would shift the byte column past where any editor renders the cursor; goto-definition and editor-jump landed in the wrong place on multi-byte source. The 0.26 Rust binding doesn't expose ts_node_*_point_utf8, so the conversion (walk back to line start, count UTF-8 codepoints in the prefix) is implemented in crates/parser/src/position.rs and used by both extract.rs (symbols) and references.rs (references). ASCII columns are unchanged. Covered by unit tests for ASCII, CJK, emoji, line-reset, and invalid-UTF-8 fallback cases.
  • Bounded the Python site-packages probe used for ONNX Runtime / NVIDIA library discovery with a 10 s timeout. Without it, a wedged Python interpreter inside OnceLock::get_or_init would block every thread waiting on Embedder::new / Reranker::new indefinitely. On timeout the child is killed and the probe falls back to the next interpreter or to system paths.
  • Moved ONNX Runtime + NVIDIA library discovery into fn main() (codesage_embed::model::init_for_main) before any thread or tokio runtime spawns. The two underlying std::env::set_var calls (LD_LIBRARY_PATH, ORT_DYLIB_PATH) are unsafe under Rust 2024 because concurrent getenv from another thread is UB; pinning them to single-threaded startup eliminates the race. The original Once::call_once-guarded calls inside Embedder::new / Reranker::new remain as a defensive fallback for direct library users.
  • Hardened the indexer + MCP output paths against three latent OOM / silent-corruption failure modes. (1) discover_files_with_excludes now skips files larger than 10MB (MAX_INDEXABLE_FILE_BYTES) at metadata time, before fs::read, so a stray large generated SQL dump or vendored data file slipping past HARD_EXCLUDE_PATTERNS no longer fills the indexer's heap. Skipped files emit a tracing::warn. (2) Embedder::detect_dim now bails with a clear error when a model's output shape has no static last-dimension, instead of silently falling back to the 384-dim default — a model with a different real dimension would otherwise store wrong-dimension vectors and turn search_knn into noise. (3) truncate_array in the MCP layer now shrinks an oversized first item's content field instead of returning it unchanged: a single 50KB chunk no longer blows past the 32KB token budget.
  • Tightened SQLite transaction boundaries on three semantic-write paths so a SQLITE_BUSY or I/O failure on COMMIT can no longer poison the long-lived MCP DB connection. (1) Database::execute_batch now issues an explicit ROLLBACK when COMMIT itself fails, instead of leaving the connection inside an open tx where every subsequent statement errors with "cannot start a transaction within a transaction". (2) repair_fts_sidecar now wraps its DELETE + INSERT…SELECT in BEGIN / COMMIT inside the same execute_batch call, so a crash between the two statements no longer leaves the FTS sidecar empty while the chunk table has data. (3) insert_chunks now wraps each call in a SAVEPOINT so the vec0 + FTS5 inserts stay atomic even when the function is called outside an outer execute_batch. None of these were observed live, but the failure modes are real (per-connection corruption, BM25 returning zero results until next process start, orphaned vec0 rows).
  • Fixed codesage index losing all in-flight progress when killed mid-pass. The semantic indexer previously wrapped the entire write pass in one SQLite transaction, so a SIGKILL / OOM / terminal close between embedding and final commit rolled back every file's hash, even files whose embeddings had already been computed. select_semantic_files then reselected those files on the next run, so the same backlog was re-embedded indefinitely. Observed on ~/php-src with Jina v2 base: a 572-file hole persisted across many post-merge hook fires (~10 min/run) until one uninterrupted run was allowed to finish. Writes are now committed in batches of 50 files; an aborted run preserves all files committed up to the last batch boundary.
  • Fixed codesage status and codesage doctor to report semantic index freshness for the configured model, including stale or missing semantic-file hashes that structural HEAD drift alone cannot detect.
  • Fixed export_context / codesage export callee handling so include_callees returns referenced callee/dependency code instead of being ignored for symbol targets or duplicating the definition chunk for query targets.
  • Fixed codesage export --symbol --limit to cap returned symbol definitions and primary chunks, preventing common names from exceeding the requested context budget.
  • Fixed scripts/leak-check.sh invalid-regex diagnostics so the underlying grep error is shown instead of an empty detail block.
  • Fixed impact_analysis target handling and precision: dotted symbols such as Repository.find now stay symbol targets by default, codesage impact --symbol can force symbol interpretation from the CLI, and qualified-symbol impact/export caller lookup now uses exact qualified references instead of broad same-tail matches.
  • Fixed PHP structural references so instance ($object->method()), nullsafe ($object?->method()), and static method names are indexed as call references.
  • Fixed scripts/leak-check.sh --range A..B to scan file content at the range endpoint instead of the current HEAD.
  • Fixed scripts/leak-check.sh to fail closed on invalid forbidden-pattern regexes, and added CI coverage for shell linting plus maintenance-script regressions.
  • Fixed scripts/release.sh to refresh the [Unreleased] changelog compare link when cutting a release.
  • Fixed structural lookup and context export edge cases: Rust/C++ :: qualified symbols now resolve through find_symbol, dotted call references such as Go fmt.Println can be found by their bare tail, Python attribute calls are indexed as call references, and export_context binds symbol summaries back to exact qualified definitions instead of the first matching short name.
  • Fixed git-history indexing to honor project exclude_patterns from .codesage/config.toml and to decay existing churn/co-change weights even when an incremental run finds HEAD unchanged.
  • Fixed codesage init TOML generation for project directory names containing quotes, backslashes, or control characters.

0.5.0 - 2026-05-02

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.

0.4.5 - 2026-04-25

Added

  • C++ as a supported language. New Language::Cpp variant, tree-sitter-cpp dependency, cpp.scm symbol query, and cpp_refs.scm reference query. Symbol coverage: free functions, in-class methods (declared and defined), out-of-line method definitions (void Foo::bar() {} resolved to Foo::bar), constructors / destructors, operator overloads (including reference-return forms like Foo& operator=), classes / structs / unions, plain enums + enum class, typedefs, using aliases, C++20 concepts, and #define macros. Template-wrapped definitions are captured automatically (the inner definition node still matches its pattern regardless of the template_declaration wrapper). Reference coverage: #include, bare / qualified / member / template-function calls, new T / new ns::T / new T<U> instantiations, base-class clauses (bare, qualified, and template-typed), and using declarations. Qualified names walk enclosing namespaces (including the C++17 namespace A::B form). Honest caveat: tree-sitter does not expand macros, so libraries that hide class/namespace behind project-specific macros (e.g. nlohmann/json's NLOHMANN_BASIC_JSON_TPL_DECLARATION and NLOHMANN_JSON_NAMESPACE_BEGIN) lose those symbols. The macro turns the surrounding declaration into an ERROR node and the structural query finds nothing. File source recognition: .cpp, .cc, .cxx, .c++, .cppm, .ixx, .hpp, .hh, .hxx, .h++, .tpp, .ipp are always C++. .h defaults to C; the discovery layer auto-flips it to C++ for any project that also contains an unambiguous C++ extension (no config knob). .c always stays C, even in a C++ project.
  • assess_risk_diff now reports import cycles that involve patch files. New cycles_touching_patch[] field on RiskDiffAssessment lists each strongly-connected component of size ≥2 in the file-level import graph where at least one member is in the patch's file list. Each entry carries members (sorted repo-relative paths), size, and max_churn_file (highest-churn member, used as a "most likely refactor site" heuristic; often the right place to break the cycle). When non-empty, a summary note like "2 import cycle(s) involve patch files (largest: 3 files)" lands in summary_notes for PR-description inclusion. Edges are derived from the existing refs table (kind = import / include / inheritance / trait_use resolving through symbols.name or symbols.qualified_name), so no new data collection. Uses an iterative Tarjan's SCC so deep include chains (php-src-scale) don't blow the stack. Honest caveat documented in the MCP tool description: we detect cycles the patch touches, not cycles the patch introduced. We don't have a pre-patch index to diff against, so pre-existing cycles involving patched files get reported too. Recommendations doc §1.1 (source: SocratiCode codebase_graph_circular, GitNexus cycle detection). 4 new tests cover two-file cycles, three-file diamonds, patches outside the cycle (not reported), and linear acyclic graphs.
  • Advisory file-lock coordination for concurrent codesage writers. index, git-index, and cleanup now take an exclusive flock on .codesage/indexing.lock at startup (stdlib File::try_lock, no new dep). If another writer already holds it, the second process exits 0 with another codesage indexer is running on <path> — skipping instead of colliding at the SQLite layer and emitting a scary Error: database is locked. Matters most for install-hooks-registered background hooks (post-commit, post-merge, post-checkout, post-rewrite), where rapid commits or a git pull during a manual index used to spam hook logs with SQLite contention errors. The concurrency-audit finding from recommendations doc §2.4 confirmed no data was at risk, but the UX looked like a bug; this eliminates the class of error. Re-run bench/concurrency-audit.py on any onboarded project to verify.
  • Gated hybrid BM25+semantic retrieval (recommendations doc §2.1 accepted). New FTS5 sidecar tables {chunk_table}_fts are created alongside each vec0 chunk table and populated in lockstep during insert_chunks. On search, a gate function (query_has_rare_literal) decides whether the query contains a distinctive literal: backticked identifiers, :: scope resolution, *.ext globs, dotted-identifier pairs like moduleref.create, or long (≥8 char) code-shaped tokens with <1% corpus DF. When the gate fires, BM25 results are fetched from the FTS5 sidecar and fused with the semantic ranking via weighted Reciprocal Rank Fusion (BM25 weighted 2x). The cross-encoder reranker is skipped in the gated path because it consistently demotes literal-match wins (the exact failure mode the original project_hybrid_bm25_rrf.md memo warned about). Rare-literal-shaped queries that miss from plain semantic retrieval (the memo's predicted failure mode on mechanism-heavy external corpora) are the specific target; everything else keeps the semantic-only path unchanged. Measured on canary corpora: ripgrep miss@10 20% → 13% (−7pp), nest miss@10 10% → 3% (−7pp), zero regressions.
  • find_coupling (MCP and CLI) now returns CouplingReport { coupled, file_indexed, file_commits, note? } instead of a bare Vec<CoChangeEntry>. Agents should read response.coupled for the ranked list. Motivation: retrospective session analysis showed 59% of find_coupling calls returned [] with no context, leaving the agent unable to tell apart the three causes: file never indexed, file has history but no co-change pair crosses the min-count=3 threshold, or path shape doesn't match the index. The new note field disambiguates each case with a concrete hint (run codesage git-index, verify path shape, or accept that this file changes in isolation). file_indexed and file_commits are always present, so even non-empty results carry enough context for the agent to judge a thin response.
  • assess_risk_diff now clusters per-file detail when a patch touches ≥5 files from one directory. The crowded directory's entries move from files[] into a new top-level clustered_directories[] field (top-3 files by score preserved in full detail, the rest listed by name as omitted_files). Rollup arrays (test_gap_files, wide_blast_files, fix_heavy_files, hotspot_files) still list every clustered file, so cross-referencing a cluster back to a specific concern still works. Small patches (≤4 files per directory) keep the original flat shape, so agent prompts written against the prior schema keep working without changes. Addresses recommendations doc §1.5 after retrospective session-log analysis measured this as the real cost center (p95 24 KB responses on this tool alone).
  • Stronger MCP tool descriptions for find_symbol, find_references, and search that explicitly say "prefer over Grep for code-symbol lookups" and call out the specific disambiguation failure modes (grep mixing definitions, comments, and string literals). Session-log analysis showed agents reach for Grep on patterns codesage answers in one call; sharper descriptions are the first intervention.
  • Drift instrumentation for the structural/semantic index. New structural_index_state table (migration 0002) records the HEAD SHA each successful codesage index run built against. codesage status and codesage doctor now show an index-drift indicator (fresh, N commits behind HEAD, not an ancestor of HEAD for rebases, or never indexed). The MCP server appends one JSON line to <project>/.codesage/drift.log the first time it resolves a project in a session, bounded to the last 10,000 lines. No auto-reindex behavior: this is pure measurement so we can characterize how often git-hook drift happens in real usage before deciding whether to build the content-hash backstop (recommendations doc §1.3).

Changed

  • bench/codesage-bench-runner scorecard output now includes a full metadata header: project HEAD SHA, repo size (file + LoC counts), corpus + case count + top-N, codesage version, embedding model + device, reranker, explicit baseline-for-comparison slot, and ISO run timestamp. Adds a "measured quantities" / "NOT measured" block so the honesty gap (no agent tool-call counts, no wall-clock-vs-grep) is visible in the output itself. New trailing "Quotable one-liner" copies all metadata into a single sentence suitable for external communication; the machine-parseable METRICS HTML comment gains the new fields for downstream tooling. Optional --baseline "ripgrep 14.1.0 (grep -rn)" flag (or CODESAGE_BENCH_BASELINE env) fills the baseline slot. Informational only, does not run a comparison tool. Template shape follows landscape-sweep item 1.6 in notes/20260424-reference-tool-recommendations.md.

Fixed

  • MCP tool params now accept integer fields encoded as JSON strings ("limit": "5" alongside "limit": 5). Strict Option<usize> deserialization was failing with invalid type: string "5", expected usize on ~10% of find_coupling calls; retrospective session-log analysis (bench/analyze-codesage-quality.py) found agents occasionally emit stringy numbers, which is standard LLM JSON behavior and not something the protocol should reject. Applies uniformly to limit, offset, and depth across CouplingParams, ImpactParams, ExportContextParams, and SearchParams. Genuinely non-numeric strings still error, and the error now quotes the offending value for diagnosability instead of a generic type-mismatch message.
  • codesage status no longer errors with no such table: on projects opened without a selected embedding model. Root cause: cmd_status opened the DB via Database::open() (which leaves chunk_table empty) and then called chunk_count(), which interpolated the empty table name into its SQL. Status now calls a new total_chunk_count() that sums across every vec0 chunk table in the DB, a more useful number anyway, since DBs that have been benchmarked across multiple models carry orphan chunk tables cleanup hasn't dropped. Zero vec tables returns 0 instead of failing.

Security

  • Bumped transitive openssl crate to 0.10.78 (from 0.10.77) to pick up fixes for CVE-2026-41676, CVE-2026-41677, CVE-2026-41678, CVE-2026-41681, and GHSA-hppc-g8h3-xhp3. Four of the five are high-severity memory-safety bugs in rust-openssl callbacks and AES key wrap. CodeSage pulls openssl transitively via native-tls (used by hf-hub for model downloads and ort-sys at build time), so the exposure is limited to TLS paths exercised during model fetches, but the bump is cheap and closes the Dependabot alerts.
  • Bumped transitive rustls-webpki to 0.103.13 (from 0.103.12) for RUSTSEC-2026-0104 (reachable panic in CRL parsing). Pulled via rustlsureq / hyper-rustls / tokio-rustls on the same model-download and reqwest paths. Clears the scheduled cargo audit workflow failure.

0.4.0 - 2026-04-16

Added

  • Go language support. Parses functions, methods (with pointer and value receivers), structs, interfaces, type aliases, and constants. Qualified names use ReceiverType.MethodName convention. References track imports and function/method calls. Test discovery already recognized _test.go convention from v0.3.0; now the parser can index Go source files.

Fixed

  • MCP server no longer dies on mutex poisoning. The per-server caches and per-instance Embedder/Reranker locks use parking_lot::Mutex, which has no poison state, so a panicked handler can no longer take the whole stdio transport with it.
  • MCP search and export_context no longer silently degrade when the configured reranker fails to load. Reranker init errors now propagate to the agent as structured errors with model/device context instead of being swallowed.
  • Reranker::new now bails when GPU is requested but the binary was built without the cuda feature, matching Embedder::new. Previously fell through to CPU silently.
  • MCP error responses now include the full anyhow cause chain ({:#}) so root causes survive across tool boundaries.
  • DB row decoders no longer silently drop rows on schema drift or type mismatch. All 15 filter_map(|r| r.ok()) / flatten() sites in storage::Database now propagate rusqlite::Error; unknown SymbolKind / ReferenceKind values surface as typed conversion errors instead of being relabelled as Function / Call.
  • Malformed .codesage/config.toml is now a hard error instead of a silent revert to defaults (MiniLM/CPU/empty excludes). load_project_config and MCP load_embedding_config return Result and surface parse errors with path context.
  • parse_log drops commits with unparseable timestamps and changes with unparseable add/delete counts instead of fabricating zeros that were indistinguishable from legitimate ts=0 or zero-line changes. Reports a one-line summary when any lines were dropped.
  • is_ancestor now distinguishes spawn failure (propagated) from exit-1 / exit-128 (fall back to full rescan with a warning). A broken git install is no longer silently hidden as "rebased history".
  • install-hooks now propagates current_exe() failures instead of silently writing a hook that embeds the literal codesage path.
  • assess_risk no longer swallows errors from internal impact_analysis and test_sibling_exists. A DB failure during risk computation surfaces with context instead of silently lowering the score.
  • Graph query pipeline (extract_known_symbols, annotate_with_symbols, impact_analysis, export_context, export_context_for_symbol, add_related_from_file) propagates internal DB errors instead of converting them into "no matches" / "no symbols".
  • churn_percentile now distinguishes "no git row" (Ok(0.0)) from DB error (Err), so a broken git-index doesn't silently lower risk scores.

Changed

  • Orphan-file cleanup (structural + semantic indexers) now runs inside a single transaction instead of one autocommit per deleted file. On a post-rename or mass-delete index this removes N fsyncs.
  • Incremental indexing (structural + semantic) replaces its N+1 get_file_hash pattern with a single-query all_file_hashes preload. On 25k-file repos this cuts thousands of SQL round-trips to one sequential scan.
  • Incremental git-history indexing preloads git_co_changes pair keys once (all_co_change_pairs) instead of running SELECT COUNT(*) per delta pair inside the write transaction. Uses HashMap<String, HashSet<String>> so membership probes are alloc-free.
  • Search boost path now calls symbol_exists (LIMIT 1 probe) instead of find_symbols when all it needs is non-emptiness for a token.
  • annotate_with_symbols batches its per-file symbol lookup via symbols_for_files, replacing N queries with one multi-path query. Same batching applied to semantic chunk augmentation during indexing.
  • Tree-sitter symbol and reference queries now compile once per language into LazyLock<QuerySpec> statics (with cached capture indices) instead of being recompiled on every file parse. On a 25k-file repo this removes 50k Query::new invocations and their capture_index_for_name scans.
  • Database::insert_chunks takes &[(&str, u32, u32, &[f32])] instead of &[(String, u32, u32, Vec<f32>)]. Semantic indexing stops cloning chunk text and embedding vectors just to satisfy the API; on repo-sized batches this saves hundreds of megabytes of redundant allocation.
  • embedding_to_bytes uses a single memcpy on little-endian targets instead of per-element to_le_bytes. The function is hit once per chunk write and once per search query.
  • Reranker score_batch stops building an intermediate Vec<(String, String)> for tokenizer pairs; it now borrows query + doc refs directly.
  • Database::remove_file now cascades the deletion to git_files and git_co_changes. Deleted files no longer linger in find_coupling / assess_risk output after an index refresh.
  • Schema migrations now live in a registry (MIGRATIONS + schema_migrations table) rather than as hand-wired function calls inside init_db. Adding migration 0002, 0003, ... is now a one-line addition. The first migration (0001_refs_name_tail) is recorded on both fresh and legacy databases so subsequent init calls are no-ops.
  • crates/graph/src/git_history.rs (1149 lines) split into git_history/{mod.rs, indexer.rs, risk.rs, tests_rec.rs}. Public API unchanged.
  • crates/storage/src/db.rs (1230 lines) split into db/{mod.rs, structural.rs, semantic.rs, git_hist.rs} via impl Database blocks per concern. Public API unchanged.
  • MCP tool responses now use the structured CallToolResult shape (isError: true on failure per MCP spec, structured_content on success alongside the pretty-printed text). MCP clients can programmatically distinguish errors from successful results instead of regexing "Error: " strings.
  • ImpactTarget::from_hint and ExportRequest::from_target in the protocol crate centralize the CLI↔MCP classification/construction helpers. Previously duplicated (with slight shape differences) between cli::main and cli::mcp.
  • CLI + library crates now log via the tracing crate instead of raw eprintln!. tracing_subscriber initializes at binary startup writing to stderr (keeps stdout clean for the MCP transport and CLI JSON output). Log level follows RUST_LOG, defaults to info.
  • Hand-rolled DistRow newtype + BinaryHeap in the multi-language search merge path replaced by a sort_by on the merged vec. No behavior change.
  • Duplicated format_bytes / git_common_dir helpers in cli::main and cli::doctor pulled into cli::util. format_bytes now uses GiB/MiB/KiB consistently (doctor previously reported GB/MB/KB powers-of-1024 with different labels).

0.3.3 - 2026-04-15

Test discovery for Laravel/Symfony mirror-tree projects + test backfill.

Added

  • recommend_tests now finds tests for Laravel mirror-tree layouts. Source at app/<rest>/<file>.php resolves to tests at tests/{Unit,Feature,Integration,Browser}/<rest>/<file>Test.php. Previously only flat tests/Unit/FooTest.php was checked, missing tests like tests/Integration/Actions/CredentialingApplication/ExportZipActionTest.php.
  • recommend_tests now finds tests for Symfony mirror-tree layouts. Source at src/<rest>/<file>.php resolves to test at tests/<rest>/<file>Test.php (no Unit/Feature subdir; tests/ mirrors src/ directly).

Fixed

  • Inline unit tests for accumulate() cover the v0.3.1 source↔test pair filter (previously verified only via integration). Pins down the rule: source-test pairs are kept; test-test pairs are skipped.

0.3.2 - 2026-04-15

Performance fix.

Fixed

  • codesage git-index was issuing one fsync per upserted row. On large repos like php-src (~25k files + ~10k pairs per worktree) this burned multi-minute disk waits in jbd2_log_wait_commit — visible as ~1% CPU per process. Now wraps clear + every upsert in a single SQLite transaction (one fsync per indexer instead of N). Estimated 10-20x speed-up on large repos with the same disk; bigger when multiple indexers share the same disk journal.

0.3.1 - 2026-04-15

Bug-fix release for two issues exposed by dogfooding 0.3.0 against php-src.

Fixed

  • codesage git-index was dropping test-like files from co-change pair generation entirely, which made recommend_tests's coupled section empty for codebases where tests are the primary partner of source changes (e.g., php-src .c.phpt). Now only test↔test pairs are skipped (which is the actual noise); source↔test pairs are kept (which is the signal recommend_tests needs).
  • recommend_tests for .c / .h source files now lists .phpt tests in <dir>/tests/ as primary (php-src convention). Skips when the tests/ directory has more than 50 files (typical of ext/standard/tests/) — that's too noisy for "primary" and the agent should rely on coupling history instead.

Required action

Re-run codesage git-index --full in any repo that was indexed under 0.3.0 to repopulate co-change pairs with the corrected logic.

0.3.0 - 2026-04-15

V2b slice 2: tools that change agent behavior per-task instead of just informing it.

Added

  • assess_risk_diff(project, file_paths[]) MCP tool and codesage risk-diff CLI command. Aggregate risk for a set of files (the file list of a patch). Returns per-file decomposition plus rollups: max_score, mean_score, max_risk_file, and lists of files in each risk category (hotspot, fix-heavy, test-gap, wide blast radius). Output includes paste-ready summary_notes for PR descriptions. Use BEFORE submitting a patch to decide whether to add tests, split the change, or flag concerns.
  • recommend_tests(project, file_paths[]) MCP tool and codesage tests-for CLI command. Returns the tests an agent should run after editing a set of files. Two layers: primary (sibling tests resolved by language convention — FooTest.php, foo.test.ts, test_foo.py, foo_test.go, plus Rust integration tests under crates/<name>/tests/*.rs) and coupled (tests that historically change with the input files via git co-change history). Coupled entries are deduped against primary so each test appears once. Empty result means no test files in the index for these paths.
  • Both new CLI commands accept positional file args or read newline-separated paths from stdin, so they compose with git diff --name-only | codesage risk-diff and similar pipelines.
  • discover::TEST_LIKE_EXCLUDE_PATTERNS: subset of the default exclude list covering test and bench files. Exposed so git_history can split test files from hard-excluded files.

Changed

  • codesage git-index now keeps test and bench files in git_files so recommend_tests and assess_risk test-gap detection can find them. Test files remain dropped from co-change pair generation, so coupling rankings stay focused on production code. Re-run codesage git-index --full after upgrading to populate test files.

0.2.1 - 2026-04-15

Hardening release. New defensive mechanisms around accidentally committing private data plus the public-release CI surface (CI, secret scan, automated release notes from CHANGELOG).

Added

  • codesage install-hooks now installs a pre-commit hook when the repo contains scripts/leak-check.sh. The hook greps staged content against extended-regex patterns from scripts/leak-patterns.txt (tracked, shared) and .git/info/leak-patterns.txt (local-only, per-developer) and blocks the commit on a match. Bypass with git commit --no-verify when a false positive is intentional.
  • scripts/leak-patterns.txt with default patterns for private-key material and common token formats (AWS, GitHub PATs, Slack, Stripe live keys).
  • Pre-commit filename policy. The hook blocks any staged file whose name matches a secret/credential pattern: .env* (except .example, .template, .sample), .secret, .secrets/, *.pem, *.p12, *.pfx, id_rsa* / id_dsa* / id_ecdsa* / id_ed25519* (except .pub public-key variants), credentials.json, service-account*.json.
  • scripts/leak-check.sh supports --range A..B and --all modes so the same script powers both the pre-commit hook and CI.

Changed

  • .gitignore excludes benchmark artifacts under bench/ (results, corpora, history, scorecards) and common secret filenames (.env*, *.pem, id_rsa*, credentials.json, etc.) so local data never enters a commit by default. Template files (.env.example, .env.template, .env.sample) remain committable.

0.2.0 - 2026-04-15

Initial public release.

Added

  • Semantic search via MiniLM-L6-v2 embeddings (384d) with optional cross-encoder reranking (ms-marco-MiniLM-L6-v2). Query-token symbol boost runs before rerank; final scores blend 50/50.
  • Structural graph: symbol definitions, references (imports, calls, inheritance), and file-level dependencies. Tree-sitter queries cover PHP, Python, C, Rust, JavaScript, and TypeScript.
  • Change impact analysis via codesage impact <symbol|file> and the impact_analysis MCP tool. Walks reverse dependencies to a configurable depth and annotates each hop with distance and reason.
  • Context export to JSON, markdown, or flat-text (gitingest-style) via codesage export and the export_context MCP tool. Optionally includes callers and callees.
  • Git history intelligence. codesage git-index populates per-file churn (180-day exponential decay), fix ratio, total commits, last commit, and pairwise co-change weights. Two MCP tools consume it:
    • find_coupling(project, file_path, limit): top files that historically change together (CLI: codesage coupling <file>).
    • assess_risk(project, file_path): composite score from churn percentile, fix ratio, depth-2 dependent pressure, coupling pressure, and test gap. Emits notes you can paste into a PR description (CLI: codesage risk <file>).
  • Incremental git history indexing with auto-refresh via git hooks. codesage git-index takes --full or --incremental; the default is auto (incremental when prior state is valid, else full). Rebased or force-pushed history falls back to full when the stored commit is no longer an ancestor of HEAD. Re-running against an unchanged HEAD returns in ~5 ms.
  • codesage doctor health checks: binary, config, database, disk space, hooks, CUDA, models, MCP registration. Supports --json.
  • codesage-tools Claude Code plugin. Slash commands: /codesage-onboard, /codesage-reset, /codesage-reindex, /codesage-bench, /codesage-eval. One global MCP server, routed per call by an absolute project argument.
  • Husky-aware codesage install-hooks. Detects core.hooksPath and Husky markers, writes to .husky/<name>, and adds installed paths to .git/info/exclude. Installs post-commit, post-merge, post-checkout, and post-rewrite.
  • Worktree-aware hooks. Each hook body uses git rev-parse --show-toplevel at runtime, so a single hook serves every worktree under a shared .git/ common dir.
  • Expanded default ignore patterns (~85 entries) covering tests, vendored dependencies, build outputs, and language caches. User exclude_patterns in config.toml extend the defaults instead of replacing them. Changelog files (NEWS, UPGRADING, CHANGELOG, HISTORY, RELEASE_NOTES) are excluded because they touch every commit and pollute co-change.
  • MCP responses cap at ~8000 tokens with truncation metadata. Large outputs degrade gracefully instead of overflowing downstream contexts.
  • Parallel indexing via Rayon with batched SQLite writes.
  • Portable CUDA and ONNX Runtime discovery. The binary checks CODESAGE_NVIDIA_LIBS, then Python site.getsitepackages() and user site-packages, then standard system paths (/usr/lib/x86_64-linux-gnu/nvidia, /usr/local/lib/nvidia, /opt/nvidia). ORT_DYLIB_PATH overrides the ONNX Runtime location. codesage doctor reports how many NVIDIA lib directories it found.
  • Bench plugin commands honor CODESAGE_BENCH_CORPUS_DIR (default: ./bench-corpora). No hardcoded personal paths.
  • Workspace-level versioning. [workspace.package] owns the version; every crate inherits via version.workspace = true, so releases bump in one line.