- 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
codesage index --no-featuresskips the feature-mapping stage.codesage status --jsonandcodesage export --json.codesage doctorwarns 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 cleanupchecks free disk space before VACUUM and fails up front with a clear message instead of mid-operation.
codesage indexskips feature mapping when an incremental pass changed no source files or build manifests. Full indexes andcodesage mapalways 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
_metaenvelope (truncation, staleness);find_coupling,export_context, andfeature_bundledescriptions document the structuredfoundfield. - Daemon shutdown waits up to 5 seconds for in-flight MCP requests to finish before exiting.
searchapplies 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.
- Watcher: edited
.hheaders 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 malformedconfig.tomlskips the watcher with a warning instead of silently droppingexclude_patterns. - The daemon no longer recreates a deleted
.codesage/index.dbas an empty index; reads report the project as not onboarded. codesage mcpwaits 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..phptfiles 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_endtolerates 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.
- 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
/codesage-reviewadds--focus all|productand--max-verify-findings; product focus skips test-suite,bench/, andscripts/slices while full coverage remains the default./codesage-revalidategains--max-verify-findingsand runs new regression findings through the same adversarial verifier.
feature_bundlereserves up to two existingrelatedslots for requested caller/callee chunks before backfilling tests and context, keeping the response limit unchanged./codesage-reviewranks 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-reviewand/codesage-revalidateresponses to cover a deterministic entry/changed/risk-ranked file set before findings merge.
- 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
fixedfinding instead of silently absorbing it; revalidation drops rediscovered untargeted findings asout_of_scopeinstead 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-reviewanchors all state paths at the project root, exempts an explicit--featurefrom 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 a
codesage-retrievalskill (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.
- Rationale-comment extraction (
WHY:/NOTE:/IMPORTANT:/TODOmarkers surfaced onfind_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
/codesage-reviewgains--deep(multi-lens reviewers on high-risk slices),--no-verify,--model, and--verify-model; new findings pass an adversarial verification stage (newcodesage-finding-verifieragent) by default.CODESAGE_HF_DOWNLOAD_TIMEOUT_SECSbounds Hugging Face artifact downloads during model load.
- Upgrade the
rmcpMCP SDK to 2.x (MCP 2025-11-25 spec model types). /codesage-review//codesage-revalidatedrop findings whose quoted evidence doesn't match the cited file, computefinding_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-revieweragent: explicit review procedure (trust-boundary tracing, error-path walks, self-audit), contract lookups viafind_symboloutside the slice, risk-scaled read depth, lens mode, and severity calibration anchors.assess_riskreturnsfound: falsefor paths absent from both the structural and git-history indexes;assess_risk_diffreturnsempty_input: truewith a note for empty file lists.- MCP tools cap large limits, depth, and file-path lists instead of accepting unbounded request sizes.
- MCP
session_startreturns a compact summary plussnapshot_path; the full file list remains persisted in.codesage/sessions/<session_id>.json. find-symbol --kindandfind-references --kindreject unknown values instead of running unfiltered.
- 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_rehearsalincludes high-risk files preserved insideassess_risk_diff.clustered_directories[].top_files.list_dependenciesreportsfound: falsewith a note for paths absent from the structural index.find_similarMCP params acceptmin_jaccardas a JSON string number, rejecting non-finite values (nan/inf) that would silently zero results.assess_risk_diff,assess_risk_batch, andrecommend_testsMCP calls reject emptyfile_pathsinstead of returning successful empty payloads.searchno longer creates a table on the query path when the BM25 FTS sidecar is missing, so it works against a read-only index.impact_analysisdeduplicates traversal by file, qualified name, and line instead of qualified name alone.searchapplies 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.
.codesagedirectories andindex.dbsidecars 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-epochto 0.9.20, clearing RUSTSEC-2026-0204 (invalid pointer dereference infmt::Pointer).
0.14.0 - 2026-07-02
- MCP tools advertise
readOnlyHint/openWorldHint: false;session_start/session_endsetreadOnlyHint: false. --lock-wait <secs>oncodesage indexandgit-index; installed hooks pass--lock-wait 60. Reruncodesage install-hooksto refresh existing hooks.foundboolean onexport_context,feature_bundle, andfind_couplingresults.- Migration runner refuses to open an index carrying an unknown
breaking_-prefixed migration.
- 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-levelvar, C double-pointer returns, and Java enum constants. assess_risk/codesage riskgive structure-aware cycle-breaking guidance (ring: lowest-weight break edge; hub: top decoupling targets).- CLI
--jsonforfind-symbol/find-references/search/similarwraps results in{"results": [...]}. - MCP
kind/languagefilter params are typed enums; an unknown value errors instead of returning unfiltered results. - The daemon reloads
.codesage/config.tomlper project on change withoutcodesage 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.filesand records dropped entries in_meta.dropped_files. codesage doctorfails a non-allowlisted model or reranker, namingCODESAGE_ALLOW_ANY_MODEL.codesage export --formatrejects an unknown value instead of rendering markdown.codesage cleanupreports failed vec-table drops and exits non-zero when any drop fails.codesage watch runresolves the project root by walking up, matchingwatch status/stop/start.
- Chunk
end_lineno 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_filepurges them. - Non-Linux daemon startup no longer SIGTERMs a recycled pid; siblings are validated against their own socket.
assess_risk/review_rehearsalflag Pythonos.system/os.popen/os.exec/os.spawn/pty.spawnas 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_overviewsurface 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".
- Embedding/reranker model names from repo config are allowlisted before download or load;
CODESAGE_ALLOW_ANY_MODEL=1overrides. - The leak-check pre-commit hook is opt-in via
codesage install-hooks --with-leak-check, not auto-installed. scripts/leak-check.shscans staged files with non-ASCII names instead of reading them as empty.codesage installwrites 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
find_similarMCP tool andcodesage 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 migration0012_symbol_fingerprints).- Rust
find_referencesnow captures method calls (obj.method()) andimpl Trait for Typeedges (trait as inheritance, type as type_hint). - PHP
find_referencesnow emitstype_hintreferences for parameter, constructor-promoted-property, and return type hints; the same signal feedsassess_risktrust-boundary derivation. - PHP
find_symbolnow resolves enum cases (PHP 8.1case Hearts;) as constants.
codesage initwrites a committed.codesage/.gitignore(instead of a per-clone.git/info/excludeentry) so index/session/review state stays out of version control for every teammate, including plugin-onboarded ones.searchcaps the KNN + reranker candidate pool on deep pagination, so a largeoffsetno longer balloons cross-encoder work (pages past the cap return fewer results).
impact_analysis/assess_riskreverse-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 inflateddependent_filesblast 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_similarfingerprinting no longer recurses per AST depth; a pathologically deep source file can't overflow an indexer worker stack and abortcodesage index.- PHP
find_referencesno longer double-counts in-class trait uses (use SomeTrait;) as both an import and a trait_use. session_start/project_overviewtop-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_analysisbounds 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/*) outsidecrates/are mapped instead of dropped; php-src#elif/#elsebranches under#if 0are no longer blanked (their symbols were lost). FileCategory::classifyrecognizes./-prefixed relative paths, so test/config files passed with a leading./are categorized correctly.- MCP tool schemas no longer advertise schemars' non-standard numeric
formatvalues (uint32,uint,float, …), so strict clients (e.g. opencode) stop logging "unknown format" warnings; unsigned params keep theirminimum: 0constraint.
0.12.0 - 2026-06-13
- The MCP daemon evicts pooled embedder/reranker models after they sit unused (default 15 min,
CODESAGE_MODEL_IDLE_SECS,0disables), freeing their GPU VRAM and (viamalloc_trim) some host memory while keeping the daemon and its client connections alive; the next query reloads them. The daemon now also spawns withMALLOC_ARENA_MAX=2unless overridden.
- 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; onlysearch/index/exportand thewatch/daemon/mcp --directservers 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
project_overviewMCP tool andcodesage overviewCLI 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_rehearsalMCP tool andcodesage rehearseCLI 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, andscope-spreadwhen a patch touches ≥4 unrelated feature areas) fromassess_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.
impact_analysisgains opt-ininclude_forward(forward dependencies),include_siblings(same-file symbols),limit, andsummary_onlycontrols; the result is now an object withresultsplus the requested extras (codesage impact --forward --siblings --limit --summary-only). Reading.resultsis unchanged.find_referencesnow populates each reference'sfrom_symbolwith the enclosing caller symbol (null at file scope), andimpact_analysiswalks 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.gitignoreand[index].exclude_patternsand skips ignored top-level trees (target/,.git/,node_modules/) from the watch set. On by default; disable with[index] watch = falseorCODESAGE_WATCH=0. Self-exits after idle (CODESAGE_WATCH_IDLE_SECS, default 1800;0disables). codesage watch run|status|stop|start [project]to manually control the live watcher (runis a foreground instance with its own embedder).
- CUDA verification now fails when neither cuDNN nor cuBLAS is loaded after GPU session creation.
recommend_testsnow finds primary sibling tests from the structural file index, independent of git-history rows.- CMake
target_sourcesfeature mapping no longer records laterPUBLIC/INTERFACEscope keywords as owned files. - CMake
add_executable/add_libraryfeature mapping no longer records option keywords (WIN32,MACOSX_BUNDLE,EXCLUDE_FROM_ALL) as owned files. - The live watcher now applies nested
.gitignorefiles consistently with indexing discovery.
0.10.0 - 2026-06-11
- 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 --verbosefor 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 uninstallsupport for Codex and opencode. - Add default project injection with
codesage mcp --project <root>. - Add optional line numbers to
export_contextandfeature_bundle. - Add repo-size-based token budgets for
export_contextandfeature_bundle. - Add Laravel route-handler references for
impact_analysisandfind_references.
- 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=0to 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.
codesage cleanupno longer treats every vec table as orphaned when the active semantic table is missing.codesage cleanup --dry-runno longer loads the embedder.codesage mcp --project,codesage install, andcodesage install-hooksreject non-UTF-8 paths instead of writing lossy values.codesage indexnow prunes excluded directories, skips unreadable or deleted files, and fails when trust-boundary backfill fails.- Chunk merging and overlap now respect
chunk_size. impact_analysisrejects 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_featurestag filters now escape SQLLIKEmetacharacters.serde_jsonimports now map to theserializationtrust boundary.list_dependenciesnow reports reverse file importers.ReferenceKindJSON now uses snake_case names and still accepts the legacy spellings.--versionhandling no longer intercepts subcommand arguments or panics on non-UTF-8 argv.codesage initrejects a non-directory.codesagepath.CODESAGE_NVIDIA_LIBScan point directly at CUDA library directories.- Unknown
devicevalues 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 statusandcodesage daemon stopcan find daemons from previous builds.codesage daemon statusandcodesage daemon stopuse a stable/tmp/codesage-$UIDfallback acrossTMPDIRchanges.session-startandsession-endvalidate snapshot paths and loaded session IDs.session-endflushes its report before exiting nonzero.codesage mapnow takes the project index lock.- Generated git hooks shell-quote the
codesagebinary path.
codesage daemon statusandcodesage daemon stopvalidate runtime dirs and pid identity before trusting pid files.codesage daemonrefuses a symlinked or foreign-owned/tmp/codesage-$UIDruntime dir.- Non-Linux
codesage daemon status/stopno longer trust a stale pid unless the daemon socket is reachable. list_features --sinceand MCPsincereject option-like git refs.
0.9.0 - 2026-06-01
- Django URL route feature slices. New
django-route/FeatureKind::Routeseed source. The Python mapper now scans.pyfiles that import fromdjango.urls/django.conf.urlsand declare aurlpatterns = [...]list forpath(),re_path(), and legacyurl()entries, emitting oneroutefeature 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 ontoentry_symbolwhere possible (views.home→home,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.tsdjangoRouteSeeds). Closes the most common Python web framework gap, since Flask + FastAPI shipped in 0.7.x. Four regression tests coverpath()routes with converter stripping,re_path()regex normalization, class-basedas_view()symbols withinclude()skipped, and the django-import gate. auth-sensitivetag on shape-flagged route slices. Route seeds whose shape suggests a privileged or state-changing surface now carry anauth-sensitivetag: the HTTP method is anything other thanGET/HEAD, or a path segment is exactlyadmin/auth/login/token(segment-exact and case-insensitive, so/authorsdoes 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 withlist_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_sensitivelives in the shared mapper module with three unit tests (method, path-segment, empty-method path-only).- CUDA support:
.cu/.cuhindexing, target detection, and concurrency boundary..cu/.cuhfiles 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 insearch/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.cufile doesn't have its.hheaders re-routed to C++. In the C/C++ mapper, CMakecuda_add_executable/cuda_add_library(legacy FindCUDA macros) are parsed alongsideadd_executable/add_library, and any target — however declared — that pulls in a.cu/.cuhsource is taggedcuda; standalone.cufiles with a top-levelmain()get the tag too. Filter withlist_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 aconcurrencyboundary (inherited by C++), so any indexed file that includes them — now including.cu/.cuhthemselves — surfacesconcurrencyinassess_riskthrough the normal per-file derivation. Ports clawpatch's CUDA mapper support (c-cpp.ts, commit35497e9). Eight regression tests: four in the mapper (cuda_add_executable, plainadd_executablewith a.cusource, standalone.cumain, 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>onfeatures-list(CLI) andsinceonlist_features(MCP). Restricts the feature list to slices whose entry, owned, or context files changed since a git ref, viagit 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, Cmain()binaries) the entrypoint is itself the source file, recorded only with theEntryrole; 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. Newcodesage_graph::changed_files_sincehelper wraps the git call, mirroring clawpatch'schangedFilesSince.- Qualified-name boost on
search, opt-in viaCODESAGE_QUALIFIED_NAME_BOOST=1. Newapply_qualified_name_booststage 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 additiveapply_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. querydefaultmatchingMode::defaultdoesn'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 atnotes/20260526-search-lift-ab-report-round2.mdandnotes/20260527-search-lift-ab-report-round3.md. Ports half of code-review-graph commitc04af36; 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.
- CMake test-target classification respects target-name precedence.
is_cmake_test_executablewas using pure OR logic (target name matches test pattern OR any source matches test path), so a regular binary likeadd_executable(app src/main.c src/test_mode.c)flipped tocmake-test/ TestSuite because the helper sourcetest_mode.cmatched 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 commitde82d0aand 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_LTLIBRARIESsource lists containing$(srcdir)/foo.cor${srcdir}/bar.cpppreviously fell through toprefix_dirverbatim, producing broken paths likesubdir/$(srcdir)/foo.cinentry_pathandowned_files. Newexpand_automake_varshelper strips$(srcdir)/and${srcdir}/to the empty prefix (the value the makefile's own directory implicitly contributes, which the existingprefix_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 commit39a2545. 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.clands atsubdir/thing.cend-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}failedis_valid_target_name(which rejects any token containing$). Newcmake_project_namehelper pre-scans the CMakeLists.txt body forproject(NAME ...)calls (last call wins per CMake semantics) andresolve_cmake_target_namesubstitutes${PROJECT_NAME}and${CMAKE_PROJECT_NAME}with the captured name before the validity check. When noproject()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 toadd_executable,add_library, andtarget_sourcesso late-bound sources attach to the resolved target. Mirrors clawpatch commit8550604. 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 onproject(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/$UIDtrap injectsXDG_RUNTIME_DIR=""in the client's MCP env so the shim falls through to/tmp/codesage-$UID. The fallback indefault_runtime_dirwas reading bothCODESAGE_DAEMON_RUNTIME_DIRandXDG_RUNTIME_DIRviastd::env::var_os, which returnsSome("")for set-but-empty vars rather thanNone. The empty string then collapsed to a relativecodesage/runtime dir that the daemon tried to create next to whatever the shim's cwd happened to be at spawn — silently littering arbitrary directories withcodesage/mcp-<version>-<hash>.{sock,pid,lock,log}when cwd was writable, and surfacingFailed to connectfor 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-$UIDfallback also kicks in for any client that explicitly blanks the var. Regression test incrates/cli/src/daemon.rsasserts the resolved path is absolute and rooted understd::env::temp_dir()when both vars are empty. - Reranker model loading matches the embedder's path.
Reranker::newandEmbedder::newnow share oneload_onnx_sessionloader (an internal dedup), which closes two reranker-only gaps: the reranker now fetches the optionalonnx/model.onnx_dataexternal-weights sidecar — so a >2 GB cross-encoder no longer fails at session commit — and, whendevice = "gpu"is configured on a binary built without thecudafeature, it fails fast before downloading the model instead of after. No config or API change.
0.8.1 - 2026-05-20
- React component feature slices. New
react-component/FeatureKind::Libraryseed source, one feature per.tsx/.jsxfile under conventional component dirs (src/pages,src/components, top-levelcomponents/) of every package that declares a React dependency. Capped at 100 components per package. Stories /.storybook// fixtures /testdata//.d.ts/.test.tsx/.spec.tsxfiles are excluded. Files already owned by areact-router-route,next-app-route,next-app-page, ornext-pages-routeseed are skipped so a route declaration file doesn't double-emit. Givesfind_feature("src/components/Button.tsx")an answer (previously returned nothing for unrouted components) andfeature_bundlea single-component target for review. Ports clawpatch react.tscomponentSeeds(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 bothsrc/pagesandsrc/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.
- 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 acmake-test/FeatureKind::TestSuiteseed instead ofcmake-bin/CliCommand. Soadd_executable(foo_tests test_main.cpp)lands inrecommend_tests(and stays out oflist_features --kind cli-command) rather than being recorded as a shippable binary. Ported from clawpatch commit f21b76c. Test-path detection uses a self-containedis_c_or_cpp_test_pathhelper mirroring upstream'sisCOrCppTestPath: undertests//test//__tests__/, basename prefixtest_/test-, stem suffix_tests/-test(and plural), orFooTest.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 (
cmakeCommandArgsparen-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 Rustregexcrate 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 andmethods={…}set literals as alternatives to the previously-only-supportedmethods=[…]list; (4) stacked decorators between the route decorator and the handlerdef(e.g.@app.get('/admin') / @auth_required / def admin_panel():). The handler function name now lands on the seed'sentry_symbol, sofind_featureandfeature_bundlereturn the actual handler in addition to the route; previously the field wasNonefor every Python route.api_routewithout an explicitmethods=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 coverapi_routeexpansion, multi-line decorators, tuple/set method containers, function-name capture for both frameworks, stacked decorators, and theapi_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.
- 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 insidemessage("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, leavingowned_fileswith 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 threeadd_executable/add_library/target_sourcesregexes withcmake_command_args, an explicit walker that tracks paren depth and skips over quoted strings ("...", including\Xescapes) and bracket arguments ([[...]],[=[...]=]) so a)inside a string is treated as ordinary content. Word-splitting moved intocmake_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/STATICetc. no longer leak as phantom sources), and direct unit tests for the walker primitives. The existing 23 C-mapper tests still pass unchanged. install-hooksanddoctorno longer refuse repos wherecore.hooksPathis 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
-
Java as a supported language.
.javafiles now parse class, interface, enum, record, method, constructor, field, and@interface(annotation type) symbols plus method-call,newexpression, inheritance, import, and annotation-usage references (@Override,@Test,@Component, including@pkg.Fooscoped 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_symbolandexport_contextnow populateSymbol.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 adef/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 thedecorated_definitionwrapper so# TODO:above@app.route(...)/@property/@dataclass/@lru_cacheetc. still attaches; the walker also climbs from a class body's first method into the class header so comments tree-sitter parks atclass_definitionlevel still attach. Markers inside string literals (e.g.,s = "# TODO not a comment") do not trigger because the extractor reads tree-sittercommentnodes, 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 eachcodesage mcpprocess loading its own copy. The socket lives under$CODESAGE_DAEMON_RUNTIME_DIR,$XDG_RUNTIME_DIR/codesage, or/tmp/codesage-$UIDwith 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 --directpreserves the previous in-process stdio server path for debugging, andcodesage daemonruns the foreground daemon explicitly. HTTP/SSE remains intentionally out of scope. -
Daemon management subcommands.
codesage daemon statusprints the running daemon's pid, socket path, reachability, and log path (exit 1 when not running).codesage daemon stopsends 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.
- MCP daemon hardening (review pass). Concurrency, lifecycle, and operational fixes after the daemon's initial review. Concurrency:
search/export_contextno 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-keyMutex<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_socketpolls 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 viaSO_PEERCREDand refuses connections from foreign UIDs (defense in depth for the$CODESAGE_DAEMON_RUNTIME_DIRoverride). Lifecycle: SIGTERM / SIGINT trigger graceful shutdown that removes socket + pid files;serve_clientenforces 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_stdioselects 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 ofDefaultHasher; on non-Unix,--runtime-diris 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)andPHP_FUNCTION(name)definitions and emits onephp-ext-method/php-ext-functionlibrary feature per public API entrypoint. This applies to both repo-root Composer/PIE extensions (config.m4/config.w32+php_<name>.h) and php-src-styleext/<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.phpttests undertests/orext/<name>/testsattach 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-routeseed 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 oneroutefeature 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), Fastifyregister, Honoroute`) 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-routeseed sources) andsetup.cfgconsole-script support (setup-cfg-script). Ports clawpatch PRs #11 (Flask), #15 (FastAPI), and #28 (setup.cfg). Flask: scans.pyfiles thatimport flaskfor@<receiver>.route('/path', methods=[…])decorators wherereceiveris a local variable initialized fromFlask(…)orBlueprint(…). Defaults toGETwhenmethods=is absent. Emits one route feature per (method, path), somethods=["GET","POST"]produces two seeds withentry_route"GET /users"and"POST /users"rather than collapsing to a single feature_id. FastAPI: same pattern against@<receiver>.METHOD('/path')forapp = FastAPI()androuter = APIRouter(). Both walkers require animport 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: blueprinturl_prefixandinclude_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 aspyproject.toml [project.scripts]. Module path resolves throughresolve_script_module_pathsoentry_pathlands on the real.pyfile (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 singleServiceanchor withframework:laraveltag, plus phpunit.xml / .env.example / config/app.php / config/database.php / routes/{web,api,console}.php as context_files),laravel-job(per-class underapp/Jobs/**,FeatureKind::Job, taggedjob+async),laravel-service(app/Services/**,FeatureKind::Service),laravel-model(app/Models/**, taggedeloquent+database),laravel-migration(groupedConfigslice overdatabase/migrations/**, owning up to 50 files because migrations rarely make sense one-at-a-time),laravel-seeder(same grouped shape fordatabase/seeders/**),laravel-test-suite(one TestSuite per existingtests/{Unit,Feature,Integration,Browser}directory, owning up to 50 files each,test_command = "composer test"), andcomposer-script(composer.jsonscriptsentries restricted to an allowlist ofsetup,dev,test,typecheck,lint,format,analyse,analyze,deploy*to keeplist_featuresfrom filling with one-off pipeline scripts; thetestscript lands asFeatureKind::TestSuitewith 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_routespreviously only scanned the repo root'sapp/andpages/, so a monorepo withapps/web/app/...orapps/marketing/pages/...emitted zero Next route features. Both now run relative to each discovered workspace package root and emit per-route seeds with theworkspacetag and a summary that names the owning package. (2) Nested frontend root discovery. When the repo root has noapp/orpages/AND no workspace package covers them, the mapper additionally probes the conventional dirsfrontend/,client/,web/,ui/for a nested Next/Vite app one level down. Lets a Rust-or-Go-backed repo with a React frontend infrontend/get its routes mapped without declaring an npm workspace. (3) Turbo-awaretest_commandrewrite. Whenturbo.jsonexists at the repo root AND declares atesttask (in either Turbo 1.xpipelineor 2.xtasksshape), every workspace package'stest_commandflips from the bare<pm> --dir <pkg> testshape toturbo 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 noapp/, the Turbo rewrite when atesttask is declared, and the negative case whereturbo.jsonexists but only declaresbuild. Smoke-tested on a synthetic monorepo: 3 packages + 3 routes (2 fromapps/webapp-router, 1 fromapps/marketingpages-router) all mapped, all three packages gotturbo run test --filter=@m/<pkg>.
- 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-boundtarget_sources(name PRIVATE|PUBLIC|INTERFACE src…)declarations are now parsed and merged into the matchingadd_executable/add_librarytarget; previously a target declared withadd_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 whoseowned_filesis empty after exclude filtering (e.g., a vendored INTERFACE library whose only file lives undervendor/) are dropped rather than emitted as phantom seeds. (9)INTERFACElibraries with only header sources still surface so reviewers can see the API. (10)**/CMakeFiles/**added toHARD_EXCLUDE_PATTERNS(joining the existing**/cmake-build-*/**) so the CMake build-tree state directory stays out of every index. (11) Thec-mainwalker now skips files undertests/,test/,__tests__/,Tests/or with_test/Teststem suffixes, so googletest/catch2/custom harnessmain()files no longer surface as CLI features when the project hasn't configured[index].exclude_patterns. Smoke-tested on libharu (3 features incl. onecmake-binfrom 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 likeRoute::prefix('a')->prefix('b')->get('/c', …)resolve to/a/b/c. (2)routes/api.phpimplicit/apiprefix. 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 withuse App\Http\Controllers\UserController;(or anas Aliasrename) 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/innerprefix()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_routescollapsed from two regexes-plus-by-key-map to one route regex + one controller-group regex; new helpersparse_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_implhelper that pre-strips comments / strings / template literals before the Express/Fastify/Hono route regex iterated bytes and emittedb 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 inentry_route, and because that field feeds thefeature_idhash,find_feature("/café")from an agent never matched. Rewritten to walk bycharinstead 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_referencespreviously didto_name[1..to_name.len() - 1]with no length guard, panicking withslice index starts at 1 but ends at 0whenever 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 intostrip_surrounding_quoteswithlen >= 2guard 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 declaringreactinapps/web/package.json(root has no React dep) missed every React Router slice; same for Express inapps/api/package.json. Newany_package_has_dephelper ORs the root package with every discovered workspace package.react_router_routesalso now scanssrc/+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 requiredpath=beforeelement=AND broke onelement={<UsersPage />}because the nested/>ended the[^>]*capture early. Replaced withextract_route_tag_bodies, a{}-depth-aware byte scanner that captures the full attribute body, followed by independentpath=/element=regex passes. Matches<Route element={<X />} path="/users" />(element-first) and<Route path="/users" element={<X />} />symmetrically. list_featurestag filter now matches substrings as documented. The SQL bound%"{tag}"%(literal quote anchors around the tag), sotag="framework"missed compound tags likeframework:react-routerdespite the docstring promising "tag substring" semantics. Changed to%{tag}%. Newcrates/storage/tests/features_test.rsintegration 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 indexandcodesage git-index --incrementalin 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-runcodesage install-hooksto pick up the new body. Hook-body generation extracted intogenerate_post_commit_hook_bodywith two regression tests asserting&&chaining and a single top-level&. bench/concurrency-audit.pybackups now snapshot the full database.backup_dbcopiedindex.dbwithout checkpointing;restore_dbdeleted the live-wal/-shmsiblings and overwroteindex.dbwith the un-checkpointed snapshot, silently dropping any committed-but-not-yet-checkpointed transactions. Common after a freshcodesage indexbecause WAL checkpoints are lazy. Now runsPRAGMA wal_checkpoint(TRUNCATE)beforeshutil.copy2, so the.dbsnapshot is a self-contained image of the database.bench/concurrency-audit.pyaborts when the checkpoint is contested.PRAGMA wal_checkpoint(TRUNCATE)returns(busy, log, checkpointed); abusy=1row means WAL frames remain on disk and the.dbsnapshot 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'sindex.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-fileworks. The harness was passing--append-system-prompt-file <path>toclaude -p, which is not a real flag.claudeaccepts 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 whenclaudeexits non-zero so a future flag rename doesn't nuke another run unobserved.bench/extract-eval-cases.pyno longer leaks sibling-directory paths into the corpus.normalize_pathdid a rawstartswith(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 withPath.resolve()+is_relative_toso the comparison is on path segments, not character offsets.bench/generate-llm-corpus.pysurvives a hungcodex execcall. A single 180ssubprocess.TimeoutExpiredused 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 asrc!=0, so a flaky case costs one wasted invocation instead of the whole run. Missingcodexbinary surfaces an explicit error instead of an opaqueFileNotFoundError.- Reranker score extraction respects the output tensor's shape.
score_batchwas reading the firstbatch_sizeentries of the flat logits tensor, which is correct only whennum_labels == 1(thems-marco-MiniLM-L6-v2default). For any reranker withnum_labels > 1(binary-classifier or NLI-style cross-encoders configured via[embedding].rerankerin.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-timetracing::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_commentswalked the source byte-by-byte and emittedbyte as char, mangling every multibyte sequence into its Latin-1 codepoint (soadd_executable(app src/café.c)produced a feature whoseentry_pathcould 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_routesran its route regexes directly againstroutes/{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 throughstrip_php_comments_preserving_stringsfirst, a new helper that blanks///#//* */comments while keeping string literals (route paths are string literals) and byte offsets intact so the cross-passconsumed_spansdedupe still works. impact_analysison.javatargets resolves as a file.looks_like_file_targetwas missed when Java was added to the supported-language set, so a bare target likeUserService.javagot classified as a symbol, hit symbol lookup, found nothing, and returned an empty result silently..javais now in the file-extension allow-list.FileCategory::classifyno longer flagsLatest.java/Manifests.java/Latest.phpas tests. The Java and PHPUnit arms lowercased the path and then matchedtest.java,tests.java, andtest.phpwithout a separator, so any source file whose name happens to end in those substrings got dropped fromimpact_analysiswithsource_only=true. Tightened to require the standard uppercase-Tconvention (*Test.java,*Tests.java,*Test.php) on the original path.codesage daemonstartup test no longer races. The integration test's wait loop incrates/cli/tests/mcp_daemon.rsexited as soon as the.sockfile appeared, but the daemon writes the.pidfile a few statements later. Aread_dirscan landing in that window leftpid_file = Noneandexpect(...)panicked spuriously on busy CI runners. The loop now waits for both files.codesage mcpstdio shim no longer preloads the CUDA/cuDNN stack.main()unconditionally raninit_for_main()before clap parsed argv, so every invocation (including the per-agent stdio shim)dlopen'dlibonnxruntime.soplus the full NVIDIA dylib set (libcublasLt,libcudnn_*,libcufft,libnvrtc,libcurand, etc.) before falling through to atokio::io::copyloop 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_varis 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 skipsinit_for_main()when argv resolves tomcpwithout--direct; all other commands (includingcodesage 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
- Subagent-driven review workflow in the
codesage-toolsplugin. Four new slash commands plus one subagent definition:/codesage-review(orchestrator — fans out feature-slice reviews in parallel via the newcodesage-feature-reviewersubagent, persists findings to.codesage/findings/<feature_id>.jsonwith run records under.codesage/reviews/),/codesage-triage(pure local state edit — mark a findingopen/false-positive/wont-fix/fixedwith an audit-trail history),/codesage-revalidate(re-runs the subagent against the slice to confirm a finding is fixed or still applies; auto-flipsopen→fixedwhen 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
-
assess_risk(andassess_risk_batch/assess_risk_diffby extension) now returns an optionaltop_symbolsfield on eachRiskAssessment: up to five symbols inside the file ranked byln(1 + line_count) + ref_count + (in_cycle ? 1.0 : 0.0), each carrying a one-linewhy("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. CLIcodesage risk <file>renders aTop 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 incrates/cli/src/mcp.rspassoutput_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) deriveschemars::JsonSchema. Schema derives respect existing#[serde(rename_all = "lowercase")]attributes automatically. Three explicit wrapper structs (FindSymbolResults,FindReferencesResults,SearchResults,ImpactAnalysisResults) describe the{"results": [...]}envelope thatrender_with_kindproduces for the four tools whose graph functions return bare arrays — agents see the wrapped shape, so the schema must too. A newevery_tool_advertises_an_output_schematest 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 coverIlluminate\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 bareenv()/config()helpers. Measured on a 1670-file Laravel app: totalfile_trust_boundariesrows grew 977 → 1296 (+33%); the previously-zerosecrets,process-exec, andauthcategories 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_bundleMCP tool + matchingcodesage feature-bundle <id>CLI. Returns the sameContextBundleshape asexport_contextbut 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 —mainresolves to the Rust crate'smain, not a Python module's__main__). Supports--include-callers/--include-calleesto expand the entry symbol's callers/callees intorelated[]. Use afterlist_features/find_featureto 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 withnot foundmarker intarget_descriptionwhen the feature_id doesn't exist; emptyprimary/relatedwith 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-srcext/<name>/config.{m4,w32}extension slices, Laravelroutes/{web,api,console,channels}.phproute registrations, autotoolsbin_PROGRAMS/lib_LTLIBRARIESand CMakeadd_executable/add_librarydeclarations, tree-sittermain()detection in C/C++ source,pyproject.toml [project.scripts]+setup.py entry_points+ Pythonif __name__ == "__main__":modules,package.jsonbin/scripts + Next.js app-router and pages-router files, and Gocmd/<name>/main.go+ rootmain.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 migration0009_feature_tablesadds three new tables (features,feature_files,feature_trust_boundaries);codesage indexruns 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_featuresandfind_feature, both withoutputSchema. -
Trust-boundary signal feeds
assess_risk. Newcrates/featurescrate 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 duringcodesage index— no extra parser pass, no DB round-trip; in-memory refs feed straight into the newfile_trust_boundariestable.RiskAssessmentgains atrust_boundaries: Vec<TrustBoundary>field and contributes a0.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;--jsonfor machine-readable output). Schema migration0008_file_trust_boundariesadds the table to existing indexes idempotently; the nextcodesage indexafter upgrade backfills boundaries file-by-file. -
Laravel application-layer mapping (
laravel-controller,laravel-request,laravel-artisan-commandseed sources). Ports the controller / form-request / Artisan-command coverage from clawpatch PR #5. The PHP mapper now walksapp/Http/Controllers/**/*.php,app/Http/Requests/**/*.php, andapp/Console/Commands/**/*.phpand emits one feature per class. Controllers are bridged back to their routes via a per-route table parsed fromroutes/{web,api,console,channels}.php— the regex capturesRoute::<verb>('/path', [Controller::class, 'action'])shape and matches by FQCN (preferringnamespace X; class Yresolved fully) or by short name when the route used ause-imported class. Artisan commands extract their$signaturefield soentry_commandresolves 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-routeseed source). Ports the<Route path element>detection from clawpatch PR #4. Scanssrc/**/*.{ts,tsx,js,jsx}andapp/**/*.{ts,tsx,js,jsx}for<Route path="..." element={<Component/>}>declarations, emits one feature per route keyed by path. Only runs when the rootpackage.jsondeclares areactdependency 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-rootumbrella +php-ext-moduleper-module). The php-src extension mapper handled theext/<name>/config.m4layout but not the Composer/PIE convention where a single extension lives at the repo root (fastchart,php_clickhouse,mdparser, etc.). Detection requiresconfig.m4/config.w32AND aphp_<name>.hheader 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|.ccas owned files. Per-module seeds fire for each<name>_<part>.{c,cpp,cc}at the root with the sibling<name>_<part>.hattached 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.care 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 singleclickhouse.cpp). -
Python project-level seed, pytest test-suite seeds, and test-command inference in the Python mapper. Previously the mapper only emitted
cli-commandseeds for[project.scripts]/setup.pyentry_points/if __name__ == "__main__":files — a project with no script entry produced no features at all. Three new surfaces: (1)python-projectlibrary seed off any ofpyproject.toml/setup.py/setup.cfg/requirements.txt, sofind_feature("src/foo.py")resolves to the project itself; (2)pyproject-poetry-scriptcli-command seeds for[tool.poetry.scripts](separate from[project.scripts]); (3)python-test-suiteseeds — one per top-level suite root (a project withtests/api/,tests/unit/, andtests/integration/produces ONEtestsseed, not three), capped at 200 files, filtered against__fixtures__/fixtures/testdatadirs 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 → barepytest, then plumbed onto each test entry'scommandfield — agents get a runnable string (uv run pytest,poetry run pytest, etc.) instead of guessing. The inferred command also lands on thepython-projectseed's summary so it's visible inlist_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'spython.tsminus 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-rootmain.gosurface. The mapper now walks the module's.gofiles filesystem-only (nogo listtoolchain dependency, keeping the mapper hermetic), buckets files by directory, parses each directory'spackage <name>declaration, and emits one feature per package:cli-command(go-cmdsource) forcmd/<name>/mains,go-root-packagefor repo-root mains,go-internal-packagefor anything underinternal/, andgo-packagelibrary otherwise. Per-package*_test.gofiles attach to the feature with a scopedgo test ./<dir>/...command (not the unscopedgo 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 forCode generated ... DO NOT EDIT) get segregated fromowned_filesintocontext_fileswith reason"generated go file"so agents skip editing them. Same-repo imports (parsed from both single-line and block-formimportstatements) 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 nestedgo.modare 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'sgo.tsminus 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 tofind_featureandfeature_bundle. The mapper now readspackage.jsonworkspaces(array form and{packages: [...]}object form), parsespnpm-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 anode-packagelibrary seed (sofind_feature("packages/api/src/auth.ts")resolves to the package manifest + per-packagetestCommand), plus per-packagepackage-json-binseeds. 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 keeplist_featuresfrom filling with N copies of the same script seed. Bin source-back resolution mapsdist/foo.js→src/foo.tswhen 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'snode.tsworkspace branch; source-group partitioning is intentionally not ported (browse-only, diluteslist_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'sQueue/Connectors/*Connector.phpreturning 10 distinct sibling files in the top-10 while pushing the conceptual targetQueueManager.php(a different dir) off the page. Newapply_directory_saturationruns afterapply_file_saturation: per-parent-dir count, threshold 2, decay0.75^excessper excess chunk. The two signals stack — a chunk from an oversaturated dir whose file is itself oversaturated gets hit twice. Default-on; opt-out viaCODESAGE_DIR_SATURATION=0. Three regression tests including a reproduction of the laravel-framework failure mode. -
FeatureRecord.test_commandfield. New optional free-form shell command on the public protocol surface (pnpm --dir packages/api test,go test ./pkg/util/...,uv run pytest). Distinct fromentry_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 inentry_commanddestabilized feature IDs across sessions. Schema migration0011_features_test_commandadds a nullabletest_commandcolumn to thefeaturestable; idempotent on existing indexes. The mapper orchestrator now copiesFeatureSeed.test_commandstraight through. CLIfeature-showrenders it on its own line. New regression testfeature_id_stable_when_test_command_changesasserts the invariant by swapping a project's lockfile (npm → pnpm) and verifying thenode-packagefeature_id is unchanged whiletest_commandflips correctly.
-
Rebalanced
assess_riskweights 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. Theassess_risk_diffsummary-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 realtrust_boundariesvalues on the nextcodesage indexpass; 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_bundleMCP tools, and the matchingmap/features-list/feature-show/feature-for/feature-bundle/trust-boundariesCLI commands. The capability table gains three rows for feature-slice mapping, curated feature bundles, and trust-boundary derivation; new recipes show thecodesage map→features-list→feature-for→feature-bundleflow. -
Tightened MCP tool descriptions on
list_dependencies,impact_analysis, andexport_contextso an agent reading the description-only listing can disambiguate adjacent tools without falling back to trial-and-error:list_dependenciesis now explicit about single-hop scope and points atimpact_analysis(multi-hop) andfind_references(per-symbol) as the right neighbors;impact_analysismirrors the bridge in reverse;export_contextnotes thatfeature_bundleis 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" +1767a259ReplaceSymbolBodyTooldocstring fix). -
impact_analysisdescription 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 atlist_dependenciesfor 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_WEIGHTwas a fixed0.5blend 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 weight0.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") use0.6(trust the cross-encoder more — semantic-only retrieval over-clusters on sibling files; cross-encoder untangles them). Mixed/fallback queries keep0.5. Default-on; opt-out viaCODESAGE_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.phpflooding top-10 instead of surfacingQueueManager.php). -
Query-aware test-file demote. Previously test-like paths got a fixed
0.3xpath_penaltyregardless of query intent. Two problems with that: production-intent queries like "request and response interceptors" still got 9 test files flooding top-10 because0.3 * strong_similaritybeat1.0 * weaker_similarityfor 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 harder0.15xdemote (= 0.3 × 0.5). Default-on; opt-out viaCODESAGE_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.phponly 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) andCODESAGE_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.
-
Feature mapper bypassed
.gitignore.crates/features/src/mappers/shared.rs::walk_fileswas a hand-rolled walker that ignored gitignore entirely and only matched a small hardcoded directory-name list.codesage mapagainst this repo was indexing.worktrees/<branch>/...as separate features even thoughgit check-ignorereports the directory as ignored. Rewrote to delegate toignore::WalkBuilder(same gitignore-aware walker the structural indexer uses) plus added.worktrees/worktreesto the belt-and-suspenders hard-exclude list. Two regression tests cover the .gitignore-honored walk and the explicitshould_skippredicate. -
feature_bundlereturned imports instead of the entrypoint body. The bundle picked the file's first chunk for entry files; on a Rust binary with 400+ lines ofusestatements beforefn main(), an agent reviewing the feature got the import preamble, not the function body.feature_bundlenow looks up the entry symbol'sline_startin 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 == 0row-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 migration0010_files_boundaries_derived_atadds aboundaries_derived_atcolumn tofiles,replace_file_trust_boundariesstamps it on every write (including when the derived set is empty, so a rule-clean file is distinguishable from a never-derived one), andcmd_indexqueriesfiles_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 attest_*.pypaths absent from thefilestable. First pass shipped a PythonFileCategory::Testskip; the broader fix now plumbs[index].exclude_patternsthrough a newMapperContextcarried into everyFeatureMapper::mapcall. The orchestrator compiles the patterns into aGlobSetonce 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_erroredflag, the orchestrator skips the stale-feature GC pass (trading small amounts of stale debt for never nuking valid rows), andcodesage indexpropagates the error instead of silently succeeding witheprintln!. -
C/C++ mapper double-emitted features for build targets that also define
main(). A binary declared inCMakeLists.txt(orMakefile.am) AND containingint main()produced two features — onecmake-bin/autotools-bin, onec-main— because thealready_seeded_pathsset was constructed empty and the dedup key includedsource. 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.m4orconfig.w32but always recordedentry_path = .../config.m4. Windows-only extensions ended up with a feature whose entry file didn't exist. Pick whichever config actually exists (preferconfig.m4when 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_patternsplumbing applied at the orchestrator level but the per-mapperseeds.retainfilter was missing incrates/features/src/mappers/rust.rsandcrates/features/src/mappers/c.rs:src/bin/<name>.rsmatching an exclude still produced acargo-binfeature, and a CMakeadd_executable(... excluded.c)still emitted a feature whose entry_path was filtered out of the structural index. Added aMapperContext::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) byctx.allowedas a safety net so noFeatureFileRefreferences 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_pathpointed at the manifest, not the resolved module.[project.scripts] acme = "acme.cli:main"produced a feature whose only file waspyproject.toml;codesage feature-for acme/cli.pyreturned nothing. Addedresolve_script_module_pathwhich probes<module>.py,<module>/__init__.py,src/<module>.py, andsrc/<module>/__init__.pyfor the dotted module target. When one of those exists (and isn't excluded), it becomesentry_path; the manifest falls back asentry_pathonly 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 tosetup.pyentry_points. -
Workspace and project routing was a no-op.
node-packageandpython-projectseeds only persisted the manifest infeature_files, whilestorage::features_for_filedoes an exactfeature_files.path = ?1lookup. Sofeature-for packages/api/src/auth.tsandfeature-for src/acme/foo.pyreturned 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 asowned_fileswith reason"workspace source"or"project source". Capped at 2,000 files per seed to keep thefeature_filestable bounded on monolithic repos. Two regression tests cover the routing contract directly. -
Go's per-package file classifier ignored project excludes.
collect_package_filesused rawfs::read_dirand skipped thectx.excludescheck thatdiscover_packageshad applied. If an excluded file sorted alphabetically before an included sibling, it becameentry_path; the post-mapseeds.retain(|s| !ctx.excluded(&s.entry_path))then dropped the entire package, losing every other file in the dir. ThreadedMapperContextintocollect_package_files(andcollect_import_context) and filter each candidate viactx.allowed. -
JS fallback workspace prefixes were always added.
packages/,apps/,extensions/,plugins/got picked up even when the rootpackage.jsondeclared explicitworkspaces. A repo withworkspaces: ["apps/*"]plus an incidentalpackages/stray/package.jsonproduced 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_filecalledfs::read_to_stringthen truncated to 2 KB — a multi-MBgo-bindataoutput without a recognized suffix allocated the full file just to inspect 2 KB. Replaced withFile::open+take(2_000)+ UTF-8valid_up_totruncation. Earlier draft usedString::truncate(2_000)which panics on a UTF-8 char boundary (BOM or multibyte comment near the cap); that's fixed by the samevalid_up_towalk-back. -
Python pytest cap didn't actually cap.
pytest_test_suitesdocumented a 200-file cap but the innerbreakonly exited the per-scan-dir loop — a project with 200 tests intests/plus more undersrc/exceeded the cap. Added a labeledbreak 'outerso the cap is global. -
Manifest files were marked as test files.
python-projectandnode-packageworkspace seeds populatedtests: vec![SeedTest { path: manifest, command: ... }]to surface the inferred test command. The orchestrator inserts everyseed.tests[]row asrole: Testin the feature-file table, sopyproject.toml/package.jsonshowed up as test files infeature_bundleoutput. The test command now surfaces viaentry_commandonly;tests[]is empty for nearby-test discovery to populate. -
JS workspace discovery ignored
.gitignore. Thepackages/*enumeration step used rawfs::read_dir. A gitignoredpackages/internal-only/would emit anode-packageseed pointing at files the structural indexer had skipped. Switched toignore::WalkBuilderwithmax_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 plainpytest. The section probe is gone; lockfile (uv.lock,poetry.lock,pdm.lock) is the only signal. Hatch falls back to barepytest. -
Go same-repo import context cap is now env-overridable.
IMPORT_CONTEXT_CAPwas hardcoded24. Now configurable viaCODESAGE_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.js→src/foo.tsalready worked; older Babel / TS-library packages publish vialib/foo.js. The same source-back resolution now applies there too. -
Internal cleanups in the mapper port. Factored
pyproject_scripts+pyproject_poetry_scriptsinto a sharedpyproject_scripts_sectionhelper (both sharename = "module:fn"syntax; only header / source-tag / title differ). Dropped an unusedroots_seenBTreeSet inpython.rs. Movedpackage_display_name's unreachable root-fallback intodebug_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 searchruns). 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 refactoringmain()to explicitlystd::process::exit(code)after stdio flush, skipping Drop glue. Safe for codesage's CLI commands: all writes commit explicitly viaexecute_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 incrates/cli/src/main.rs::mainso the next dev sees the rationale. -
codesage-onboardplugin script gains--refresh-hintand writes a 0.7.0-aware.claude/CLAUDE.mdtemplate. The hint now teacheslist_features/find_feature/feature_bundle/trust-boundariesby the question shape they answer alongside the prior tools, mentions feature mapping + trust boundaries in the operations section, and lists the newmap/features-list/feature-show/feature-for/feature-bundle/trust-boundariesCLI 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.shautomates the post-upgrade flow across every onboarded repo on the machine: re-runscodesage index(applies migrations 0008/0009/0010, populates feature tables, backfillsfile_trust_boundaries), re-installs git hooks (idempotent; protects againstcurrent_exepath moves), and re-runscodesage-onboard --refresh-hintto update the per-repo.claude/CLAUDE.md. Skips~/ai/bench-repos/*and~/ai/codesage_ref/*per the bench-repos study-only rule.--dry-runto preview,--no-indexto refresh hints only.
0.6.0 - 2026-05-09
- Raised
MAX_SEQ_LENGTHfrom 256 to 512 tokens andDEFAULT_CHUNK_SIZEfrom 1000 to 1500 chars (DEFAULT_MIN_CHUNK_SIZE250 → 350,DEFAULT_CHUNK_OVERLAP150 → 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 atbench/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_excludesnow usesWalkParallelwith 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),.hfiles still get C/C++ project-aware classification, and the result is sorted by path so callers that depend on stable ordering keep working.
Symbol.rationale[]field onfind_symbolandexport_contextMCP responses, populated for Rust definitions whose attached doc-comments start withWHY:,NOTE:,IMPORTANT:,FIXME:,HACK:,XXX:, orTODO:. 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 emptyrationale[]until per-language extractors land. Schema migration0007_symbols_rationaleadds arationalecolumn to thesymbolstable; existing indexes pick up real values on the next file-touch + reindex (no global re-index required).[embedding].poolingconfig field ("mean"or"cls") overrides the model-name heuristic that previously decided pooling strategy from a substring match onbge-. The heuristic is silent and wrong for any non-bge-CLS model (intfloat/e5-*, etc.) and anybge-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.
- Tightened protocol types:
SearchResult.languageis now aLanguageenum (was a free-formString) andSymbolSummary.kindis aSymbolKindenum (wasString). JSON serialization is unchanged (both serialize as the same lowercase strings), but Rust consumers that buildSearchResult/SymbolSummaryliterals ormatchon 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/Eqderives toSymbol,Reference, andFileInfo, andHashtoFileCategory. Lets consumers dedup, assert against, and key hash maps on these types — previously blocked by missing trait derives. codesage git-indexnow passes--since=@<2-years-ago>togit 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 accumulatecount >= 3within the window. ConstantHISTORY_WINDOW_DAYS = 730incrates/graph/src/git_history/indexer.rs.codesage install-hooksnow wraps the background indexer invocations innice -n 19and (Linux-only, gated oncommand -v ionice)ionice -c 3so a hook-triggered indexer can't soak foreground CPU or block disk I/O. Existing installs need to re-runcodesage install-hooksto pick up the niced template; new installs get it automatically.
- 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'sPoint.columnis 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 exposets_node_*_point_utf8, so the conversion (walk back to line start, count UTF-8 codepoints in the prefix) is implemented incrates/parser/src/position.rsand used by bothextract.rs(symbols) andreferences.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-packagesprobe used for ONNX Runtime / NVIDIA library discovery with a 10 s timeout. Without it, a wedged Python interpreter insideOnceLock::get_or_initwould block every thread waiting onEmbedder::new/Reranker::newindefinitely. 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 underlyingstd::env::set_varcalls (LD_LIBRARY_PATH,ORT_DYLIB_PATH) areunsafeunder Rust 2024 because concurrentgetenvfrom another thread is UB; pinning them to single-threaded startup eliminates the race. The originalOnce::call_once-guarded calls insideEmbedder::new/Reranker::newremain 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_excludesnow skips files larger than 10MB (MAX_INDEXABLE_FILE_BYTES) at metadata time, beforefs::read, so a stray large generated SQL dump or vendored data file slipping pastHARD_EXCLUDE_PATTERNSno longer fills the indexer's heap. Skipped files emit atracing::warn. (2)Embedder::detect_dimnow 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 turnsearch_knninto noise. (3)truncate_arrayin the MCP layer now shrinks an oversized first item'scontentfield 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_batchnow 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_sidecarnow wraps its DELETE + INSERT…SELECT in BEGIN / COMMIT inside the sameexecute_batchcall, so a crash between the two statements no longer leaves the FTS sidecar empty while the chunk table has data. (3)insert_chunksnow wraps each call in aSAVEPOINTso the vec0 + FTS5 inserts stay atomic even when the function is called outside an outerexecute_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 indexlosing 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_filesthen reselected those files on the next run, so the same backlog was re-embedded indefinitely. Observed on~/php-srcwith 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 statusandcodesage doctorto 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 exportcallee handling soinclude_calleesreturns referenced callee/dependency code instead of being ignored for symbol targets or duplicating the definition chunk for query targets. - Fixed
codesage export --symbol --limitto cap returned symbol definitions and primary chunks, preventing common names from exceeding the requested context budget. - Fixed
scripts/leak-check.shinvalid-regex diagnostics so the underlyinggreperror is shown instead of an empty detail block. - Fixed
impact_analysistarget handling and precision: dotted symbols such asRepository.findnow stay symbol targets by default,codesage impact --symbolcan 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..Bto scan file content at the range endpoint instead of the currentHEAD. - Fixed
scripts/leak-check.shto fail closed on invalid forbidden-pattern regexes, and added CI coverage for shell linting plus maintenance-script regressions. - Fixed
scripts/release.shto refresh the[Unreleased]changelog compare link when cutting a release. - Fixed structural lookup and context export edge cases: Rust/C++
::qualified symbols now resolve throughfind_symbol, dotted call references such as Gofmt.Printlncan be found by their bare tail, Python attribute calls are indexed as call references, andexport_contextbinds symbol summaries back to exact qualified definitions instead of the first matching short name. - Fixed git-history indexing to honor project
exclude_patternsfrom.codesage/config.tomland to decay existing churn/co-change weights even when an incremental run finds HEAD unchanged. - Fixed
codesage initTOML generation for project directory names containing quotes, backslashes, or control characters.
0.5.0 - 2026-05-02
assess_risk_batchMCP tool (and matchingcodesage risk-batch <files...>CLI subcommand). Takes a list of repo-relative file paths and returns oneRiskAssessmentper 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 individualassess_riskcalls per 30-day window vs 13assess_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 usingassess_risk_diff— the two answer different questions. CLI accepts stdin too:git diff --name-only | codesage risk-batch. Response includes a top-level_legendshort-code map (same shape as the new field onRiskDiffAssessmentbelow). Three integration tests cover input ordering, empty input, and the legend behaviour at threshold.session_startandsession_endMCP tools (and matchingcodesage session-start/codesage session-endCLI subcommands).session_startsnapshots 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_endreloads that snapshot, recomputes current state, and returns aSessionDiff: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-effortgit_head_before/git_head_after, andsummary_notesready 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-runningsession_startoverwrites the prior snapshot for that id (resets the baseline mid-session). Pattern borrowed from sentrux'ssession_start/session_endtools, 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.
assess_risk_diffandassess_risk_batchnow emit a top-level_legendmap 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, orcodesage git-indexhasn'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_legendentry 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_legendsee 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=5000is now set on every database connection ininit_db. A second MCP session reading mid-index waits up to 5 s for the lock instead of failing instantly withSQLITE_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=WALandforeign_keys=ONwere already set. - The MCP server no longer fails every tool call when a project's
.codesage/config.tomlis malformed.load_embedding_confignow logs a warning and falls back toEmbeddingConfig::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.searchwill 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.pygenerates LLM-as-judge eval cases by sampling source files in a project, askingcodex execto 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'sbenchmarks/data.py.bench/codesage-bench-runnernow reportsmean tokens to first relevant hitper Semble's headline metric. Counts cl100k_base tokens (viatiktoken) 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 atokens→hitcolumn; the<!-- METRICS: ... -->line gainsmean_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.searchnow 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 —ModuleRefmatchesmodule_ref.py,module-ref.ts, orModuleRef.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; downstreamapply_definition_boostlifts it by3 * 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 withCODESAGE_STEM_SCAN=0. Validated on the nest index:ModuleRefquery went from "definition not in top-5" to rank 1 (score 1.61, def chunk atmodule-ref.ts:20);InstanceWrapperunchanged 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 viaCODESAGE_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 configurationmoved its expected file from rank 2 → 1 (the only bench query containing a qualifying embedded symbol). Pattern from Semble'sboosting.py _boost_embedded_symbols.searchnow boosts candidate chunks that contain a language-keyword definition of a queried bare symbol (fn foo,class FooBar,defmodule Phoenix.Router, etc.). Triggered only whenis_symbol_queryaccepts 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:ApplicationConfigdefinition chunk surfaced from off-top-5 to rank 1 (0.76 → 1.45);MicroservicesModulesimilarly (0.65 → 2.57). Boost is additive3 * max_score, with a 1.5× multiplier when the file stem matches the symbol name (snake_case-normalized:login_controller.rsmatchesLoginController).typedefis intentionally not in the keyword list (itskeyword TYPE NAMEshape can't be expressed in the namespace-prefix regex;find_symbolcovers it). 17 keywords from Rust/Python/JS/TS/Go/PHP/Elixir/Kotlin/Scala/Swift/C#/Java. Default-on; disable withCODESAGE_DEFINITION_BOOST=0. Pattern from Semble'sboosting.py _boost_symbol_definitions. 13 unit tests cover the symbol-vs-NL gate, namespace stripping, definition-vs-reference matching, and the file-stem bonus.searchnow 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'sranking/penalties.py(THRESHOLD=1, DECAY=0.5). Default-on; disable withCODESAGE_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.searchnow applies multiplicative path-based penalties to results before reranker blending. Test/bench files (TEST_LIKE_EXCLUDE_PATTERNS),compat/_compat/legacy/_legacydirectory segments, andexamples/_examplesdirectory 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 incompat/gets 0.09×). Singularexampleis intentionally NOT a trigger so thecom.example.*Java/Kotlin namespace is unaffected. Default-on; disable withCODESAGE_PATH_PENALTY=0if 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'sranking/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_PATTERNSdropped them at file discovery, sofind_referencescould not see callsites in tests,find_symbolcould not find test fixtures, andsearchhad 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 fromDEFAULT_EXCLUDE_PATTERNSand 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%) andfind_referencesaccuracy improves substantially (nest reference count +217%, 12,555 → 39,804). The git-historycompile_excludesfilter that subtractedTEST_LIKEfromDEFAULTis now redundant and removed. Thecodesage initconfig template comment updated accordingly. assess_riskscore now includes import-cycle membership as a sixth signal. New fields onRiskAssessment: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) / 4clamped 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. Whenin_cycleis 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 poweringassess_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.
codesage initnow writesmodel = "jinaai/jina-embeddings-v2-base-code"anddevice = "gpu"as the default. Validated 2026-05-02 against four codebases (one PHP, one C, one Rust, one TypeScript) using LLM-judged corpora generated bybench/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 conservativedevice = "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::newnow applies the same silent-CUDA-fallback hard-fail check shipped forEmbedder::newlast 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/mapsprobe, sameCODESAGE_ALLOW_CPU_FALLBACK=1bypass. The shared helpermodel::require_cuda_libs_mappedis nowpub(crate)so both call sites use one implementation.Embedder::newnow hard-fails with an actionable error whendevice = "gpu"was requested but ORT silently fell back to CPU. Detected by inspecting/proc/self/mapsafter session commit (Linux only): iflibcuda.soandlibcudart.soaren'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 aSuccessfully registered CUDAExecutionProviderlog line —/proc/self/mapshad zero CUDA entries. Bypass for tests / mixed-mode setups: setCODESAGE_ALLOW_CPU_FALLBACK=1. Missing-only-cuDNN-or-cuBLAS produces aWARN(per-op fallback is normal for some attention shapes); both libcuda and libcudart missing is a hard error.codesage cleanupnow treats only actual vec0 chunk tables as vector tables, so the active{chunk_table}_ftssidecar 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-basedexport_contextstill uses existing semantic chunks when the configured model table is already present, without constructing the embedder. codesage export --symbolnow 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 clearcodesage index --fullrebuild instruction instead of silently dropping semantic context.codesage indexnow fails loudly when semantic model setup fails, unless--no-semanticwas 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-semanticstructural 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
offsetis 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 doctornow verifies thepost-rewritehook in addition topost-commit,post-merge, andpost-checkout, matching the hook set installed bycodesage install-hooks.
0.4.5 - 2026-04-25
- C++ as a supported language. New
Language::Cppvariant,tree-sitter-cppdependency,cpp.scmsymbol query, andcpp_refs.scmreference query. Symbol coverage: free functions, in-class methods (declared and defined), out-of-line method definitions (void Foo::bar() {}resolved toFoo::bar), constructors / destructors, operator overloads (including reference-return forms likeFoo& operator=), classes / structs / unions, plain enums +enum class, typedefs,usingaliases, C++20 concepts, and#definemacros. Template-wrapped definitions are captured automatically (the inner definition node still matches its pattern regardless of thetemplate_declarationwrapper). 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), andusingdeclarations. Qualified names walk enclosing namespaces (including the C++17namespace A::Bform). Honest caveat: tree-sitter does not expand macros, so libraries that hideclass/namespacebehind project-specific macros (e.g. nlohmann/json'sNLOHMANN_BASIC_JSON_TPL_DECLARATIONandNLOHMANN_JSON_NAMESPACE_BEGIN) lose those symbols. The macro turns the surrounding declaration into anERRORnode and the structural query finds nothing. File source recognition:.cpp,.cc,.cxx,.c++,.cppm,.ixx,.hpp,.hh,.hxx,.h++,.tpp,.ippare always C++..hdefaults to C; the discovery layer auto-flips it to C++ for any project that also contains an unambiguous C++ extension (no config knob)..calways stays C, even in a C++ project. assess_risk_diffnow reports import cycles that involve patch files. Newcycles_touching_patch[]field onRiskDiffAssessmentlists 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 carriesmembers(sorted repo-relative paths),size, andmax_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 insummary_notesfor PR-description inclusion. Edges are derived from the existingrefstable (kind = import / include / inheritance / trait_use resolving throughsymbols.nameorsymbols.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: SocratiCodecodebase_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
codesagewriters.index,git-index, andcleanupnow take an exclusiveflockon.codesage/indexing.lockat startup (stdlibFile::try_lock, no new dep). If another writer already holds it, the second process exits 0 withanother codesage indexer is running on <path> — skippinginstead of colliding at the SQLite layer and emitting a scaryError: database is locked. Matters most forinstall-hooks-registered background hooks (post-commit,post-merge,post-checkout,post-rewrite), where rapid commits or agit pullduring 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-runbench/concurrency-audit.pyon any onboarded project to verify. - Gated hybrid BM25+semantic retrieval (recommendations doc §2.1 accepted). New FTS5 sidecar tables
{chunk_table}_ftsare created alongside each vec0 chunk table and populated in lockstep duringinsert_chunks. Onsearch, a gate function (query_has_rare_literal) decides whether the query contains a distinctive literal: backticked identifiers,::scope resolution,*.extglobs, dotted-identifier pairs likemoduleref.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 originalproject_hybrid_bm25_rrf.mdmemo 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 returnsCouplingReport { coupled, file_indexed, file_commits, note? }instead of a bareVec<CoChangeEntry>. Agents should readresponse.coupledfor the ranked list. Motivation: retrospective session analysis showed 59% offind_couplingcalls 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 newnotefield disambiguates each case with a concrete hint (runcodesage git-index, verify path shape, or accept that this file changes in isolation).file_indexedandfile_commitsare always present, so even non-empty results carry enough context for the agent to judge a thin response.assess_risk_diffnow clusters per-file detail when a patch touches ≥5 files from one directory. The crowded directory's entries move fromfiles[]into a new top-levelclustered_directories[]field (top-3 files by score preserved in full detail, the rest listed by name asomitted_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, andsearchthat 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_statetable (migration0002) records the HEAD SHA each successfulcodesage indexrun built against.codesage statusandcodesage doctornow show anindex-driftindicator (fresh,N commits behind HEAD,not an ancestor of HEADfor rebases, ornever indexed). The MCP server appends one JSON line to<project>/.codesage/drift.logthe 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).
bench/codesage-bench-runnerscorecard 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-parseableMETRICSHTML comment gains the new fields for downstream tooling. Optional--baseline "ripgrep 14.1.0 (grep -rn)"flag (orCODESAGE_BENCH_BASELINEenv) fills the baseline slot. Informational only, does not run a comparison tool. Template shape follows landscape-sweep item 1.6 innotes/20260424-reference-tool-recommendations.md.
- MCP tool params now accept integer fields encoded as JSON strings (
"limit": "5"alongside"limit": 5). StrictOption<usize>deserialization was failing withinvalid type: string "5", expected usizeon ~10% offind_couplingcalls; 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 tolimit,offset, anddepthacrossCouplingParams,ImpactParams,ExportContextParams, andSearchParams. Genuinely non-numeric strings still error, and the error now quotes the offending value for diagnosability instead of a generic type-mismatch message. codesage statusno longer errors withno such table:on projects opened without a selected embedding model. Root cause:cmd_statusopened the DB viaDatabase::open()(which leaveschunk_tableempty) and then calledchunk_count(), which interpolated the empty table name into its SQL. Status now calls a newtotal_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.
- Bumped transitive
opensslcrate 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 pullsopenssltransitively vianative-tls(used byhf-hubfor model downloads andort-sysat 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-webpkito 0.103.13 (from 0.103.12) for RUSTSEC-2026-0104 (reachable panic in CRL parsing). Pulled viarustls→ureq/hyper-rustls/tokio-rustlson the same model-download and reqwest paths. Clears the scheduledcargo auditworkflow failure.
0.4.0 - 2026-04-16
- Go language support. Parses functions, methods (with pointer and value receivers), structs, interfaces, type aliases, and constants. Qualified names use
ReceiverType.MethodNameconvention. References track imports and function/method calls. Test discovery already recognized_test.goconvention from v0.3.0; now the parser can index Go source files.
- 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
searchandexport_contextno 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::newnow bails when GPU is requested but the binary was built without thecudafeature, matchingEmbedder::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 instorage::Databasenow propagaterusqlite::Error; unknownSymbolKind/ReferenceKindvalues surface as typed conversion errors instead of being relabelled asFunction/Call. - Malformed
.codesage/config.tomlis now a hard error instead of a silent revert to defaults (MiniLM/CPU/empty excludes).load_project_configand MCPload_embedding_configreturnResultand surface parse errors with path context. parse_logdrops commits with unparseable timestamps and changes with unparseable add/delete counts instead of fabricating zeros that were indistinguishable from legitimatets=0or zero-line changes. Reports a one-line summary when any lines were dropped.is_ancestornow distinguishes spawn failure (propagated) from exit-1 / exit-128 (fall back to full rescan with a warning). A brokengitinstall is no longer silently hidden as "rebased history".install-hooksnow propagatescurrent_exe()failures instead of silently writing a hook that embeds the literalcodesagepath.assess_riskno longer swallows errors from internalimpact_analysisandtest_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_percentilenow distinguishes "no git row" (Ok(0.0)) from DB error (Err), so a broken git-index doesn't silently lower risk scores.
- 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_hashpattern with a single-queryall_file_hashespreload. On 25k-file repos this cuts thousands of SQL round-trips to one sequential scan. - Incremental git-history indexing preloads
git_co_changespair keys once (all_co_change_pairs) instead of runningSELECT COUNT(*)per delta pair inside the write transaction. UsesHashMap<String, HashSet<String>>so membership probes are alloc-free. - Search boost path now calls
symbol_exists(LIMIT 1 probe) instead offind_symbolswhen all it needs is non-emptiness for a token. annotate_with_symbolsbatches its per-file symbol lookup viasymbols_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 50kQuery::newinvocations and theircapture_index_for_namescans. Database::insert_chunkstakes&[(&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_bytesuses a single memcpy on little-endian targets instead of per-elementto_le_bytes. The function is hit once per chunk write and once per search query.- Reranker
score_batchstops building an intermediateVec<(String, String)>for tokenizer pairs; it now borrows query + doc refs directly. Database::remove_filenow cascades the deletion togit_filesandgit_co_changes. Deleted files no longer linger infind_coupling/assess_riskoutput after an index refresh.- Schema migrations now live in a registry (
MIGRATIONS+schema_migrationstable) rather than as hand-wired function calls insideinit_db. Adding migration0002,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 intogit_history/{mod.rs, indexer.rs, risk.rs, tests_rec.rs}. Public API unchanged.crates/storage/src/db.rs(1230 lines) split intodb/{mod.rs, structural.rs, semantic.rs, git_hist.rs}viaimpl Databaseblocks per concern. Public API unchanged.- MCP tool responses now use the structured
CallToolResultshape (isError: trueon failure per MCP spec,structured_contenton success alongside the pretty-printed text). MCP clients can programmatically distinguish errors from successful results instead of regexing "Error: " strings. ImpactTarget::from_hintandExportRequest::from_targetin the protocol crate centralize the CLI↔MCP classification/construction helpers. Previously duplicated (with slight shape differences) betweencli::mainandcli::mcp.- CLI + library crates now log via the
tracingcrate instead of raweprintln!.tracing_subscriberinitializes at binary startup writing to stderr (keeps stdout clean for the MCP transport and CLI JSON output). Log level followsRUST_LOG, defaults toinfo. - Hand-rolled
DistRownewtype + BinaryHeap in the multi-language search merge path replaced by asort_byon the merged vec. No behavior change. - Duplicated
format_bytes/git_common_dirhelpers incli::mainandcli::doctorpulled intocli::util.format_bytesnow 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.
recommend_testsnow finds tests for Laravel mirror-tree layouts. Source atapp/<rest>/<file>.phpresolves to tests attests/{Unit,Feature,Integration,Browser}/<rest>/<file>Test.php. Previously only flattests/Unit/FooTest.phpwas checked, missing tests liketests/Integration/Actions/CredentialingApplication/ExportZipActionTest.php.recommend_testsnow finds tests for Symfony mirror-tree layouts. Source atsrc/<rest>/<file>.phpresolves to test attests/<rest>/<file>Test.php(no Unit/Feature subdir; tests/ mirrors src/ directly).
- 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.
codesage git-indexwas issuing one fsync per upserted row. On large repos like php-src (~25k files + ~10k pairs per worktree) this burned multi-minute disk waits injbd2_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.
codesage git-indexwas dropping test-like files from co-change pair generation entirely, which maderecommend_tests'scoupledsection 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 signalrecommend_testsneeds).recommend_testsfor.c/.hsource files now lists.phpttests in<dir>/tests/as primary (php-src convention). Skips when thetests/directory has more than 50 files (typical ofext/standard/tests/) — that's too noisy for "primary" and the agent should rely on coupling history instead.
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.
assess_risk_diff(project, file_paths[])MCP tool andcodesage risk-diffCLI 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-readysummary_notesfor 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 andcodesage tests-forCLI 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 undercrates/<name>/tests/*.rs) andcoupled(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-diffand similar pipelines. discover::TEST_LIKE_EXCLUDE_PATTERNS: subset of the default exclude list covering test and bench files. Exposed sogit_historycan split test files from hard-excluded files.
codesage git-indexnow keeps test and bench files ingit_filessorecommend_testsandassess_risktest-gap detection can find them. Test files remain dropped from co-change pair generation, so coupling rankings stay focused on production code. Re-runcodesage git-index --fullafter 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).
codesage install-hooksnow installs apre-commithook when the repo containsscripts/leak-check.sh. The hook greps staged content against extended-regex patterns fromscripts/leak-patterns.txt(tracked, shared) and.git/info/leak-patterns.txt(local-only, per-developer) and blocks the commit on a match. Bypass withgit commit --no-verifywhen a false positive is intentional.scripts/leak-patterns.txtwith 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.pubpublic-key variants),credentials.json,service-account*.json. scripts/leak-check.shsupports--range A..Band--allmodes so the same script powers both the pre-commit hook and CI.
.gitignoreexcludes benchmark artifacts underbench/(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.
- 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 theimpact_analysisMCP 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 exportand theexport_contextMCP tool. Optionally includes callers and callees. - Git history intelligence.
codesage git-indexpopulates 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-indextakes--fullor--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 doctorhealth checks: binary, config, database, disk space, hooks, CUDA, models, MCP registration. Supports--json.codesage-toolsClaude Code plugin. Slash commands:/codesage-onboard,/codesage-reset,/codesage-reindex,/codesage-bench,/codesage-eval. One global MCP server, routed per call by an absoluteprojectargument.- Husky-aware
codesage install-hooks. Detectscore.hooksPathand Husky markers, writes to.husky/<name>, and adds installed paths to.git/info/exclude. Installspost-commit,post-merge,post-checkout, andpost-rewrite. - Worktree-aware hooks. Each hook body uses
git rev-parse --show-toplevelat 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_patternsinconfig.tomlextend 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 Pythonsite.getsitepackages()and user site-packages, then standard system paths (/usr/lib/x86_64-linux-gnu/nvidia,/usr/local/lib/nvidia,/opt/nvidia).ORT_DYLIB_PATHoverrides the ONNX Runtime location.codesage doctorreports 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 viaversion.workspace = true, so releases bump in one line.