Skip to content

fix(cache): absolutize local sources before computing the index cache key - #105

Merged
amondnet merged 3 commits into
mainfrom
amondnet/100-canonicalize-cache-key-source
Sep 4, 2026
Merged

fix(cache): absolutize local sources before computing the index cache key#105
amondnet merged 3 commits into
mainfrom
amondnet/100-canonicalize-cache-key-source

Conversation

@amondnet

@amondnet amondnet commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Summary

resolve_cache_dir hashed the raw source string the CLI/MCP received after a purely lexical normalize_posix, while from_path recorded std::path::absolute in the manifest. Every csp search run with the default . therefore shared one cache key across all repos, which forced a full rebuild on each switch and seeded the incremental-reuse path from the other repo's manifest. The same repo reached through different spellings got separate entries.

normalize_source in crates/csp/src/indexing/cache.rs now makes local paths absolute with std::path::absolute before path-normalizing. That call touches no filesystem and is the same one the MCP in-memory key and the manifest already use. ., ./r/../r, and /abs/r now share one cache leaf, and the keyed form equals the manifest sourceId. Git URLs stay verbatim.

This mirrors upstream semble cache_key's Path.resolve(). ADR-0002 already specified "normalized absolute path for local sources", so this is an implementation drift fix and needs no ADR change. Entries keyed from the old relative form become unreachable; clear index and clear orphans clean them up. A drift note is added to .please/docs/references/semble.md.

Related issue

Closes #100

Related: #87 / #102 (clear orphans), where this was found.

Checklist

  • PR title follows Conventional Commits
  • Tests added or updated, and the suite passes (cargo test --workspace — 333 lib + 26 CLI pass)
  • Lint/format pass (cargo fmt, cargo clippy --all-targets --all-features -- -D warnings)
  • Documentation updated if behavior changed (drift note in .please/docs/references/semble.md)
  • No breaking change, or a BREAKING CHANGE: note is included

Tests

Three new unit tests: relative and absolute forms share a key (including a ./sub/../sub detour), two relative names key by their absolute forms without colliding, and git URLs are not absolutized.

End-to-end verification

Ran the built binary against a throwaway HOME:

  • Two repos searched with . produce two cache entries (previously one).
  • Searching ../repo-a reuses repo-a's entry, so no third entry appears.
  • Deleting repo-b and running csp clear orphans reclaims exactly that entry.

Summary by cubic

Local source cache keys now use absolute, normalized paths instead of raw relative spellings, so searches from different repositories using . no longer collide and equivalent path spellings reuse one entry. MCP session keys use the same normalizer, while every remote accepted by is_git_url remains verbatim.

Written for commit 221a6db. Summary will update on new commits.

… key

`resolve_cache_dir` hashed the raw source string the CLI/MCP received after a
purely lexical normalize, while `from_path` recorded `std::path::absolute` in
the manifest. Every `csp search` run with the default `.` therefore shared one
cache key across all repos, forcing a full rebuild on each switch, and the same
repo reached via different spellings got separate entries.

`normalize_source` now makes local paths absolute (`std::path::absolute`, no
filesystem access — the same call the MCP in-memory key and the manifest use)
before path-normalizing, so `.`, `./r/../r`, and `/abs/r` share one leaf and the
keyed form equals the manifest `sourceId`. Git URLs stay verbatim. Mirrors
upstream `cache_key`'s `Path.resolve()`; ADR-0002 already specified this.
Entries keyed from the old relative form become unreachable and are cleaned by
`clear index` / `clear orphans`.

Closes #100
@codacy-production

codacy-production Bot commented Sep 4, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 11 complexity · 2 duplication

Metric Results
Complexity 11
Duplication 2

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the cache key source identity normalization logic in crates/csp/src/indexing/cache.rs to make local paths absolute against the current directory using std::path::absolute before normalizing them. This ensures that relative paths (like .) and their absolute counterparts resolve to the same cache key, preventing collisions when searching different repositories. Corresponding documentation has been updated in semble.md, and comprehensive unit tests have been added to verify the new behavior. I have no feedback to provide as the implementation is correct and well-tested.

@codecov

codecov Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 5 untouched benchmarks


Comparing amondnet/100-canonicalize-cache-key-source (221a6db) with main (fe75d1b)

Open in CodSpeed

@amondnet
amondnet marked this pull request as ready for review September 4, 2026 17:44
@greptile-apps

greptile-apps Bot commented Sep 4, 2026

Copy link
Copy Markdown

Greptile Summary

This PR makes local disk-cache identities absolute before lexical normalization, adds coverage for equivalent local path spellings, and documents the corrected cache model.

  • Prevents relative local sources such as . from sharing cache leaves across repositories.
  • Preserves URL-like sources verbatim, but misses supported SCP-style remotes whose username is not git.
  • Adds unit tests for relative/absolute equivalence, distinct repositories, and common Git URL forms.

Confidence Score: 4/5

The PR should not merge until all supported SCP-style Git remotes remain independent of the caller’s current directory.

The local-path fix works for the tested forms, but the new normalization regresses cache reuse for accepted user@host:repo remotes by turning their cache identity into a current-directory-dependent absolute path; the accompanying documentation also overstates alignment with the manifest identity.

