Skip to content

Commit a7b8342

Browse files
authored
fix: recover generated state during init indexing (#7)
## Summary This prepares the `v2.1.1` patch release and fixes the setup/indexing failure mode seen after upgrading when existing generated `.gather-step/storage` state is stale or incompatible. The release also improves operator-facing recovery text so the CLI gives sentence-cased, actionable guidance instead of exiting with only a storage-open context. ## Root Cause `gather-step init --index` invoked the normal index path without auto-recovery. If generated graph/search/metadata state from an older version was stale or schema-incompatible, setup could fail before parsing repos and only surface the surrounding storage-open context. ## Key Decisions - Treat `init --index` as a setup/rebuild flow and enable generated-state auto-recovery there. - Keep plain `gather-step index` conservative; operators still opt into rebuilds with `--auto-recover`. - Point unsupported graph schema state to `gather-step index --auto-recover`. - Use proper sentence casing for stable operator errors. - Document Homebrew upgrades as `brew update` followed by `brew upgrade thedoublejay/tap/gather-step`. ## Files Changed | File | Change | | --- | --- | | `crates/gather-step-cli/src/commands/init.rs` | Enables auto-recovery for setup-triggered indexing. | | `crates/gather-step-cli/src/commands/index.rs` | Clarifies recovery progress output. | | `crates/gather-step-cli/src/errors.rs` | Adds proper-cased, actionable recovery messages for generated-state failures. | | `crates/gather-step-cli/tests/*` | Adds and updates recovery/message regression coverage. | | `Cargo.toml`, crate manifests, `Cargo.lock`, `website/package.json` | Bumps package metadata to `2.1.1`. | | `website/src/content/docs/changelog.md` | Marks `v2.1.1` as released. | | `website/src/content/docs/guides/installation.md` | Clarifies Homebrew upgrade commands. | ## Verification - [x] `cargo fmt --check` - [x] `cargo check -p gather-step --all-targets` - [x] `cargo test -p gather-step --test cli_wizard_full -- --nocapture` - [x] `cargo test -p gather-step --test cli_commands corrupt_graph_index_reports_auto_recover_and_auto_recover_rebuilds -- --exact --nocapture` - [x] `cargo test -p gather-step --test cli_commands unsupported_metadata_schema_reports_stable_rebuild_message -- --exact --nocapture` - [x] `cargo test -p gather-step --test cli_commands stable_error_when_config_yaml_is_malformed -- --exact --nocapture` - [x] `cargo nextest run --all-features` - [x] `cargo clippy -p gather-step --all-targets --all-features -- -D warnings` - [x] `cargo run -q -p gather-step -- --version` prints `gather-step 2.1.1` - [x] `cd website && bun run build` ## Rollout After merge, tag `v2.1.1` and run the release workflow. ## Follow-ups - None for this PR.
2 parents e2216c6 + 42916d7 commit a7b8342

15 files changed

Lines changed: 148 additions & 53 deletions

File tree

Cargo.lock

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ members = [
1313
]
1414

1515
[workspace.package]
16-
version = "2.1.0"
16+
version = "2.1.1"
1717
authors = ["JJ Adonis"]
1818
edition = "2024"
1919
rust-version = "1.94.1"
@@ -23,11 +23,11 @@ homepage = "https://github.com/thedoublejay/gather-step"
2323
description = "High-performance multi-repo codebase intelligence engine"
2424

2525
[workspace.dependencies]
26-
gather-step-analysis = { path = "crates/gather-step-analysis", version = "2.1.0" }
27-
gather-step-core = { path = "crates/gather-step-core", version = "2.1.0" }
28-
gather-step-mcp = { path = "crates/gather-step-mcp", version = "2.1.0" }
29-
gather-step-parser = { path = "crates/gather-step-parser", version = "2.1.0" }
30-
gather-step-storage = { path = "crates/gather-step-storage", version = "2.1.0" }
26+
gather-step-analysis = { path = "crates/gather-step-analysis", version = "2.1.1" }
27+
gather-step-core = { path = "crates/gather-step-core", version = "2.1.1" }
28+
gather-step-mcp = { path = "crates/gather-step-mcp", version = "2.1.1" }
29+
gather-step-parser = { path = "crates/gather-step-parser", version = "2.1.1" }
30+
gather-step-storage = { path = "crates/gather-step-storage", version = "2.1.1" }
3131

3232
tree-sitter = "=0.26.8"
3333
tree-sitter-typescript = "0.23.2"

crates/gather-step-bench/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ harness = false
3030

3131
[dependencies]
3232
gather-step-core.workspace = true
33-
gather-step = { path = "../gather-step-cli", version = "2.1.0" }
33+
gather-step = { path = "../gather-step-cli", version = "2.1.1" }
3434
gather-step-mcp.workspace = true
3535
gather-step-storage.workspace = true
3636
anyhow.workspace = true

crates/gather-step-cli/Cargo.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ clap.workspace = true
2323
comfy-table.workspace = true
2424
console.workspace = true
2525
crossbeam-channel.workspace = true
26-
gather-step-analysis = { path = "../gather-step-analysis", version = "2.1.0" }
26+
gather-step-analysis = { path = "../gather-step-analysis", version = "2.1.1" }
2727
gather-step-core.workspace = true
28-
gather-step-git = { path = "../gather-step-git", version = "2.1.0" }
29-
gather-step-mcp = { path = "../gather-step-mcp", version = "2.1.0" }
30-
gather-step-output = { path = "../gather-step-output", version = "2.1.0" }
28+
gather-step-git = { path = "../gather-step-git", version = "2.1.1" }
29+
gather-step-mcp = { path = "../gather-step-mcp", version = "2.1.1" }
30+
gather-step-output = { path = "../gather-step-output", version = "2.1.1" }
3131
gather-step-parser.workspace = true
3232
gather-step-storage.workspace = true
3333
indicatif.workspace = true

crates/gather-step-cli/src/commands/index.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ fn reset_and_reopen_indexer(
189189
)
190190
})?;
191191
output.line(format!(
192-
" {} Auto-recovered generated index state; rebuilding from source repos.",
192+
" {} Rebuilding generated index state from source repos.",
193193
style("→").cyan()
194194
));
195195
RepoIndexer::open(storage_root, IndexingOptions::default())

