A version bump touches seven places and is graded by eight tests across five crates, with no command that does it #284

Closed
opened 2026-09-17 13:26:22 +02:00 by buildagent · 1 comment
Member

What v0.30.0 actually cost

The bump itself is one number. Finding every place that number lives took three CI cycles, and six of the seven sites were found by a gate going red rather than by looking.

# site found by
1 Cargo.toml grep
2 install.ps1 (×2) grep
3 README.md (×6, incl. "Current release") grep
4 Cargo.lock cargo check
5 distribution/registry.v1.json — version + tag cli/registry_schema_gate
6 distribution/registry.v1.json — 8 plugin asset URLs cli/registry_schema_gate
7 crates/guest/{ruby,example,svelte,timeline}/Cargo.lock indexer/release_gate

grep 0.29.0 finds sites 1–3. Everything after that is discovered by pushing and waiting.

Site 7 is the one no local command can reach

The guest crates are outside the workspace — #276's subject. So cargo check --workspace, cargo clippy --workspace, cargo test --workspace and cargo test -p <anything> are all structurally blind to their lockfiles. The only instrument that sees them is release_gate::every_guest_lockfile_agrees_with_the_workspace_version, which failed on a pushed commit ~50 minutes after the push.

To its credit the failure names the exact repair:

Run cargo update --offline -p code-index-guest in that directory as part of the version bump.

A gate that tells you the fix is a good gate. It is still a gate you meet by pushing.

Site 6 is the one that would have shipped a broken catalog

every_plugin_fact_is_derived_from_the_package_it_describes refuses a catalog whose plugin URLs do not carry its own tag:

de.h-dv.ruby 0.6.0 url does not carry the release-tag segment /download/v0.30.0/ … The tag is v0.30.0, which this catalog states two fields away.

That is not pedantry. Every release republishes all four .cip packages under its own tag (release needs package-plugin, -timeline, -ruby, -svelte). A catalog left pointing at v0.29.0 resolves to assets the new release did not publish, and every plugin install from it 404s.

The eight gates that grade a bump

cli/registry_schema_gate                 8 tests
daemon/startup_beacon_e2e                2
indexer/release_gate                    36
indexer/corpus_stage                    11
mcp-server/registry_mcp_e2e              7
mcp-server/answer_provenance_e2e        19
mcp-server/plugin_add_package_fetch_e2e  4
plugin-host/tree_abi_bench               5

