release v0.32.1 — fix the v0.32.0 review findings #302
No reviewers
Labels
No labels
code-review
correctness
dos
performance
security
severity/high
severity/low
severity/medium
tech-debt
Kind/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
h-dv/code-index!302
Loading…
Reference in a new issue
No description provided.
Delete branch "release/v0.32.1"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Fixes the precision and recall defects that two independent deep reviews found in v0.32.0 (
4e3034a), plus the findings of a final review of the combined branch.Lanes (records in
_prdoc/records/)LEXICAL_LOCALandPYTEST_FIXTURErole bits.review_diffgrades by visibility.reconcile_pendingdisclosure.parametrizehandling.reconcile_pendingcount honesty.--logpassthrough.query_cli.Measured against v0.32.0 (nine pinned repositories, bind by bind, every change read at source)
No wrong binds were added anywhere. Guzzle, Sinatra and Dapper are unchanged.
Local gates
Lane I's final HEAD
3df2510:Release commit
1f4189b:Filed as out of scope for this release: #300 (six wrong-bind shapes that predate it) and #301 (make resolution resumable).
🤖 Generated with Claude Code
https://claude.ai/code/session_0126PDDLB4wNHxKXvWM1VNmu
I072 R4 (`follow_own_imports`) copied ANY resolved import bind onto every bare use of the name. Reviewed bind for bind on rust-analyzer it spread 80 phantoms: `quote` 41, `check` 29, `crate_def_map` 4, `Ordering` 4, `Id` 2. Three structural clauses: (a) the target is free (`temp.sym_free`): never a method, field or any symbol with a non-module parent; (b) every import row of the name was proven: tier1q_pass1 or tier1q_rel_samefile, or -- for a tie-break/rel_pkg bind -- the path names the target's FILE by the language's module convention (Rust `crate::`/`super::`/`self::`/exact package head -> src/a/b.rs or a/b/mod.rs, target top-level there or re-exported by a proven import; Python dotted module -> trailing path components); (c) a Rust `use` head is a relative marker, a workspace package tail, or a module the importing file declares. At source (tier 1Q stem arm, Rust only): a one-segment qualifier naming a package is kept to that package's root, so `use hir::Label` no longer tie-breaks onto hir-def/src/hir.rs. Corpus, occurrence-joined vs4e3034a: rust-analyzer -89 (80 phantoms + 2 Label phantoms removed, 7 correct glob-re-export binds lost), 1 retarget (Label use site back to its pre-existingdc841aftier-3 target); the other eight repos bind-for-bind identical. precision_gate rust: +11 probes, +10 forbid sites (7 phantom classes, 2 positives); every forbid site BOUND with index.rs at4e3034a. Clause mutations each went RED on exactly their decoy. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0126PDDLB4wNHxKXvWM1VNmuReviewer B: `follow_own_imports` keys `import_name_target` by (file, name), so `mod tests { use crate:🅱️:build; }` or a function-local `from pkg.b_mod import build` decided every bare `build` in the file. New clause (d): every use site tier 3 is about to decide must SEE an import of the name -- a file-level one, or a nested one whose function, method, class or Rust inline-`mod` body holds the site -- and every import it sees must be resolved to the table's one target; an unresolved nested import still rebinds its scope. Otherwise the name is left to reachability. Same rule in all seven languages; the table is also cut to the names tier 3 is deciding before any per-row work. A first cut (file-level imports only) withdrew 670 correct rust-analyzer binds (574 `check_diagnostics` inside `mod tests`), 110 ripgrep and 32 py-django; this form is measured instead. Corpus vs the previous head: py-django -2 (proxy_model_inheritance/tests.py:33 was a phantom -- a method-local import rebinds ProxyModel there -- and :51 a correct bind the per-name decision cannot keep); the other eight repos identical. Tests: Rust and Python precision decoys (reviewer B's rs1/user.rs:4 and py3/own.py:5 shapes; both BOUND on4e3034a), an indexer test for the unresolved nested import. Mutations RUN: scope check off -> both decoys PHANTOM; target check off -> indexer test RED. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0126PDDLB4wNHxKXvWM1VNmuReviewer B: the R5 barrier was per file. A function-local `from pkg.a_mod import thing as fast` refused every `fast` in the file, so a module-level `def fast` lost its callers elsewhere; `mod inner { use crate:🅱️:Settings as Config; }` refused the file's own `struct Config`. Each aliased import now carries the span it binds in (the innermost function, method, class or Rust inline-`mod` body holding it; none for a file-level import), and a site is barred only by aliases whose span holds it. Item 4's same-file fallback exemption now applies only when every barring alias is FILE-LEVEL -- inside a function that aliases the name itself, the alias wins over the module definition (it did not before this change: B's alias.py:3 would have bound the module `def fast`). Generic, no Python-specific code (lane G). Corpus: bind-for-bind identical in all nine repositories. Tests: Rust and Python precision probes -- required binds at `alias_make` and `alias_g` (both unresolved on4e3034a) and forbidden sites inside the aliasing scope. Mutations RUN: span check off -> both RECALL misses; file-level condition off -> `alias_f` PHANTOM. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0126PDDLB4wNHxKXvWM1VNmuReviewer B: `function gen<T extends (a: number) => void>(cb: T) { return a(); }` -- `header_parameters` took the first `(` of the signature, the generic constraint's function type, so the body's `a()` was refused as a parameter and the real parameter `cb` was not one. `parameter_list_open` finds the first `(` at `<…>` depth 0 (the `>` of `=>` closes nothing); `header_parameters` and `header_nested_parameters` both use it, and the parameters of function TYPES inside the generics are counted as nested (parameters of nothing that runs here). Tests: a unit test and a bind-level test. Mutations RUN: first `(` again -> both RED; generics scan off -> unit test RED (the bind test stays green: the TypeScript extractor writes no binding row for a function type's parameters today, so that clause is belt and braces, recorded as such). Corpus: bind-for-bind identical in all nine repositories. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0126PDDLB4wNHxKXvWM1VNmuReviewer B, three parameters that are locals and were not refused: * a one-line JS/TS function (`function oneLine(helper) { return helper(); }`): the parameter rule required a line after the header. A one-line signature now yields the column where the body starts on that line; * an arrow parameter used on its own line (`items.map((x) => x.y)`): binding rows compared by line only. They now compare by (line, column); * a Rust parameter whose type has no nameable head (`helper: fn() -> u32`): the extractor wrote binding rows for path-typed parameters only. It now writes one for every identifier parameter, qualifier-less when the type has no head -- tier 1R already reads that as "type unknown". The module doc, which claimed all typed parameters, is corrected. Corpus vs B7: rust-analyzer -7, ts-zod -4, all phantoms read at source (`resolver(path)` and `normalize(ty)` on `impl Fn` parameters had bound `pub mod resolver` / `mod normalize`; `select`; Zod arrow parameters `a`, `b`, `iss`); 0 recall loss. New binding rows: rust-analyzer +4 618, rust-ripgrep +607 refs (the baseline `refs` counts move by exactly that). Not modelled still, and recorded: an arrow OUTSIDE every indexed function (a module-level `const f = (h) => h()`) has binding rows but no enclosing scope to bound them. Tests: one bind-level test with three sites; three mutations RUN, each RED on its site. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0126PDDLB4wNHxKXvWM1VNmuObserved live: an installed v0.30.1 (schema 69) against a schema-70 index. Every MCP call failed with a generic "exited with status 1", `code-index query` said only "project_not_available: opening database <path>", and the migration runner's refusal went to the daemon's stderr, which an MCP-spawned daemon has redirected to null. - MCP server: a project slot that is not ready is probed for a schema newer than CURRENT_VERSION (the --db path for the primary). If so, both legs return project_not_available with one canonical hint -- "index schema N is newer than this build supports (M); upgrade code-index (or delete .code-index to rebuild with this version)" -- and a structured `schema_skew: {index_schema, supported_schema}`. Before this the daemon leg said `warming_up` (retry) and the snapshot leg the bare outer context. - Slot errors keep the whole anyhow chain (`{e:#}`), not only the outermost context, for every attach failure. - Daemon: a fatal error from `run` is logged to daemon.log before exit. - CLI: a refusal carrying `schema_skew` also prints the sentence on stderr. Tests: schema_newer_refusal_e2e (both legs, two tools, daemon.log) and query_cli::an_index_newer_than_this_build_is_refused_with_the_repair. Mutations run: skew refusal -> None (both RED), daemon log dropped (RED), CLI stderr line dropped (RED). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0126PDDLB4wNHxKXvWM1VNmu4e3034atree, not the release string, in I074 prose 2866779c24A list `query` returns `{"batches": [...]}` as one successful MCP reply, each entry carrying its own `error`, so `isError` is never set and a batch in which every query was refused exited 0 -- the code of an answer -- while each query asked alone exits 1. Contract, documented in `query --help`, the README CLI reference and `query::every_batch_entry_refused`: exit 1 only when EVERY entry refused; otherwise 0, with refused entries named in batches[i].error. What counts as an entry's refusal is the tool's own (search_text answers a miss with total: 0, alone and batched). The stderr line says "every one of the N batch entries refused". Test: query_cli::a_batch_exits_one_only_when_every_entry_refused. Mutations run: drop the batch check (RED: all-refused exits 0); all -> any (RED: partly answered exits 1). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0126PDDLB4wNHxKXvWM1VNmuSibling-first lookup was already the design everywhere: the CLI spawns `code-index-mcp` from beside itself (else PATH), the server spawns `code-index-daemon` from beside itself (else PATH), and the plugin host is looked for ONLY beside the running server. The observed failure was a `code-index` copied ALONE: the sibling lookup found nothing and fell back to the installed older server on PATH silently, and that server then spoke for its own daemon. The "plugin packages could not be carried" failure lost its reason the same way item 2's did: the slot stored only the outer anyhow context. - CLI: warn on stderr when code-index-mcp was not found beside the CLI and PATH is used. - CLI: warn when the build that answered differs from the CLI's own (`CODE_INDEX_VERSION`, version + hash): the server's, read off the reply's `answer_provenance.build`, and the attached daemon's, read off its registration (with the pid to stop). An unreadable daemon build warns too; the agreeing case is silent. - MCP server: the slot stores the whole chain via `slot_error`, so the plugin-host refusal now reaches the payload with its witness and repair. - query_cli's sibling server (and the daemon) are now built every run: a sibling left from an earlier commit IS a different build, and the new warning said so on the first run. Tests: query_cli::{a_copied_cli_says_it_is_using_the_server_on_path (with a no-warning negative control), a_query_answered_by_a_server_of_ another_build_says_so (unix), a_query_answered_by_a_daemon_of_another_ build_says_so (real daemon)}, query::build_skew_tests, main.rs slot_error_tests. Five mutations run, all RED. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0126PDDLB4wNHxKXvWM1VNmuAfter a schema upgrade, a one-shot `code-index query` with no daemon answered `warming_up` ("retry in 1-2 seconds") forever: on the pinned django repo, 80 queries over 166 s, every one warming_up. Option (b), durable per-call progress, was measured first and does not converge: reparsed files ARE durable (killing `index` every 15 s took pending code files 2,970 -> 1,570 -> 370 -> 0), but the reference-resolution stage after them is ONE transaction (32 s on django) and twelve 15 s windows never completed it. Converging per call needs a window longer than the resolve, which grows with the repo, or a resumable resolver -- indexer work outside this lane. So option (a), disclosure: `code-index query` passes a new `code-index-mcp --one-shot`; a one-shot server with no daemon to heal to (no re-attach launcher) answers `warming_up` with `reconcile_pending` {converges_on_retry: false, code_files_awaiting_reparse, code_files_total, remedy, semantics}, and its hint no longer promises retries succeed. The remedy: "run `code-index index` once, or start the daemon (`code-index watch` / the MCP server)". The CLI prints it on stderr. Long-lived sessions keep the ordinary warming_up: a retry does converge there. Test: query_cli::a_one_shot_query_mid_upgrade_names_the_remedy stages the upgrade exactly as migration 70 does (1,500 generated files), asserts the premise (the reply IS warming_up), the block, the stderr line, and that the named remedy then works. Mutations run: block disabled (RED), `--one-shot` not passed (RED). I075 record updated. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0126PDDLB4wNHxKXvWM1VNmu4e3034a, not the release string, in three review comments 73801ca34cItem 7 of the final review. B1 (I073) decided the own-import rule per (file, name) and withdrew the whole name when any use did not see an import, which refused correct nested binds: Rust `mod tests { use crate:🅱️:build; build() }` beside a glob-imported build() at file level, and Python's function-local import beside a star import (reviewer B's own.py:10). Each use is now graded on the INNERMOST import it can see, by the language's scoping: Rust inline modules do not inherit the parent's use except through `use super::*` (any other glob there may supply the name); a Python class body's import is invisible to its methods; other languages see an import wherever its scope holds the use. Where some uses see the import and others do not, and reachability decides nothing for the name, the name is decided and the out-of-scope uses are refused per site (temp.own_import_refused, applied with the other per-site refusals, and only where the import is what decides the name). Tests: two integration tests (Rust inline mod, super::* glob, other glob; Python shadowing and class body; and a refusal that must not withhold a tier-2 bind), plus both fixtures as REQUIRED binds in the Rust and Python precision oracles. Six mutations run, all red; the whole-name withdrawal also turns both gates' recall red. Corpus bind diff againstf208704, all nine repos: py-django +1 (proxy_model_inheritance/tests.py:51 ProxyModel -> models.py:8, correct; I073 recorded it as the bind the per-name rule could not keep). Every other repo identical. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0126PDDLB4wNHxKXvWM1VNmu