Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.develop.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ Never `unwrap_or_default()` a store error on a search path — "no results" and

## Changelog highlights (recent)

- **v1.5.33 (pending)** — force reindex (`n`, `index -f`) rebuilds the SCIP symbol indexes too — shared `finish_force_reindex` tail for HTTP + TUI; `?symbols=true` stays symbol-only, plain reindex stays gate-driven
- **v1.5.33 (pending)** — Razor searchable: `.razor`/`.cshtml` via text chunking (Html variant); `obj/`, `.g.cs`, `.designer.cs` excluded as generated
- **v1.5.33 (pending)** — /status + MCP status answer from a background snapshot (never measure inline): always responsive under rebuild waves, with `snapshot_age_secs` + a `load` summary for calling agents (`CODESEARCH_STATUS_REFRESH_SECS`, default 2s)
- **v1.5.31 (pending)** — scip-csharp VS-MSBuild restore fallback: NETSDK1045-class CLI SDK gaps (hollow SDK dir, net10-only-in-VS) retry via vswhere + `MSBuild -t:restore`, and MSBuild registration picks a VS instance when no CLI SDK can target the repo
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ finalized in place with a date — no renaming/migration step needed.

### Fixed

- **A force reindex now rebuilds the SCIP symbol indexes too.** `n` in the serve TUI and `codesearch index -f` promised "rebuild everything" but dated from before the symbol indexes existed: the HTTP handler forced the symbol pass only behind the separate `?symbols=true` flag (so a fresh SCIP gate — no `.cs` changes — skipped it), and the TUI's force reindex never ran a symbol pass at all. Field effect: after Razor joined the text index, `find_impact` kept answering from ~19-hour-old references. Force now implies symbols (`finish_force_reindex` is the shared tail of the HTTP and TUI force paths: fresh FSW, then a forced, gate-bypassing symbol pass); `?symbols=true` stays the explicit no-force symbol-only trigger, and a plain reindex stays gate-driven. Local CLI builds (`index -f --local` / delegation failure) still build no symbols — serve owns the symbol pipeline there.

- **Razor markup is now indexed: `.razor` / `.cshtml` ride the text-fallback chunker.** Neither extension had a language mapping, so every Razor file was skipped — field test 2026-10-16 on a customer MVC repo: 9 `.cshtml` files on disk, 0 in the index, and component usage findable only through generated `obj/**/*_razor.g.cs` lines. Both extensions now map to the Html variant (no extractor → text chunking): directives (`@model`, `@inject`, `@page`) and component tags become searchable, without running the C# tree-sitter parser on markup. The watcher and indexer gates key off the same mapping, so incremental reindex covers Razor automatically. Existing indexes keep their chunks until the next reindex. Related deliberate exclusions: `obj/` is now always excluded (always .NET build output — generated code can no longer leak in even when a repo's `.gitignore` misses it), and `.g.cs` / `.designer.cs` suffixes are skipped as generated (the watch layer always claimed `.designer.cs` was excluded; the list never had it). `bin/` is deliberately **not** excluded — several ecosystems keep real scripts there — so the accidental `.g.cs` coverage disappears by design: the real `.razor` files are searchable now.

