• v0.28.0 017c6d875d

    code-index v0.28.0
    All checks were successful
    CI / cargo fmt (push) Successful in 52s
    CI / OSS corpus tier-3 scale (nightly) (push) Has been skipped
    CI / Grammar rebuild from source (nightly) (push) Has been skipped
    CI / cargo doc (intra-doc links) (push) Successful in 6m41s
    CI / cargo test (abi, 32-bit + wasm32) (push) Successful in 6m46s
    CI / cargo deny (push) Successful in 7m52s
    CI / cargo check (MSRV 1.98) (push) Successful in 8m16s
    CI / cargo clippy (push) Successful in 8m24s
    CI / cargo check (windows-gnu) (push) Successful in 8m40s
    CI / OSS corpus (tier 1) (push) Successful in 31m58s
    CI / cargo test (push) Successful in 38m9s
    CI (Windows) / fmt + clippy + build + test (windows) (push) Successful in 50m57s
    CI / cargo test (daemon transport) (push) Successful in 15m6s
    CI / Plugin path cost + pool throughput (nightly) (push) Has been skipped
    Release Build / Generate Version (push) Successful in 31s
    Release Build / Required CI green (push) Successful in 1m3s
    Release Build / Build linux-aarch64 (push) Successful in 12m34s
    Release Build / Build linux-x86_64 (push) Successful in 15m1s
    Release Build / Build linux-x86_64-musl (push) Successful in 15m34s
    Release Build / Pack the XAML reference package (push) Successful in 51s
    Release Build / Pack the Ruby language package (push) Successful in 58s
    Release Build / Pack the TimeLine package (push) Successful in 1m10s
    Release Build / Build windows-x86_64 (push) Successful in 20m21s
    Release Build / Windows archive smoke (msvc) (push) Successful in 8s
    Release Build / Create Forgejo Release (push) Successful in 4m7s
    Stable

    buildagent released this 2026-09-10 23:39:06 +02:00 | 257 commits to master since this release

    code-index v0.28.0

    Build: v0.28.0+738

    Four-binary release: code-index-mcp (MCP stdio bridge — the usual entry), code-index-daemon (long-lived watcher + RPC server, auto-spawned), code-index-plugin-host (bounded worker for plugin packages, spawned by the daemon), and code-index (CLI: init, index, watch, doctor, link, plugin).

    Install, and update, with one command — NEW

    curl -sSfL https://git.h-dv.de/h-dv/code-index/raw/branch/master/install.sh | sh
    

    The same command does both. It asks this repository's release API for
    the latest tag; if that version is already installed it says so and
    downloads nothing.

    sh install.sh --check                  # installed vs published; writes nothing
    sh install.sh --tag v0.27.1            # a specific release, including an older one
    sh install.sh --prefix "$HOME/.local"  # default: /usr/local if writable, else ~/.local
    

    There is no self-update subcommand, deliberately: a self-update cannot
    install the first copy, so it can only ever be half a mechanism — and
    the half that rots while installs keep working.

    It refuses rather than guesses, and a refusal installs NOTHING.

    • It never installs bytes it has not verified against the published
      .sha256. If neither sha256sum nor shasum is available it
      REFUSES instead of skipping — a check that silently does not run is
      worse than none, because it reads as having run.
    • The four binaries are staged inside the target directory, verified
      there, and only then renamed into place, so a failure part way
      through cannot leave two new binaries beside two old ones.
    • The staged code-index is asked its own version BEFORE anything is
      replaced, so a wrong-libc or mis-rolled archive leaves your existing
      install untouched rather than bricking it with no way back.
    • An archive member that is a symlink is refused: a symlink resolves
      against YOUR machine, and the default prefix is /usr/local.
    • A destination that is already a directory is refused before the first
      rename, instead of writing inside it and reporting success.
    • macOS is refused by name (#59), Windows is named and pointed at its
      .zip, an unrecognised uname prints what it saw, and the libc
      decision is disclosed in both directions — including when it is a
      guess.

    Every one of those is graded against a real archive, and each arm
    asserts that nothing was installed — not merely that the exit code was
    non-zero. Most of them exist because an independent adversarial pass
    broke the first version of this script and the tests could not see it:
    four of those tests were satisfied by something other than what they
    named, each demonstrated with a mutation that survived.

    Which platforms it must handle is nobody's list: the published set is
    derived from the release workflow's own build matrices, and an archive
    that is neither installable nor waived by name fails the build.

    All three language packages are published — for the first time

    de.h-dv.ruby 0.6.0 ships as a release asset here. v0.27.1 published
    de.h-dv.xaml and de.h-dv.timeline and nothing for Ruby, while
    this repository's own docs called it product — because the audit's
    required-package list was a literal and could not grow when the tree
    did. That list is now enumerated from tests/packages/*/plugin.toml,
    so a fourth package is required the moment its directory exists.

    Installing it, in one command — a URL REQUIRES --sha256, and the
    digest is published beside the package in its .digest.txt:

    code-index plugin add \
      https://git.h-dv.de/h-dv/code-index/releases/download/v0.28.0/de.h-dv.ruby-0.6.0.cip \
      --sha256 sha256:a6b8d7787cecb49c225ff3dda9016c96babc00c4cf59242a2d774c8b06672048 \
      --grant requested
    

    That digest is the recorded one, and the release REFUSES to publish a
    package whose packed digest differs from it — so it is the value the
    asset will have, not a guess. de.h-dv.ruby-0.6.0.cip.digest.txt ships
    beside the package if you would rather read it from there.

    --grant requested is what Ruby needs and the other two packages do
    not: requested means the manifest's whole request, and Ruby's includes
    [capabilities] derived_names, which is what admits the Rails
    association DSL. plugin check is RED without that grant and GREEN with
    it, and BOTH directions are graded — a smoke that only ever passes the
    flag proves nothing about the flag.

    BEHAVIOUR CHANGE: plugin update now compares versions

    Before this release plugin update compared no version at all: an
    OLDER package auto-applied as an update. Measured — 0.0.1 replaced
    9.9.9. It now refuses anything that is not strictly newer, and says
    which side it could not parse when a version is not comparable
    (#255).

    Also on the update path:

    • plugin add <url> now offers the [update] stanza in its payload,
      at the one moment the operator has both the id and the source URL in
      front of them (#257).
    • plugin update has operator documentation. It shipped with none, and
      nothing graded that a subcommand is documented; now something does
      (#256).

    A cold index of a Rust repository costs 36% less SQLite work

    One statement was 36.8% of an entire cold index of rust-ripgrep: the
    container rule's UPDATE refs … EXISTS(…), a correlated subquery
    re-walking every symbol declared above every ref in the file. It is a
    join off the module rows now.

    rust-ripgrep   vm_step 74,456,733 -> 47,462,831    -36.2% of the whole pass
      that statement 27,411,064 ->   404,127           -98.5%, x38 executions both sides
    rust-analyzer         880,065,547 -> 846,528,318    -3.81%
    

    The rust-ripgrep figure is checkable against this repository's own
    records: the 74,456,733 is what cost-baseline.json carried before this
    change, and an independent run after it measured 47,466,737 — 0.008%
    from the re-recorded value, which is inside the run-to-run jitter this
    gate documents. rust-analyzer is NOT priced by that gate, so only its
    post-change total was re-measured independently (846,638,064, 0.013%
    away); its before-figure is a single measurement and is reported as
    one.

    Every other statement in the top eight is identical to the digit, and
    the six priced repos that declare no Rust test module sit inside
    run-to-run jitter. The same statement runs on every single-file
    re-index, so saving a file with a mod tests in it went from about
    721,000 SQLite steps to about 10,600 on ripgrep-sized files.

    Bit 32 is the TEST role, which decides exclude_tests, so a shape that
    tagged a different set would be a correctness regression wearing a perf
    change's clothes. Counts cannot see WHICH refs carry the bit, so the ref
    projection — path, name, kind, line, col, roles — was compared
    binary-to-binary: rust-ripgrep 41,428 refs and rust-analyzer 410,278
    refs md5-IDENTICAL, with 14,804 and 27,609 test-bit-carrying refs as the
    non-vacuous population (#259).

    Honesty and gate fixes

    • Two ratcheted records that describe the same index must now agree:
      sum(stage-baseline.json rule.*) == baseline.json resolved, per repo.
      One resolver change moves several records, and re-recording only the
      one that fired left master's corpus job red for six commits (#248).
    • The agent-task bench pins correct_via_fallback exactly. It was
      recorded and compared by nothing, so answers reachable ONLY through
      the name-fallback channel could move silently (#249).
    • A per-link payload ceiling was measuring the temp directory's own
      path length rather than the payload (#252).
    • A conformance verdict dropped the bound a refusal named, so
      host.deadline_exceeded could not be told from a different limit
      hitting the same wall (#254).
    • read_code no longer says an empty body "PROVES the range does not
      exist". It proves it about the tree THIS SERVER indexes, which
      answer_provenance.indexed_trees names — if you are reading a sibling
      worktree, it is not a statement about your copy (#258).
    • cost_attribution covers all nine pinned corpus repos, not three, and
      selects one with COSI_ATTRIBUTION_REPO. A positional cargo filter
      that matches nothing exits 0 and reads as a clean run, which is how a
      cost was blessed that could name a change but not a statement (#259).

    Known gaps, stated

    • answer_provenance reports a commit read from git, so an index that
      has not caught up to the working tree still reports a clean tree at
      the new HEAD — a watcher-lag miss and a measured absence are
      indistinguishable (#260, with #245).
    • Two py-django phantoms and two lost correct binds remain diagnosed
      and unfixed (#250, #251).
    • macOS is still not built and not published (#59).
    • install.sh cannot yet be pinned to a release; it is fetched from the
      default branch (#261).
    • code-index-plugin-host does not answer --version, so it is the one
      shipped binary that cannot say which build it is (#262).

    Verify what you downloaded

    Every archive ships a .sha256 beside it, and every .cip ships
    .sha256, .digest.txt and a signature by the recorded first-party
    publisher key. install.sh checks the archive automatically.

    Downloads

    Four platforms are built for every release. The table below says which ones THIS release published.

    Platform Archive
    Linux x86_64 (glibc) code-index-v0.28.0-linux-x86_64.tar.gz
    Linux x86_64 (static/musl) code-index-v0.28.0-linux-x86_64-musl.tar.gz
    Linux ARM64 code-index-v0.28.0-linux-aarch64.tar.gz
    Windows x64 code-index-v0.28.0-windows-x86_64.zip

    XAML plugin package

    de.h-dv.xaml-0.2.0.cip makes .xaml files carry symbols and references instead of being text-only: x:Class and Click= handlers bind into the paired C# code-behind, x:Name becomes a searchable declaration, and {Binding …} stays unresolved because no bridge can reach it.

    It is NOT installed by installing code-index. Packages are installed into a user-controlled store and approved per project, and installing grants nothing — enable is the grant.

    Download de.h-dv.xaml-0.2.0.cips too and keep the .cips beside the .cip. Packages are signed and an unsigned one is refused (signature_missing) with no flag that accepts it. You do NOT need to anchor us first: this release's publisher key is compiled into the binary, so the install below works on a machine that has anchored nobody. code-index plugin trust list shows it, marked [BUILTIN], and code-index plugin trust remove sha256:1cb03259a8c870b6db02360abd9351e17e67724d1f8c3509d85c4bf6b06fa72c withdraws it — see About that key.

    code-index plugin install de.h-dv.xaml-0.2.0.cip --sha256 sha256:7b572f5cc32ffcd5af550900a451bbef37e255804776d7e3e3e71d2dd7d0aa79
    code-index plugin check   sha256:7b572f5cc32ffcd5af550900a451bbef37e255804776d7e3e3e71d2dd7d0aa79
    code-index plugin enable  sha256:7b572f5cc32ffcd5af550900a451bbef37e255804776d7e3e3e71d2dd7d0aa79 \
        --capabilities bridge_source \
        --bridges 'de.h-dv.xaml/xaml:type->csharp:class,de.h-dv.xaml/xaml:call->csharp:method'
    

    The digest above is the one this release packed and it is what --sha256 pins; de.h-dv.xaml-0.2.0.cip.digest.txt carries it and the extraction identity. A digest that changes between releases means the package changed — re-pin, and read the notes.

    Only .xaml is claimed by this package. The TimeLine package below claims .dataset, .xsql, .shd and .lgd; all other markup remains text-only (searchable, no symbols).

    TimeLine plugin package

    de.h-dv.timeline-0.1.0.cip makes the four TimeLine definition formats carry symbols and references instead of being text-only: .dataset and .xsql through one language, .shd and .lgd through another, both out of a single sandboxed extractor. References resolve INSIDE a definition file; nothing in v1 crosses into the C# that loads it.

    It is NOT installed by installing code-index. Packages are installed into a user-controlled store and approved per project, and installing grants nothing — enable is the grant.

    Download de.h-dv.timeline-0.1.0.cips too and keep the .cips beside the .cip. The same signature rules apply as for the XAML package above, and the same key signs both.

    code-index plugin install de.h-dv.timeline-0.1.0.cip --sha256 sha256:150ceb22ed9757a660d5a2bf58b80c81b60043cc419e5da5dde559d5d6fc9c9f
    code-index plugin check   sha256:150ceb22ed9757a660d5a2bf58b80c81b60043cc419e5da5dde559d5d6fc9c9f
    code-index plugin enable  sha256:150ceb22ed9757a660d5a2bf58b80c81b60043cc419e5da5dde559d5d6fc9c9f \
        --capabilities same_file_candidate,exported_candidate
    

    No --bridges, and that is the package saying so: it declares none, so there is nothing to grant. The digest above is the one this release packed and it is what --sha256 pins; de.h-dv.timeline-0.1.0.cip.digest.txt carries it and the extraction identity.

    The 5 fixtures shipped inside it are synthetic and modelled on the worked examples in the two format specifications. They carry no customer definition.

    Ruby plugin package

    de.h-dv.ruby-0.6.0.cip is the compiled-in Ruby extractor as an EXTERNAL, sandboxed package: tree-sitter-ruby loaded at runtime, the extractor compiled to wasm32-unknown-unknown and byte-reproducible from crates/guest/ruby/, and the same symbols, references, visibility and Rails association names the built-in produces.

    It claims .rbx, not .rb. The compiled-in Ruby plugin owns .rb, .rake, .gemspec, Rakefile and Gemfile, and a package may not claim a file a built-in claims. So installing this changes NOTHING about how your .rb files are indexed — it is the migration proof for running a full language out of a package, published so it can be read and run rather than described. Do not install it expecting to replace built-in Ruby support.

    It is NOT installed by installing code-index. Packages are installed into a user-controlled store and approved per project, and installing grants nothing — enable is the grant.

    Download de.h-dv.ruby-0.6.0.cips too and keep the .cips beside the .cip. The same signature rules apply as for the packages above, and the same key signs all three.

    code-index plugin install de.h-dv.ruby-0.6.0.cip --sha256 sha256:a6b8d7787cecb49c225ff3dda9016c96babc00c4cf59242a2d774c8b06672048
    code-index plugin check   sha256:a6b8d7787cecb49c225ff3dda9016c96babc00c4cf59242a2d774c8b06672048 --derived-names
    code-index plugin enable  sha256:a6b8d7787cecb49c225ff3dda9016c96babc00c4cf59242a2d774c8b06672048 --derived-names \
        --capabilities same_file_candidate,exported_candidate,reachability_anchor,qualified_candidate,type_position_candidate,member_candidate
    

    --derived-names is required on check as well as on enable, and without it check REPORTS FAILED. Rails' has_many :posts emits a type reference named Post at the span of the literal :posts, so the name was not copied out of the source it points at; validation is all-or-nothing per file, so the authority is what admits the whole file rather than the one row. Withhold it and you keep the package with every Rails file refused — that is a supported answer, not a broken install.

    No --bridges: Ruby resolves inside its own language, so the package declares none. The six capabilities above are exactly what its manifest requests and enable refuses a grant wider than the request; grant fewer and the index stays searchable but resolves less, because every candidate pool a reference could be admitted to is one of them.

    The digest above is the one this release packed and it is what --sha256 pins; de.h-dv.ruby-0.6.0.cip.digest.txt carries it and the extraction identity.

    The 5 fixtures shipped inside it were generated from the COMPILED-IN Ruby extractor, by a different author from the port they grade, and plugin check compares the package's facts against them exhaustively.

    About that key

    sha256:1cb03259a8c870b6db02360abd9351e17e67724d1f8c3509d85c4bf6b06fa72c is a long-lived Ed25519 key held as a secret in this project's CI, used to sign released packages automatically, and it is compiled into the binaries in this release. It signs every package above. That trusts this project's release pipeline, not only its maintainers: anyone who can run a release workflow here can produce a package that verifies under it. It adds nothing you had not already granted — the bytes are inside the program you are running, so forging that anchor means forging the binary — but you are entitled to know it is there before a package installs rather than after.

    There is no expiry — deliberately, because offline installations must not break on a timer — and withdrawal is on your side and works: code-index plugin trust remove sha256:1cb03259a8c870b6db02360abd9351e17e67724d1f8c3509d85c4bf6b06fa72c. A compiled-in key has no file to delete, so that writes one instead, carrying denied = true; the key stops verifying at the next load, plugin trust list shows it marked [DENIED], and deleting that file (the command prints its path) is the only undo. If the key is ever rotated, the new fingerprint is published here and in tests/packages/first-party.fingerprint, and a build that predates the rotation will not trust it.

    You may anchor it yourself instead — code-index plugin trust add code-index-publisher.pub --fingerprint sha256:1cb03259a8c870b6db02360abd9351e17e67724d1f8c3509d85c4bf6b06fa72c --name 'a label you choose', with code-index-publisher.pub from this release's assets. Your file replaces the compiled-in entry, and the --name is a label YOU choose: nothing in the package, the key file or the signature can name its own publisher.

    macOS is not currently built. No x86_64-apple-darwin or aarch64-apple-darwin archive is published — macOS users should build from source (cargo build --release). This is a deliberate deferral, tracked in #59; no date is promised.

    Wire into Claude Code

    { "mcpServers": { "code-index": { "command": "/usr/local/bin/code-index-mcp", "args": ["--root", "/path/to/your/project"] } } }
    

    SHA256 checksums (.sha256 files) available for every archive.

    Downloads