Skip to content

v0.8.0

Choose a tag to compare

@github-actions github-actions released this 18 May 00:13
v0.8.0
470fea4

Added

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

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

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

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

Changed

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

Fixed

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