- **`GET /status` and the MCP `status` tool always answer, even under a heavy rebuild wave.** They used to measure inline in the request path: per registered repo several filesystem probes plus a SCIP LMDB env open and a sysinfo refresh, synchronous on async workers that a rebuild wave (dotnet restore + scip-csharp children) starves — with 28 repos one call took tens of seconds and polling clients (remote TUI, calling agents) piled up and timed out; the project list died the same death. Now a single background task measures (on the blocking pool, one pass at a time — under load the cadence stretches instead of piling measuring tasks onto a starved machine) and stores a snapshot whose readers answer in microseconds. Every answer carries `snapshot_age_secs` (staleness is visible, never a hang) and a `load` summary (`text_indexing` / `symbol_indexing` / `errored` / `refresh_interval_secs`) so calling agents can throttle themselves; before the first pass completes they answer `"starting"` immediately. Cadence: `CODESEARCH_STATUS_REFRESH_SECS`, default 2s.
Expand Down
36 changes: 24 additions & 12 deletions src/serve/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5151,6 +5151,23 @@ async fn reload_handler(
/// - `symbols=true` — also rebuild the symbol index (C# SCIP) after text reindex.
///
/// Returns 202 Accepted immediately; the reindex runs in the background.
/// Shared tail of every FORCE reindex — the HTTP handler and the TUI `n`
/// task both end here, so the two paths cannot drift on whether (or in
/// which order) the symbol pass runs. The pass is FORCED, and the force
/// branch carries no flag: "rebuild everything this repo has" is not a
/// decision a caller (or a future refactor) can opt out of. The explicit
/// no-force symbol-only trigger is `?symbols=true` on the plain reindex.
pub(crate) async fn finish_force_reindex(
state: &Arc<ServeState>,
alias: &str,
project_path: &Path,
db_path: &Path,
stores: Arc<SharedStores>,
) {
state.restart_fsw(alias, stores).await;
maybe_rebuild_symbols(alias, project_path, db_path, state, true).await;
}

async fn reindex_handler(
axum::extract::Path(alias): axum::extract::Path<String>,
axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>,
Expand Down Expand Up @@ -5241,10 +5258,6 @@ async fn reindex_handler(
let guard_alias = alias_bg.clone();
let guard_state = state.clone();

// `symbols=true` forces a symbol rebuild; without it the rebuild is
// gate-driven (missing/stale indexes are still healed).
let do_symbols = symbols;

if force {
// Force rebuild: stop FSW -> clear data in-place -> full reindex -> restart FSW.
// The FSW must be stopped before clearing the FileMetaStore, otherwise it
Expand Down Expand Up @@ -5368,12 +5381,8 @@ async fn reindex_handler(
return;
}

// 3. Restart FSW with fresh IndexManager.
g_state.restart_fsw(&g_alias, stores).await;

// 4. Symbol index rebuild — gate-driven for every installed
// language; `symbols=true` forces it (previous behaviour).
maybe_rebuild_symbols(&alias_bg, &project_path, &db_path, &g_state, do_symbols).await;
// 3. Shared tail: fresh FSW + FORCED symbol pass.
finish_force_reindex(&g_state, &g_alias, &project_path, &db_path, stores).await;

g_state.end_indexing(&g_alias, IndexingOwner::Reindex);
});
Expand Down Expand Up @@ -5454,8 +5463,11 @@ async fn reindex_handler(
}

// Symbol index rebuild — gate-driven for every installed
// language; `symbols=true` forces it (previous behaviour).
maybe_rebuild_symbols(&alias_bg, &project_path, &db_path, &g_state, do_symbols).await;
// language; the explicit `symbols=true` query param forces it
// (the no-force, symbol-only trigger). The FORCE branch above
// carries no flag at all: it ends in `finish_force_reindex`,
// which is always forced — there is no decision left to revert.
maybe_rebuild_symbols(&alias_bg, &project_path, &db_path, &g_state, symbols).await;

g_state.end_indexing(&g_alias, IndexingOwner::Reindex);
});
Expand Down
51 changes: 51 additions & 0 deletions src/serve/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5151,3 +5151,54 @@ async fn mcp_status_and_projects_answer_starting_pre_first_pass() {
"projects pre-first-pass must carry the warming warning, got: {text}"
);
}

#[tokio::test]
async fn finish_force_reindex_starts_fsw_and_keeps_guard_contract() {
// Seam pin for the shared FORCE tail (HTTP force branch + TUI `n` both
// end here). The harness has no SCIP helpers, so the forced symbol
// pass is a per-language no-op here — the forced semantics are pinned
// by the maybe_rebuild_symbols(force=true) test and by the tail
// carrying NO flag a refactor could flip. What this test pins is the
// wiring: the tail (re)starts the FSW for the alias and leaves the
// reindex guard release to the callers (HTTP/TUI), in that order.
let tmp = tempfile::tempdir().unwrap();
let project = tmp.path().join("repo");
std::fs::create_dir_all(&project).unwrap();
let db_path = project.join(DB_DIR_NAME);
let state = std::sync::Arc::new(ServeState::new(ReposConfig::default(), None));

let stores = match state
.try_open_stores("repo", &db_path, true, false, None)
.expect("a fresh repo must open in write mode")
{
OpenedStores::Write(s) => s,
OpenedStores::Readonly(_) => panic!("allow_create=true must open write mode"),
};
assert!(state.begin_indexing("repo", IndexingOwner::Reindex));
// restart_fsw skips aliases that are not registered — register first.
state
.config
.write()
.unwrap()
.repos
.insert("repo".to_string(), project.clone());

// Production order: the force path builds the index BEFORE the tail,
// and the tail's IndexManager init needs a real (empty) index to open.
let token = tokio_util::sync::CancellationToken::new();
IndexManager::force_reindex_with_stores(&project, &db_path, &stores, None, &token, None)
.await
.expect("force reindex of an empty repo must succeed");

super::finish_force_reindex(&state, "repo", &project, &db_path, stores).await;

assert!(
state.fsw_tasks.contains_key("repo"),
"the force tail must leave an FSW task registered for the alias"
);
assert!(
state.is_indexing("repo"),
"the tail must not release the reindex guard — callers own that"
);
state.end_indexing("repo", IndexingOwner::Reindex);
}
6 changes: 4 additions & 2 deletions src/serve/tui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1252,8 +1252,10 @@ pub(crate) fn spawn_force_reindex(alias: String, state: &Arc<ServeState>) -> Rei
return;
}

// Restart FSW with fresh IndexManager.
state_bg.restart_fsw(&alias_bg, stores).await;
// Shared tail: fresh FSW + FORCED symbol pass (the TUI never ran a
// symbol pass at all before — `n` left find_impact on stale
// references while the text index was brand new).
super::finish_force_reindex(&state_bg, &alias_bg, &project_path, &db_path, stores).await;

// Remove guard
state_bg.end_indexing(&alias_bg, IndexingOwner::Reindex);
Expand Down
Loading