Enumerated by grep -rl CARGO_PKG_VERSION --include=*.rs crates/*/tests. Running all eight takes about a minute locally and would have caught sites 5, 6 and 7 before any push.

What would close this

Something that turns seven-places-and-hope into one step. In rough order of appetite:

  1. code-index release bump <version> — a subcommand that rewrites all seven sites and runs cargo update --offline -p code-index-guest in each guest directory. It is the same shape as code-index rules: the tool already owns the files it generates, and this is another set of files it owns.
  2. A gate over the SITES, not just the values. Today each site has its own test. Nothing asserts that the list of sites is complete — a new file carrying a version string joins the set silently, and the next bump discovers it the same way this one did. A registry of version sites, derived where possible, is the shape this repository uses for exactly this problem elsewhere (bounding_site_registry, disclosure_surface_registry).
  3. At minimum, a documented checklist in _prdoc/guides/, listing the seven sites and the eight-gate command line. Cheapest, and strictly better than the current state, which is that the knowledge exists only in this issue.

Option 2 is the one that matches how this project handles "a set nobody is counting" everywhere else.

  • #276 — why site 7 is invisible to every workspace-wide command
  • I066 — why the committed registry must stay platforms.state: unmeasured and only the release may write checksums. That constraint is correct and this issue does not propose changing it; a bump command must edit version, tag and the asset URLs and must NOT touch the platform block.
## What v0.30.0 actually cost The bump itself is one number. Finding every place that number lives took **three CI cycles**, and six of the seven sites were found by a gate going red rather than by looking. | # | site | found by | |---|---|---| | 1 | `Cargo.toml` | grep | | 2 | `install.ps1` (×2) | grep | | 3 | `README.md` (×6, incl. "Current release") | grep | | 4 | `Cargo.lock` | `cargo check` | | 5 | `distribution/registry.v1.json` — `version` + `tag` | `cli/registry_schema_gate` | | 6 | `distribution/registry.v1.json` — 8 plugin asset URLs | `cli/registry_schema_gate` | | 7 | `crates/guest/{ruby,example,svelte,timeline}/Cargo.lock` | `indexer/release_gate` | `grep 0.29.0` finds sites 1–3. Everything after that is discovered by pushing and waiting. ## Site 7 is the one no local command can reach The guest crates are **outside the workspace** — #276's subject. So `cargo check --workspace`, `cargo clippy --workspace`, `cargo test --workspace` and `cargo test -p <anything>` are all structurally blind to their lockfiles. The only instrument that sees them is `release_gate::every_guest_lockfile_agrees_with_the_workspace_version`, which failed on a pushed commit ~50 minutes after the push. To its credit the failure names the exact repair: > Run `cargo update --offline -p code-index-guest` in that directory as part of the version bump. A gate that tells you the fix is a good gate. It is still a gate you meet by pushing. ## Site 6 is the one that would have shipped a broken catalog `every_plugin_fact_is_derived_from_the_package_it_describes` refuses a catalog whose plugin URLs do not carry its own tag: > `de.h-dv.ruby` 0.6.0 url does not carry the release-tag segment `/download/v0.30.0/` … The tag is `v0.30.0`, which this catalog states two fields away. That is not pedantry. Every release republishes all four `.cip` packages under its own tag (`release` needs `package-plugin`, `-timeline`, `-ruby`, `-svelte`). A catalog left pointing at `v0.29.0` resolves to assets the new release did not publish, and every `plugin install` from it 404s. ## The eight gates that grade a bump ``` cli/registry_schema_gate 8 tests daemon/startup_beacon_e2e 2 indexer/release_gate 36 indexer/corpus_stage 11 mcp-server/registry_mcp_e2e 7 mcp-server/answer_provenance_e2e 19 mcp-server/plugin_add_package_fetch_e2e 4 plugin-host/tree_abi_bench 5 ``` Enumerated by `grep -rl CARGO_PKG_VERSION --include=*.rs crates/*/tests`. Running all eight takes about a minute locally and would have caught sites 5, 6 and 7 before any push. ## What would close this Something that turns seven-places-and-hope into one step. In rough order of appetite: 1. **`code-index release bump <version>`** — a subcommand that rewrites all seven sites and runs `cargo update --offline -p code-index-guest` in each guest directory. It is the same shape as `code-index rules`: the tool already owns the files it generates, and this is another set of files it owns. 2. **A gate over the SITES, not just the values.** Today each site has its own test. Nothing asserts *that the list of sites is complete* — a new file carrying a version string joins the set silently, and the next bump discovers it the same way this one did. A registry of version sites, derived where possible, is the shape this repository uses for exactly this problem elsewhere (`bounding_site_registry`, `disclosure_surface_registry`). 3. **At minimum, a documented checklist** in `_prdoc/guides/`, listing the seven sites and the eight-gate command line. Cheapest, and strictly better than the current state, which is that the knowledge exists only in this issue. Option 2 is the one that matches how this project handles "a set nobody is counting" everywhere else. ## Related - #276 — why site 7 is invisible to every workspace-wide command - I066 — why the committed registry must stay `platforms.state: unmeasured` and only the release may write checksums. That constraint is correct and this issue does not propose changing it; a bump command must edit `version`, `tag` and the asset URLs and must NOT touch the platform block.
Author
Member

Closed by #292, shipped in v0.31.0 (2453904).

This took option 2 — the gate over the sites — as the issue recommended, plus a script for option 1. Option 3 was skipped deliberately: a checklist in _prdoc/guides/ is a document nothing checks, which is the same failure mode one level up.

What the tree actually said, versus the table above

Checking each of the seven sites before building anything changed the scope:

Sites 5 and 6 are already gated. crates/cli/tests/registry_schema_gate.rs asserts catalog.code_index.tag == format!("v{}", env!("CARGO_PKG_VERSION")) (line 355) and that every plugin asset URL carries /download/{tag}/ (line 678). They fire at cargo test, not at release time. Nothing new was added for them, on purpose — two gates over one claim can disagree about which one failed.

The genuinely ungated sites were 3 and 7: README.md and the four guest lockfiles.

And there was an eighth thing the table does not list. install.ps1's two occurrences say "a specific release, including an older one" while naming the current release. The install.sh lines they mirror have sat at v0.28.0 untouched. So site 2 was being bumped every release purely to keep its own comment false. Freezing the two .ps1 examples removes the site rather than automating it — seven sites became eight files but one fewer thing to remember.

The registry

crates/indexer/tests/version_site_registry.rs, 6 tests. The site set is derived: the gate lists tracked files, finds those containing the current version, and compares that against BUMP_SITES. A hand list checked against nothing is the drift the gate exists to prevent.

Exclusions are rules, not names: _prdoc/ (records — "Release vX.Y.Z only after CI is green" is a statement about what happened) and dot-directories except .forgejo/.github/.gitlab.

The claim that earns its place is no_human_facing_file_names_a_release_that_is_not_this_one. All three per-file claims pass over a README where three of five occurrences moved; only this one fails. That mutation was run.

Three things measured rather than assumed

  1. .code-index/index.db. The gate's first run went red on the daemon's own database, which stamps the version it was written by. That is what turned the dot-directory exclusion from a guess into a rule.

  2. rusqlite was already at the version being bumped to. Letting cargo own the lockfiles is not tidiness: a textual rewrite would have moved a third-party pin to a version that does not exist and left its checksum describing the old one.

  3. Core dumps. The gate passed locally and failed on the runner, naming twelve crates/indexer/core.<pid> files. CI's container has core dumps enabled, deliberately-killed test subprocesses leave them, and a core dump contains the version string because it snapshots a process whose binary embeds CARGO_PKG_VERSION. Not a regression — that job passed 4002 of 4003 tests with no signal in its log. But the gate was wrong: a version site is something under version control. It now scans git ls-files, with an anti-vacuity floor so a broken listing cannot pass over nothing.

The script

.forgejo/scripts/version_bump.sh <version> derives the same set, hands lockfiles to cargo, and then asserts that nothing outside the exclusion rules still states the old version. A bump that half-lands exits non-zero there rather than at CI or in a reader's install command.

It refuses a leading v, a non-version, and the version already in Cargo.toml. It also refuses when any candidate site is a .rs file — Rust reads the version through env!("CARGO_PKG_VERSION"), so a bump has nothing to write in source, and a match there means something upstream is already wrong.

That last guard exists because the mutation found it. Widening the argument guard let version = "v9.9.9" reach Cargo.toml; the next invocation read old back as that value and rewrote v9.9.9 wherever it appeared — which in this tree is installer_e2e.rs and installer_ps1_e2e.rs, where it is the fake release tag their fixtures are built around. The bump reported success having quietly edited two test suites.

The test for that guard was also wrong at first, and worth recording: it asserted only a non-zero exit, and the mutation survived it, because the script got as far as cargo update and cargo rejected the manifest the script had just written. A non-zero exit borrowed from a different gate — fired after the damage — is not evidence about the gate under test. Each arm now matches the refusal's own words.

Also closed

code-index rules and code-index query shipped in v0.30.0, a release whose stated purpose was agent adoption, and appeared nowhere in README.md for its whole life. Both are documented now, and crates/cli/tests/readme_cli_reference.rs reads the verb list out of code-index --help and fails on any subcommand the README omits.

Verification

The bump to 0.31.0 was performed by the script. It touched exactly the 10 fields this issue specifies — version, tag, and the eight plugin asset URLs. The platform block is byte-identical and still state: unmeasured, so the I066 constraint this issue records is honoured: only the release writes checksums. No package digest moved, since the release packs a checked-in extractor.wasm rather than rebuilding the guest.

Green on one unchanged tree (hash recorded before and after): fmt, clippy -D warnings, rustdoc -D warnings, 373 workspace suites / 4003 tests, 73 e2e suites / 840 tests on the daemon leg, guest gates under --locked, corpus_ratchet 6/6 with tests/corpus/baseline.json unmoved, and precision_gate 7/7 with every POPULATION line printed. Then 13/13 CI jobs on the PR head and 13/13 again on the merge commit.

Closed by #292, shipped in **v0.31.0** (`2453904`). This took **option 2** — the gate over the sites — as the issue recommended, plus a script for option 1. Option 3 was skipped deliberately: a checklist in `_prdoc/guides/` is a document nothing checks, which is the same failure mode one level up. ## What the tree actually said, versus the table above Checking each of the seven sites before building anything changed the scope: **Sites 5 and 6 are already gated.** `crates/cli/tests/registry_schema_gate.rs` asserts `catalog.code_index.tag == format!("v{}", env!("CARGO_PKG_VERSION"))` (line 355) and that every plugin asset URL carries `/download/{tag}/` (line 678). They fire at `cargo test`, not at release time. Nothing new was added for them, on purpose — two gates over one claim can disagree about which one failed. **The genuinely ungated sites were 3 and 7**: `README.md` and the four guest lockfiles. **And there was an eighth thing the table does not list.** `install.ps1`'s two occurrences say *"a specific release, including an older one"* while naming the **current** release. The `install.sh` lines they mirror have sat at `v0.28.0` untouched. So site 2 was being bumped every release purely to keep its own comment false. Freezing the two `.ps1` examples **removes** the site rather than automating it — seven sites became eight files but one fewer thing to remember. ## The registry `crates/indexer/tests/version_site_registry.rs`, 6 tests. The site set is **derived**: the gate lists tracked files, finds those containing the current version, and compares that against `BUMP_SITES`. A hand list checked against nothing is the drift the gate exists to prevent. Exclusions are **rules, not names**: `_prdoc/` (records — "Release vX.Y.Z only after CI is green" is a statement about what happened) and dot-directories except `.forgejo`/`.github`/`.gitlab`. The claim that earns its place is `no_human_facing_file_names_a_release_that_is_not_this_one`. All three per-file claims pass over a README where three of five occurrences moved; only this one fails. That mutation was run. ## Three things measured rather than assumed 1. **`.code-index/index.db`.** The gate's first run went red on the daemon's own database, which stamps the version it was written by. That is what turned the dot-directory exclusion from a guess into a rule. 2. **`rusqlite` was already at the version being bumped to.** Letting cargo own the lockfiles is not tidiness: a textual rewrite would have moved a third-party pin to a version that does not exist and left its `checksum` describing the old one. 3. **Core dumps.** The gate passed locally and failed on the runner, naming twelve `crates/indexer/core.<pid>` files. CI's container has core dumps enabled, deliberately-killed test subprocesses leave them, and a core dump contains the version string because it snapshots a process whose binary embeds `CARGO_PKG_VERSION`. Not a regression — that job passed 4002 of 4003 tests with no signal in its log. But the gate was wrong: **a version site is something under version control.** It now scans `git ls-files`, with an anti-vacuity floor so a broken listing cannot pass over nothing. ## The script `.forgejo/scripts/version_bump.sh <version>` derives the same set, hands lockfiles to cargo, and then **asserts that nothing outside the exclusion rules still states the old version**. A bump that half-lands exits non-zero there rather than at CI or in a reader's install command. It refuses a leading `v`, a non-version, and the version already in `Cargo.toml`. It also refuses when any candidate site is a `.rs` file — Rust reads the version through `env!("CARGO_PKG_VERSION")`, so a bump has nothing to write in source, and a match there means something upstream is already wrong. That last guard exists because the mutation found it. Widening the argument guard let `version = "v9.9.9"` reach `Cargo.toml`; the next invocation read `old` back as that value and rewrote `v9.9.9` wherever it appeared — which in this tree is `installer_e2e.rs` and `installer_ps1_e2e.rs`, where it is the fake release tag their fixtures are built around. The bump reported success having quietly edited two test suites. **The test for that guard was also wrong at first**, and worth recording: it asserted only a non-zero exit, and the mutation *survived* it, because the script got as far as `cargo update` and **cargo** rejected the manifest the script had just written. A non-zero exit borrowed from a different gate — fired after the damage — is not evidence about the gate under test. Each arm now matches the refusal's own words. ## Also closed `code-index rules` and `code-index query` shipped in v0.30.0, a release whose stated purpose was agent adoption, and appeared **nowhere** in `README.md` for its whole life. Both are documented now, and `crates/cli/tests/readme_cli_reference.rs` reads the verb list out of `code-index --help` and fails on any subcommand the README omits. ## Verification The bump to 0.31.0 was performed by the script. It touched exactly the 10 fields this issue specifies — `version`, `tag`, and the eight plugin asset URLs. The **platform block is byte-identical and still `state: unmeasured`**, so the I066 constraint this issue records is honoured: only the release writes checksums. No package digest moved, since the release packs a checked-in `extractor.wasm` rather than rebuilding the guest. Green on one unchanged tree (hash recorded before and after): fmt, clippy `-D warnings`, rustdoc `-D warnings`, 373 workspace suites / 4003 tests, 73 e2e suites / 840 tests on the daemon leg, guest gates under `--locked`, `corpus_ratchet` 6/6 with `tests/corpus/baseline.json` unmoved, and `precision_gate` 7/7 with every POPULATION line printed. Then 13/13 CI jobs on the PR head and 13/13 again on the merge commit.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
h-dv/code-index#284
No description provided.