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
18 changes: 17 additions & 1 deletion .please/docs/references/semble.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,22 @@ Ported faithfully (`LazyLock<Regex>` for the static patterns, `RefCell<HashMap>`
(NFR-003), tightening pre-existing dirs on Unix.
- `clear_index_cache` removes only the index dir — never the `~/.csp` home (which also holds
`savings.jsonl`).
- `clear_orphan_indexes` (← upstream `cli.py::_clear_orphans`, #243) removes per-source leaves
whose manifest `sourceId` is a local path that is now `NotFound`. Same root guard as
`clear_index_cache`; a leaf is considered only when its directory name has the cache-key
shape (32 lowercase hex, mirroring upstream's `_SHA_256_REGEX`), so stray directories are
never swept. **Drift note:** upstream can guard with `cache_key(root_path) == dir name`
because its `cache_key` resolves the path (`Path(p).expanduser().resolve()`); csp's
`resolve_cache_dir` normalizes only lexically while the manifest records
`std::path::absolute`, so the two never agree for a relative source (`csp search "q" .`) —
the key must not be re-derived here. Git leaves are excluded by `is_git_url(sourceId)`
(`from_git` re-roots the manifest to the URL, unlike upstream, which stores the temp clone
dir). Relative `sourceId`s are skipped (they would resolve against the caller's cwd), and
only a `NotFound` counts as gone — a source that errors for another reason (unreadable
parent, stale network handle) keeps its cache; an unmounted volume's path is simply absent
and is swept like upstream. Traversal errors in the index root fail the sweep instead of
being skipped. Exposed as `csp clear orphans`; not part of `clear all` (matches
upstream).
- **Cache validity** (`try_reuse`): a cached index is reused only when the manifest's
`chunk_size` equals the current `DESIRED_CHUNK_LENGTH_CHARS` (a manifest predating the field
→ `None` → rebuild) **and**, for local sources, the live source-file content hash matches.
Expand Down Expand Up @@ -336,7 +352,7 @@ Clean two-layer split:
### 4.17 `csp/src/bin/csp/main.rs` — CLI (clap)

- `#[derive(Parser)]` with a `Command` `#[derive(Subcommand)]` enum: **search**, **find-related**,
**index** (build + persist a standalone index), **savings**, **clear** (`all|index|savings`),
**index** (build + persist a standalone index), **savings**, **clear** (`all|index|savings|orphans`),
**init** (write an agent file), **mcp** (run the stdio server).
- `search` / `find-related` route through `load_or_build_index` (or an explicit `--index` via
`LoadOptions`). Output is the snake_case wire JSON (`utils::format_results`).
Expand Down
3 changes: 3 additions & 0 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -451,10 +451,13 @@ stdout이 컬러를 지원하는 TTY일 때 출력에 색이 입혀집니다(`NO
csp clear savings # ~/.csp/savings.jsonl 삭제
csp clear index # 글로벌 인덱스 캐시 ~/.csp/index/ 삭제
csp clear all # 인덱스 캐시와 savings 모두 삭제
csp clear orphans # 소스 디렉터리가 더 이상 없는 캐시 인덱스만 삭제
```

`clear index`는 글로벌 인덱스 캐시 `~/.csp/index/`(여기에 `csp search`/`find-related`가 인덱스를 자동 캐시합니다)를 삭제하고 제거된 캐시 엔트리 수를 보고합니다. `~/.csp/savings.jsonl`은 보존됩니다. `clear all`은 `~/.csp/index/`와 `~/.csp/savings.jsonl`을 각각 독립적으로 삭제합니다.

`clear orphans`는 `~/.csp/index/`를 순회하며 기록된 로컬 소스 경로가 더 이상 존재하지 않는 엔트리(예: 삭제했거나 옮긴 저장소)만 제거합니다. git URL로 만든 인덱스는 orphan으로 취급하지 않으며, 지금 당장 읽을 수 없을 뿐인 소스(예: 권한 오류)도 그대로 둡니다. 다만 마운트 해제되거나 연결이 끊긴 볼륨 위의 소스는 삭제된 소스와 구분할 수 없어 삭제 대상이 되므로, `clear orphans`를 실행하기 전에 다시 마운트하세요. `clear orphans`는 별도의 선택지이며 `clear all`에는 포함되지 않습니다.

`csp index -o <경로>`로 명시적으로 기록한 인덱스 경로는 자동 캐시 대상이 아니므로 `clear`가 건드리지 않습니다. 해당 디렉터리는 직접 삭제하세요.

</details>
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -451,10 +451,13 @@ Stats are stored in `~/.csp/savings.jsonl`.
csp clear savings # delete ~/.csp/savings.jsonl
csp clear index # delete the global index cache at ~/.csp/index/
csp clear all # delete both the index cache and savings
csp clear orphans # delete cached indexes whose source directory no longer exists
```

`clear index` removes the global index cache at `~/.csp/index/` (where `csp search`/`find-related` auto-cache indexes) and reports how many cached entries were removed; your `~/.csp/savings.jsonl` is preserved. `clear all` removes both `~/.csp/index/` and `~/.csp/savings.jsonl` as two independent actions.

`clear orphans` walks `~/.csp/index/` and removes only the entries whose recorded local source path no longer exists (for example, a repo you deleted or moved). Indexes built from git URLs are never treated as orphans, and a source that merely cannot be read right now (for example, a permission error) is left alone. A source on an unmounted or unplugged volume is indistinguishable from a deleted one and is swept — remount it before running `clear orphans`. `clear orphans` is a separate choice — `clear all` does not run it.

Explicit index paths written with `csp index -o <path>` are not part of the auto-cache, so `clear` never touches them — delete those directories yourself.

</details>
Expand Down
71 changes: 67 additions & 4 deletions crates/csp/src/bin/csp/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
use std::process::ExitCode;

use clap::{Parser, Subcommand, ValueEnum};
use csp::indexing::cache::{clear_index_cache, CacheLocation};
use csp::indexing::cache::{clear_index_cache, clear_orphan_indexes, CacheLocation};
use csp::indexing::index::{
load_or_build_index, CspIndex, LoadOptions, LoadOrBuildOptions, QueryOptions,
};
Expand Down Expand Up @@ -120,12 +120,13 @@
},
/// Clear cached data.
Clear {
/// One of: all, index, savings.
/// One of: all, index, savings, orphans. `orphans` removes cached
/// indexes whose source path no longer exists (not part of `all`).
what: String,
},
}

const CLEAR_CHOICES: &str = "all, index, savings";
const CLEAR_CHOICES: &str = "all, index, savings, orphans";

/// Process exit codes returned by `dispatch` / `run_clear` (mapped to
/// `ExitCode` in `run`). Plain `u8` so tests can assert on them directly.
Expand Down Expand Up @@ -325,8 +326,8 @@

/// `run_clear` with the cache location and savings file injected, so tests can
/// exercise the destructive branches against temp dirs instead of real `~/.csp`.
fn run_clear_at(what: &str, cache_loc: &CacheLocation, stats_file: &Path) -> u8 {

Check failure on line 329 in crates/csp/src/bin/csp/main.rs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 18 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=pleaseai_code-search&issues=AaBsocybEeFUPdCmjvut&open=AaBsocybEeFUPdCmjvut&pullRequest=102
if !["all", "index", "savings"].contains(&what) {
if !["all", "index", "savings", "orphans"].contains(&what) {
eprintln!("Invalid clear type: {what}. Choices: {CLEAR_CHOICES}");
return EXIT_FAILURE;
}
Expand Down Expand Up @@ -357,6 +358,21 @@
println!("No savings file found at `{}`", path.display());
}
}
// Mirrors upstream: `orphans` is its own choice, never folded into `all`.
if what == "orphans" {
match clear_orphan_indexes(cache_loc) {
Ok(removed) if removed.is_empty() => println!("No orphaned indexes found"),
Ok(removed) => {
for orphan in removed {
println!("Cleared orphaned index for `{}`", orphan.source_id);
}
}
Err(e) => {
eprintln!("{e}");
failed = true;
}
}
}
if failed {
EXIT_FAILURE
} else {
Expand Down Expand Up @@ -674,6 +690,53 @@
assert_eq!(run_clear_at("index", &loc, &stats), EXIT_SUCCESS);
assert_eq!(run_clear_at("savings", &loc, &stats), EXIT_SUCCESS);
assert_eq!(run_clear_at("all", &loc, &stats), EXIT_SUCCESS);
assert_eq!(run_clear_at("orphans", &loc, &stats), EXIT_SUCCESS);
}

#[test]
fn run_clear_at_orphans_removes_only_dead_sources() {
use csp::indexing::cache::{resolve_cache_dir, resolve_index_root};
use csp::indexing::index::{IndexManifest, INDEX_SCHEMA_VERSION};

let home = tempdir().unwrap();
let loc = CacheLocation {
base_dir: Some(home.path().join(".csp")),
..Default::default()
};
let stats = home.path().join("savings.jsonl");
let write_entry = |source: &Path| -> PathBuf {
let dir = resolve_cache_dir(&source.to_string_lossy(), &[ContentType::Code], &loc);
std::fs::create_dir_all(&dir).unwrap();
let manifest = IndexManifest {
schema_version: INDEX_SCHEMA_VERSION,
content_hash: "hash".to_string(),
source_id: Some(source.to_string_lossy().into_owned()),
content: vec![ContentType::Code],
model_id: "model".to_string(),
model_kind: Some("stub".to_string()),
chunk_size: Some(750),
files: Default::default(),
};
std::fs::write(
dir.join("manifest.json"),
serde_json::to_string(&manifest).unwrap(),
)
.unwrap();
dir
};
let live = home.path().join("live");
std::fs::create_dir_all(&live).unwrap();
let live_dir = write_entry(&live);
let dead_dir = write_entry(&home.path().join("gone"));

assert_eq!(run_clear_at("orphans", &loc, &stats), EXIT_SUCCESS);
assert!(live_dir.exists());
assert!(!dead_dir.exists());
// `all` never sweeps orphans on its own; it removes the whole root,
// live entries included.
assert_eq!(run_clear_at("all", &loc, &stats), EXIT_SUCCESS);
assert!(!live_dir.exists());
assert!(!resolve_index_root(&loc).exists());
}

#[test]
Expand Down
Loading
Loading