crates/gather-step-cli/src/commands/init.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ async fn run_non_interactive(app: &AppContext, args: InitArgs) -> Result<()> {
9595
let output = app.output();
9696

9797
if args.index && !args.no_index {
98-
index::run(app, index::IndexArgs::default()).await?;
98+
index::run(app, init_index_args()).await?;
9999
}
100100
if args.generate_ai_files && !args.no_generate_ai_files {
101101
generate::run_summary_pair(app)?;
@@ -167,7 +167,7 @@ async fn run_wizard(app: &AppContext, args: InitArgs) -> Result<()> {
167167
write_default_config_with_repos(app, &args, repos)?;
168168

169169
if do_index {
170-
index::run(app, index::IndexArgs::default()).await?;
170+
index::run(app, init_index_args()).await?;
171171
}
172172
if do_ai {
173173
generate::run_summary_pair(app)?;
@@ -186,6 +186,13 @@ async fn run_wizard(app: &AppContext, args: InitArgs) -> Result<()> {
186186
Ok(())
187187
}
188188

189+
fn init_index_args() -> index::IndexArgs {
190+
index::IndexArgs {
191+
auto_recover: true,
192+
..index::IndexArgs::default()
193+
}
194+
}
195+
189196
fn write_default_config(app: &AppContext, args: &InitArgs) -> Result<()> {
190197
let repos = discover_git_repos(&app.workspace_path)?;
191198
write_default_config_with_repos(app, args, repos)

crates/gather-step-cli/src/errors.rs

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use anyhow::Error;
44
use gather_step_core::ConfigError;
55
use gather_step_storage::{GraphStoreError, MetadataStoreError, SearchStoreError};
66

7-
const UNSUPPORTED_SCHEMA_MESSAGE: &str = "your local index uses an unsupported schema; run `gather-step clean && gather-step index` to rebuild";
7+
const UNSUPPORTED_SCHEMA_MESSAGE: &str = "Generated index state uses an unsupported schema. Run `gather-step index --auto-recover` to rebuild from source repos.";
88

99
#[must_use]
1010
pub fn format_operator_error(error: &Error) -> String {
@@ -27,36 +27,39 @@ pub fn format_operator_error(error: &Error) -> String {
2727
match graph_error {
2828
GraphStoreError::StorageHeld { .. }
2929
| GraphStoreError::StorageHeldByDaemon { .. } => {
30-
return "another gather-step process is using this workspace; stop `gather-step watch` or `gather-step serve --watch`, then retry".to_owned();
30+
return "Another gather-step process is using this workspace. Stop `gather-step watch` or `gather-step serve --watch`, then retry.".to_owned();
3131
}
3232
GraphStoreError::Corrupt { .. } => {
33-
return "your index is corrupt or incomplete; run `gather-step index --auto-recover` to rebuild generated state, or run `gather-step clean && gather-step index`".to_owned();
33+
return "Your index is corrupt or incomplete. Run `gather-step index --auto-recover` to rebuild generated state, or run `gather-step clean && gather-step index`.".to_owned();
34+
}
35+
GraphStoreError::SchemaVersionMismatch { .. } => {
36+
return UNSUPPORTED_SCHEMA_MESSAGE.to_owned();
3437
}
3538
_ => {}
3639
}
3740
}
3841
}
3942

4043
if contains_ascii_case_insensitive(&full, "workspace is not a git repository") {
41-
return "workspace is not a git repository. Next step: run from a git checkout or omit `--release-gate` for an unsealed run".to_owned();
44+
return "Workspace is not a git repository. Next step: run from a git checkout or omit `--release-gate` for an unsealed run.".to_owned();
4245
}
4346
if contains_ascii_case_insensitive(&full, ".gather-step")
4447
&& contains_ascii_case_insensitive(&full, "permission denied")
4548
{
46-
return "cannot write `.gather-step` generated state. Next step: fix permissions on `.gather-step` or pass writable `--storage`/`--registry` paths".to_owned();
49+
return "Cannot write `.gather-step` generated state. Next step: fix permissions on `.gather-step` or pass writable `--storage`/`--registry` paths.".to_owned();
4750
}
4851
if contains_ascii_case_insensitive(&full, "database already open")
4952
|| contains_ascii_case_insensitive(&full, "already locked by another gather-step process")
5053
|| contains_ascii_case_insensitive(&full, "locked by gather-step pid")
5154
{
52-
return "another gather-step process is using this workspace; stop `gather-step watch` or `gather-step serve --watch`, then retry".to_owned();
55+
return "Another gather-step process is using this workspace. Stop `gather-step watch` or `gather-step serve --watch`, then retry.".to_owned();
5356
}
5457
if contains_ascii_case_insensitive(&full, "db corrupted")
5558
|| contains_ascii_case_insensitive(&full, "corrupt")
5659
|| contains_ascii_case_insensitive(&full, "repair aborted")
5760
|| contains_ascii_case_insensitive(&full, "manual upgrade required")
5861
{
59-
return "your index is corrupt or incomplete; run `gather-step index --auto-recover` to rebuild generated state, or run `gather-step clean && gather-step index`".to_owned();
62+
return "Your index is corrupt or incomplete. Run `gather-step index --auto-recover` to rebuild generated state, or run `gather-step clean && gather-step index`.".to_owned();
6063
}
6164

6265
one_line(error.to_string())
@@ -66,27 +69,29 @@ fn format_config_error(error: &ConfigError) -> String {
6669
match error {
6770
ConfigError::Read { path, source } if source.kind() == ErrorKind::NotFound => {
6871
format!(
69-
"config not found: {path}. Next step: run `gather-step init` or pass `--config <path>`"
72+
"Config not found: {path}. Next step: run `gather-step init` or pass `--config <path>`."
7073
)
7174
}
7275
ConfigError::Read { path, source } if source.kind() == ErrorKind::PermissionDenied => {
7376
format!(
74-
"cannot read config: {path}. Next step: fix file permissions or pass `--config <path>`"
77+
"Cannot read config: {path}. Next step: fix file permissions or pass `--config <path>`."
7578
)
7679
}
7780
ConfigError::Read { path, .. } => {
78-
format!("cannot read config: {path}. Next step: fix the path or pass `--config <path>`")
81+
format!(
82+
"Cannot read config: {path}. Next step: fix the path or pass `--config <path>`."
83+
)
7984
}
8085
ConfigError::Parse { path, .. } => {
81-
format!("config YAML is malformed: {path}. Next step: fix the YAML syntax and rerun")
86+
format!("Config YAML is malformed: {path}. Next step: fix the YAML syntax and rerun.")
8287
}
8388
ConfigError::Validation { reason, .. } if reason.contains("path does not exist") => {
8489
format!(
85-
"configured repo path does not exist: {reason}. Next step: create the repo directory or fix the repo path in the config"
90+
"Configured repo path does not exist: {reason}. Next step: create the repo directory or fix the repo path in the config."
8691
)
8792
}
8893
ConfigError::Validation { reason, .. } => {
89-
format!("config is invalid: {reason}. Next step: fix the config and rerun")
94+
format!("Config is invalid: {reason}. Next step: fix the config and rerun.")
9095
}
9196
}
9297
}
@@ -118,3 +123,24 @@ fn contains_ascii_case_insensitive(haystack: &str, needle: &str) -> bool {
118123
.windows(needle.len())
119124
.any(|window| window.eq_ignore_ascii_case(needle.as_bytes()))
120125
}
126+
127+
#[cfg(test)]
128+
mod tests {
129+
use gather_step_storage::GraphStoreError;
130+
131+
use super::format_operator_error;
132+
133+
#[test]
134+
fn graph_schema_mismatch_reports_auto_recover() {
135+
let error = anyhow::Error::new(GraphStoreError::SchemaVersionMismatch {
136+
stored: 0,
137+
expected: 1,
138+
})
139+
.context("opening storage at /tmp/workspace/.gather-step/storage");
140+
141+
let message = format_operator_error(&error);
142+
143+
assert!(message.contains("unsupported schema"));
144+
assert!(message.contains("gather-step index --auto-recover"));
145+
}
146+
}

crates/gather-step-cli/tests/cli_commands.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,7 @@ fn stable_error_when_config_is_missing() {
486486

487487
let output = run_fail(temp.path(), &["index", "--json"]);
488488
let stderr = String::from_utf8_lossy(&output.stderr);
489-
assert!(stderr.contains("config not found:"));
489+
assert!(stderr.contains("Config not found:"));
490490
assert!(stderr.contains("Next step: run `gather-step init`"));
491491
}
492492

@@ -497,7 +497,7 @@ fn stable_error_when_config_yaml_is_malformed() {
497497

498498
let output = run_fail(temp.path(), &["index", "--json"]);
499499
let stderr = String::from_utf8_lossy(&output.stderr);
500-
assert!(stderr.contains("config YAML is malformed:"));
500+
assert!(stderr.contains("Config YAML is malformed:"));
501501
assert!(stderr.contains("Next step: fix the YAML syntax and rerun"));
502502
}
503503

@@ -516,7 +516,7 @@ repos:
516516

517517
let output = run_fail(temp.path(), &["index", "--json"]);
518518
let stderr = String::from_utf8_lossy(&output.stderr);
519-
assert!(stderr.contains("configured repo path does not exist:"));
519+
assert!(stderr.contains("Configured repo path does not exist:"));
520520
assert!(stderr.contains("repo `backend_standard` path does not exist"));
521521
}
522522

@@ -526,7 +526,7 @@ fn release_gate_rejects_non_git_workspace_with_stable_error() {
526526

527527
let output = run_fail(temp.path(), &["index", "--release-gate", "--json"]);
528528
let stderr = String::from_utf8_lossy(&output.stderr);
529-
assert!(stderr.contains("workspace is not a git repository"));
529+
assert!(stderr.contains("Workspace is not a git repository"));
530530
assert!(stderr.contains("omit `--release-gate`"));
531531
}
532532

@@ -542,7 +542,7 @@ fn corrupt_graph_index_reports_auto_recover_and_auto_recover_rebuilds() {
542542

543543
let output = run_fail(temp.path(), &["index", "--json"]);
544544
let stderr = String::from_utf8_lossy(&output.stderr);
545-
assert!(stderr.contains("index is corrupt or incomplete"));
545+
assert!(stderr.contains("Your index is corrupt or incomplete"));
546546
assert!(stderr.contains("gather-step index --auto-recover"));
547547

548548
let recovered = run_ok(temp.path(), &["index", "--auto-recover", "--json"]);
@@ -566,7 +566,7 @@ fn unsupported_metadata_schema_reports_stable_rebuild_message() {
566566
let output = run_fail(temp.path(), &["index", "--json"]);
567567
let stderr = String::from_utf8_lossy(&output.stderr);
568568
assert!(stderr.contains("unsupported schema"));
569-
assert!(stderr.contains("gather-step clean && gather-step index"));
569+
assert!(stderr.contains("gather-step index --auto-recover"));
570570

571571
let recovered = run_ok(temp.path(), &["index", "--auto-recover", "--json"]);
572572
let recovered_json = stdout_json(&recovered);
@@ -584,8 +584,8 @@ fn concurrent_graph_open_reports_stable_process_error() {
584584
.expect("graph should open and hold the redb lock");
585585
let output = run_fail(temp.path(), &["status", "--json"]);
586586
let stderr = String::from_utf8_lossy(&output.stderr);
587-
assert!(stderr.contains("another gather-step process is using this workspace"));
588-
assert!(stderr.contains("stop `gather-step watch`"));
587+
assert!(stderr.contains("Another gather-step process is using this workspace"));
588+
assert!(stderr.contains("Stop `gather-step watch`"));
589589
}
590590

591591
#[test]
@@ -605,7 +605,7 @@ fn generated_state_permission_denied_reports_stable_error() {
605605
let output = run_fail(temp.path(), &["index", "--json"]);
606606
let _ = fs::set_permissions(&generated_root, fs::Permissions::from_mode(0o700));
607607
let stderr = String::from_utf8_lossy(&output.stderr);
608-
assert!(stderr.contains("cannot write `.gather-step` generated state"));
608+
assert!(stderr.contains("Cannot write `.gather-step` generated state"));
609609
assert!(stderr.contains("fix permissions on `.gather-step`"));
610610
}
611611

0 commit comments

Comments
 (0)