Files Needing Attention: crates/csp/src/indexing/cache.rs, .please/docs/references/semble.md

Important Files Changed

Filename Overview
crates/csp/src/indexing/cache.rs Absolutizes local cache-key sources and adds tests, but misclassifies supported non-git SCP-style remotes as local paths.
.please/docs/references/semble.md Documents the cache-key fix, including an inaccurate claim that the normalized key source always equals the manifest source identity.

Fix all with Greploop Fix All in Claude Code

Prompt To Fix All With AI
### Issue 1
crates/csp/src/indexing/cache.rs:150
**SCP remotes become local paths**

Non-`git` SCP-style remotes such as `deploy@host:org/repo.git` are recognized as Git URLs by `is_git_url`, but this check treats them as local paths because it only exempts strings beginning with `git@`. On Unix, `std::path::absolute` then prefixes the current directory, so using the same supported remote from different directories creates separate cache entries and forces redundant index builds. Use the shared Git URL classifier here so every accepted remote syntax remains verbatim.

### Issue 2
.please/docs/references/semble.md:299-300
**Documented identities can differ**

The documentation says the normalized cache-key source equals the manifest `sourceId`, but `normalize_source` also collapses lexical `.` and `..` components while `from_path` stores only the output of `std::path::absolute`. For inputs such as `.` or `./sub/../sub`, those strings differ. This gives maintainers an inaccurate cache invariant, so the statement should be corrected or the normalization paths aligned.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "fix(cache): absolutize local sources bef..." | Re-trigger Greptile

Comment thread crates/csp/src/indexing/cache.rs Outdated
Comment thread .please/docs/references/semble.md Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files

Architecture diagram
sequenceDiagram
    participant CLI as CLI/MCP/SDK
    participant Cache as cache.rs
    participant FS as Filesystem
    participant Manifest as Manifest store
    participant Index as Index cache store

    Note over CLI,Index: Local source normalization flow

    CLI->>Cache: resolve_cache_dir(source, contentType)
    Cache->>Cache: is_url_scheme(source)?
    alt URL or scp-style git source
        Cache-->>CLI: Return key from verbatim source
    else Local path
        Cache->>FS: std::path::absolute(source)
        FS-->>Cache: Absolute path
        Cache->>Cache: normalize_posix(abs_path)
        Cache->>Index: Compute sha256 key from normalized path
        Index-->>Cache: Cache leaf path
        Cache-->>CLI: Return cache directory
        CLI->>Manifest: Record sourceId = absolute normalized path
    end

    Note over CLI,Index: In-memory MCP key path
    MCP->>Cache: resolve_cache_dir(".")
    Cache->>FS: std::path::absolute(".")
    FS-->>Cache: /current/working/dir
    Cache->>Cache: normalize_posix(/current/working/dir)
    Cache-->>MCP: Same cache leaf as manifest sourceId

    Note over CLI,Index: Different repos with default "."
    CLI->>Cache: resolve_cache_dir(".") in repo-a
    Cache->>FS: std::path::absolute(".")
    FS-->>Cache: /repos/repo-a
    Cache->>Index: Key from /repos/repo-a
    CLI->>Cache: resolve_cache_dir(".") in repo-b
    Cache->>FS: std::path::absolute(".")
    FS-->>Cache: /repos/repo-b
    Cache->>Index: Key from /repos/repo-b
    Note over Index: Two separate cache entries<br/>(previously one shared entry)
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread crates/csp/src/indexing/cache.rs Outdated
…s sweepable

- Route `IndexCache::compute_key` through `cache::normalize_source` so `.`,
  `./r/../r`, and `/abs/r` name one in-memory session entry as well as one
  on-disk cache key (no duplicate `Arc<CspIndex>` per spelling, `evict`
  can't miss).
- Fold `\` into `/` only on Windows: on Unix a backslash is an ordinary
  filename byte, so `/repos/a\b` and `/repos/a/b` must keep distinct keys and
  `..\y` must not synthesize a `..` segment.
- `source_is_gone` now requires both the recorded `sourceId` and its
  lexically normalized form to be `NotFound`: `from_path` records
  `std::path::absolute`, which keeps `..`, so deleting only an intermediate
  directory must not make a still-present source look orphaned.
…ache key

`normalize_source` exempted only `git@…` prefixes, so a non-`git` scp remote
such as `deploy@host:org/repo.git` — which `is_git_url` accepts and
`load_or_build_index` clones — was absolutized against the caller's cwd and
keyed differently from every directory. Route the check through the shared
classifier, and correct the semble drift note that claimed the keyed form
equals the manifest `sourceId`.
@sonarqubecloud

sonarqubecloud Bot commented Sep 4, 2026

Copy link
Copy Markdown

@amondnet
amondnet merged commit 7cb0b01 into main Sep 4, 2026
12 of 13 checks passed
@amondnet
amondnet deleted the amondnet/100-canonicalize-cache-key-source branch September 4, 2026 18:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(cache): canonicalize local source before computing the index cache key (. collides across repos)

1 participant