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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.15.0] - 2026-08-31

### Added

- **Opt-in `vecq` vector store for Brain Mode.** Set `brain.vector_store: vecq` in `.cora.yaml` to replace the usearch HNSW index with a vecq quantized scan (pure Rust, deterministic, ~5x smaller). Keyed persistence included: symbol ids survive reload, so a fresh process serves the index as-is and `cora index` no longer re-embeds unchanged projects (#542, #547).
- **`brain.vector_bits` quantization-width knob.** `residual` (default) | `4` | `5` | `6` — 4-bit base codes with second-pass residual rescoring, or plain Lloyd-Max widths. The default is residual: best recall@10 at 4-bit scan speed in a recall study on cora's own embeddings, ahead of plain 5-bit at 1k/5k/13k-symbol scales. Changing the width rebuilds the index once on the next `cora index` instead of silently serving the old width; unknown values fall back to `residual`.

### Fixed

- **Vector signal never fired in a fresh process.** `cora brain` and MCP `brain_search` only saw the vector index if the same process had run the embed — otherwise results silently degraded to FTS-only. The search path now lazy-loads the on-disk index once per process, with a dimension guard against backend switches (#545).
- **Stale embed fingerprints after a global vector-index rebuild.** The vector index is a single file shared by all projects; rebuilding it (width/dims change, legacy file, corruption) wiped every project's vectors while their fingerprints still said "embedded" — the incremental path would skip those symbols forever. A rebuild now clears fingerprints for all projects, and the usearch dims-mismatch path (which deleted the index without clearing) joins the same heal.

### Changed

- **vecq-core dependency 0.2.0 → 0.3.0.** Picks up the 4-bit+residual mode, plain 5/6-bit widths, runtime-detected AVX2 scoring, and file formats v1.3–v1.5 with the keyed-slot table. Pre-0.3.0 `.vecq` files carry no key table and rebuild once with a warning, then upgrade to the keyed format.

## [0.14.0] - 2026-08-28

### Fixed
Expand Down
9 changes: 8 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "cora-code"
version = "0.14.0"
version = "0.15.0"
edition = "2024"
description = "CLI-first AI code review — BYOK, diff/scan/branch, pre-commit hooks"
license = "Apache-2.0"
Expand Down Expand Up @@ -72,6 +72,7 @@ rusqlite = { version = "0.31", features = ["bundled"] }

# Vector search (Phase 3 — Brain Mode)
usearch = "2"
vecq-core = "0.3.0"
fs2 = "0.4"

# Self-update (upgrade command)
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ cargo install --git https://github.com/codecoradev/cora-code

> Pin a version: `CORA_VERSION=v0.6.1 curl -fsSL ... | sh`

**Upgrading:** run `cora upgrade` (downloads the latest release, verifies its SHA-256 checksum, replaces the binary) or `cora upgrade --check` to just see if one is available. If you installed via `cargo install --path .`, re-run that instead.

**Verify which `cora` you're running** — `which -a cora` will reveal stale copies from other channels:

```bash
Expand Down Expand Up @@ -213,6 +215,7 @@ Works on **all CI platforms** — [Gitea, GitLab, Bitbucket →](https://codecor
| `cora serve` | Start MCP server + auto-reindex on startup |
| `cora install` | Auto-detect and configure AI coding agents |
| `cora hook install` | Install pre-commit hook |
| `cora upgrade` | Self-upgrade from GitHub Releases (checksum-verified) |

See **[CLI Reference →](https://codecora.dev/cora/docs/cli-reference)** for all flags and examples.

Expand Down
13 changes: 13 additions & 0 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,19 @@ See [Code Intelligence](./code-intelligence) for detailed usage.
| `cora mcp` | Start MCP server for AI coding agents (Claude Code, Cursor, Windsurf) |
| `cora serve` | Start MCP server with auto-reindex on startup |

### Self-Upgrade

| Command | Description |
|---------|-------------|
| `cora upgrade --check` | Check for a newer release; no download |
| `cora upgrade` | Detect OS/arch, download the latest GitHub release asset, verify its SHA-256 checksum, and replace the running binary |
| `cora upgrade -y` | Same, but skip the confirmation prompt (CI/automation) |

Notes:

- Background update notification: a non-blocking check runs on startup, cached for 24 h at `~/.codecora/cora-code/update-cache.json`. Notices go to stderr only. Disable with the `CORA_NO_UPDATE_CHECK=1` environment variable.
- If the running binary came from `cargo install --path .`, prefer re-running that instead of `cora upgrade` (the release asset would shadow your source build).

## Quick Examples

```bash
Expand Down
1 change: 1 addition & 0 deletions src/commands/watch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ pub fn run_watch(
.map(|c| c.brain.embedding.to_string())
.unwrap_or_else(|| "auto".to_string());
crate::embed::resolve_backend(&brain_mode);
crate::index::vector::apply_config_store(config.as_ref());

let skip_ref: Option<&[String]> = skip_patterns.as_deref();

Expand Down
109 changes: 108 additions & 1 deletion src/config/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,8 @@ pub struct CoraFile {
pub profile: Option<crate::engine::profiles::ProfileRef>,
#[serde(skip_serializing_if = "Option::is_none")]
pub analysis: Option<AnalysisConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub brain: Option<BrainSection>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
Expand Down Expand Up @@ -445,8 +447,33 @@ pub struct AnalysisConfig {
/// By default (`auto`), cora selects the best available backend at runtime:
/// pretrained 768d (if compiled with `pretrained-embed` feature) → hashing 256d fallback.
/// Users can force a specific backend via `.cora.yaml`.
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
fn default_vector_store() -> String {
"usearch".to_string()
}

fn default_vector_bits() -> String {
"residual".to_string()
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct BrainConfig {
/// Vector store backend for Brain Mode search.
///
/// - `"usearch"` (default) — HNSW graph, f32
/// - `"vecq"` — quantized brute-force scan (pure Rust, deterministic,
/// ~5x smaller index; recall trade absorbed by RRF fusion)
#[serde(default = "default_vector_store")]
pub vector_store: String,
/// Quantization width for the `vecq` store (ignored by `usearch`).
///
/// - `"residual"` (default) — 4-bit base + second-pass residual codes:
/// best recall at 4-bit scan speed on cora's default embeddings
/// - `"4"` / `"5"` / `"6"` — plain Lloyd-Max width
///
/// Invalid values fall back to `"residual"`. Changing the width rebuilds
/// the index once on the next `cora index`.
#[serde(default = "default_vector_bits")]
pub vector_bits: String,
/// Embedding backend selection.
///
/// - `"auto"` (default) — best available: pretrained → hashing
Expand All @@ -458,6 +485,31 @@ pub struct BrainConfig {
pub embedding: BrainEmbeddingMode,
}

// Manual Default so `Config::default()` carries the documented values
// ("usearch"/"residual") — the derived one would leave empty strings, and
// the store/width parsers only meet those values leniently.
impl Default for BrainConfig {
fn default() -> Self {
Self {
vector_store: default_vector_store(),
vector_bits: default_vector_bits(),
embedding: BrainEmbeddingMode::default(),
}
}
}

/// `.cora.yaml` `brain:` section — mirrors the subset of [`BrainConfig`]
/// that is meaningful in a config file.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BrainSection {
#[serde(skip_serializing_if = "Option::is_none")]
pub vector_store: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub vector_bits: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub embedding: Option<BrainEmbeddingMode>,
}

/// Embedding backend mode for Brain Mode.
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
Expand Down Expand Up @@ -766,6 +818,19 @@ impl CoraFile {
.clone_from(&analysis.entry_point_patterns);
}
}
// Merge brain config (Brain Mode vector store / embedding backend).
// Field-wise like the other sections: unset fields keep Config defaults.
if let Some(brain) = &self.brain {
if let Some(v) = &brain.vector_store {
config.brain.vector_store.clone_from(v);
}
if let Some(v) = &brain.vector_bits {
config.brain.vector_bits.clone_from(v);
}
if let Some(v) = &brain.embedding {
config.brain.embedding = v.clone();
}
}
Ok(())
}
}
Expand Down Expand Up @@ -837,6 +902,48 @@ mod tests {
assert_eq!(cfg.output.format, "pretty");
}

#[test]
fn brain_vector_bits_defaults_to_residual() {
let cfg = Config::default();
assert_eq!(cfg.brain.vector_bits, "residual");
assert_eq!(cfg.brain.vector_store, "usearch");
}

#[test]
fn merge_brain_vector_store_and_bits() {
let mut cfg = Config::default();
let cora = CoraFile::from_str(
r"
brain:
vector_store: vecq
vector_bits: '6'
embedding: hashing
",
)
.unwrap();

cora.merge_into(&mut cfg).unwrap();

assert_eq!(cfg.brain.vector_store, "vecq");
assert_eq!(cfg.brain.vector_bits, "6");
}

#[test]
fn merge_brain_vector_bits_absent_keeps_default() {
let mut cfg = Config::default();
let cora = CoraFile::from_str(
r"
brain:
vector_store: vecq
",
)
.unwrap();

cora.merge_into(&mut cfg).unwrap();

assert_eq!(cfg.brain.vector_bits, "residual");
}

#[test]
fn merge_provider_overrides() {
let mut cfg = Config::default();
Expand Down
Loading
Loading