Skip to content

v0.7.0

Choose a tag to compare

@github-actions github-actions released this 16 May 20:13
v0.7.0
dca1cf4

Added

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Changed

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

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

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

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

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

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

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

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

Fixed

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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