v0.7.0
Added
-
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.
Changed
-
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.
Fixed
-
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.